+
{audioMuted && !(videoMuted && !noVideo) && }
{videoMuted && !noVideo && }
{showName && {name}}
)
)}
+ {showOptions && (
+
+
+
+ )}
);
diff --git a/src/video-grid/VideoTile.module.css b/src/video-grid/VideoTile.module.css
index eda83ca2..3bc7aa83 100644
--- a/src/video-grid/VideoTile.module.css
+++ b/src/video-grid/VideoTile.module.css
@@ -44,10 +44,8 @@
object-fit: contain;
}
-.memberName {
+.infoBubble {
position: absolute;
- bottom: 16px;
- left: 16px;
height: 24px;
padding: 0 8px;
color: white;
@@ -62,6 +60,21 @@
z-index: 1;
}
+.optionsButton {
+ right: 16px;
+ top: 16px;
+}
+
+.optionsButton button svg {
+ height: 20px;
+ width: 20px;
+}
+
+.memberName {
+ left: 16px;
+ bottom: 16px;
+}
+
.memberName > * {
margin-right: 6px;
}
diff --git a/src/video-grid/VideoTileContainer.jsx b/src/video-grid/VideoTileContainer.jsx
index 8fb4f803..be7567ef 100644
--- a/src/video-grid/VideoTileContainer.jsx
+++ b/src/video-grid/VideoTileContainer.jsx
@@ -20,6 +20,8 @@ import { useCallFeed } from "./useCallFeed";
import { useSpatialMediaStream } from "./useMediaStream";
import { useRoomMemberName } from "./useRoomMemberName";
import { VideoTile } from "./VideoTile";
+import { VideoTileSettingsModal } from "./VideoTileSettingsModal";
+import { useModalTriggerState } from "../Modal";
export function VideoTileContainer({
item,
@@ -37,6 +39,7 @@ export function VideoTileContainer({
isLocal,
audioMuted,
videoMuted,
+ localVolume,
noVideo,
speaking,
stream,
@@ -49,26 +52,44 @@ export function VideoTileContainer({
audioOutputDevice,
audioContext,
audioDestination,
- isLocal
+ isLocal,
+ localVolume
);
+ const {
+ modalState: videoTileSettingsModalState,
+ modalProps: videoTileSettingsModalProps,
+ } = useModalTriggerState();
+ const onOptionsPress = () => {
+ videoTileSettingsModalState.open();
+ };
// Firefox doesn't respect the disablePictureInPicture attribute
// https://bugzilla.mozilla.org/show_bug.cgi?id=1611831
return (
-
+ <>
+
+ {videoTileSettingsModalState.isOpen && (
+
+ )}
+ >
);
}
diff --git a/src/video-grid/VideoTileSettingsModal.module.css b/src/video-grid/VideoTileSettingsModal.module.css
new file mode 100644
index 00000000..8edff8a3
--- /dev/null
+++ b/src/video-grid/VideoTileSettingsModal.module.css
@@ -0,0 +1,70 @@
+.content {
+ margin: 27px 34px;
+}
+
+.localVolumePercentage {
+ width: 3ch;
+}
+
+.localVolumeSlider[type="range"] {
+ -ms-appearance: none;
+ -moz-appearance: none;
+ -webkit-appearance: none;
+ appearance: none;
+
+ cursor: pointer;
+ width: 100%;
+}
+
+.localVolumeSlider[type="range"]::-moz-range-track {
+ -moz-appearance: none;
+ appearance: none;
+
+ height: 4px;
+}
+.localVolumeSlider[type="range"]::-ms-track {
+ -ms-appearance: none;
+ appearance: none;
+
+ height: 4px;
+}
+.localVolumeSlider[type="range"]::-webkit-slider-runnable-track {
+ -webkit-appearance: none;
+ appearance: none;
+
+ height: 4px;
+}
+
+.localVolumeSlider[type="range"]::-moz-range-thumb {
+ -moz-appearance: none;
+ appearance: none;
+
+ height: 16px;
+ width: 16px;
+ margin-top: -6px;
+
+ border-radius: 100%;
+ background: var(--accent);
+}
+.localVolumeSlider[type="range"]::-ms-thumb {
+ -ms-appearance: none;
+ appearance: none;
+
+ height: 16px;
+ width: 16px;
+ margin-top: -6px;
+
+ border-radius: 100%;
+ background: var(--accent);
+}
+.localVolumeSlider[type="range"]::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+
+ height: 16px;
+ width: 16px;
+ margin-top: -6px;
+
+ border-radius: 100%;
+ background: var(--accent);
+}
diff --git a/src/video-grid/VideoTileSettingsModal.tsx b/src/video-grid/VideoTileSettingsModal.tsx
new file mode 100644
index 00000000..f76721d1
--- /dev/null
+++ b/src/video-grid/VideoTileSettingsModal.tsx
@@ -0,0 +1,74 @@
+/*
+Copyright 2022 New Vector Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, { ChangeEvent, useState } from "react";
+import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
+
+import selectInputStyles from "../input/SelectInput.module.css";
+import { FieldRow } from "../input/Input";
+import { Modal } from "../Modal";
+import styles from "./VideoTileSettingsModal.module.css";
+
+interface LocalVolumeProps {
+ feed: CallFeed;
+}
+
+const LocalVolume: React.FC
= ({
+ feed,
+}: LocalVolumeProps) => {
+ const [localVolume, setLocalVolume] = useState(feed.getLocalVolume());
+
+ const onLocalVolumeChanged = (event: ChangeEvent) => {
+ const value: number = +event.target.value;
+ setLocalVolume(value);
+ feed.setLocalVolume(value);
+ };
+
+ return (
+ <>
+ Local Volume
+
+
+ {`${Math.round(localVolume * 100)}%`}
+
+
+
+ >
+ );
+};
+
+// TODO: Extend ModalProps
+interface Props {
+ feed: CallFeed;
+}
+
+export const VideoTileSettingsModal = ({ feed, ...rest }: Props) => {
+ return (
+
+
+
+
+
+ );
+};
diff --git a/src/video-grid/useCallFeed.js b/src/video-grid/useCallFeed.js
index b294f589..963d48ac 100644
--- a/src/video-grid/useCallFeed.js
+++ b/src/video-grid/useCallFeed.js
@@ -27,6 +27,7 @@ function getCallFeedState(callFeed) {
: true,
videoMuted: callFeed ? callFeed.isVideoMuted() : true,
audioMuted: callFeed ? callFeed.isAudioMuted() : true,
+ localVolume: callFeed ? callFeed.getLocalVolume() : 0,
stream: callFeed ? callFeed.stream : undefined,
purpose: callFeed ? callFeed.purpose : undefined,
};
@@ -44,6 +45,10 @@ export function useCallFeed(callFeed) {
setState((prevState) => ({ ...prevState, audioMuted, videoMuted }));
}
+ function onLocalVolumeChanged(localVolume) {
+ setState((prevState) => ({ ...prevState, localVolume }));
+ }
+
function onUpdateCallFeed() {
setState(getCallFeedState(callFeed));
}
@@ -51,6 +56,7 @@ export function useCallFeed(callFeed) {
if (callFeed) {
callFeed.on(CallFeedEvent.Speaking, onSpeaking);
callFeed.on(CallFeedEvent.MuteStateChanged, onMuteStateChanged);
+ callFeed.on(CallFeedEvent.LocalVolumeChanged, onLocalVolumeChanged);
callFeed.on(CallFeedEvent.NewStream, onUpdateCallFeed);
}
@@ -63,6 +69,10 @@ export function useCallFeed(callFeed) {
CallFeedEvent.MuteStateChanged,
onMuteStateChanged
);
+ callFeed.removeListener(
+ CallFeedEvent.LocalVolumeChanged,
+ onLocalVolumeChanged
+ );
callFeed.removeListener(CallFeedEvent.NewStream, onUpdateCallFeed);
}
};
diff --git a/src/video-grid/useMediaStream.ts b/src/video-grid/useMediaStream.ts
index 5fbf57d0..d3df2372 100644
--- a/src/video-grid/useMediaStream.ts
+++ b/src/video-grid/useMediaStream.ts
@@ -33,7 +33,8 @@ declare global {
export const useMediaStream = (
stream: MediaStream,
audioOutputDevice: string,
- mute = false
+ mute = false,
+ localVolume: number
): RefObject => {
const mediaRef = useRef();
@@ -84,6 +85,13 @@ export const useMediaStream = (
}
}, [audioOutputDevice]);
+ useEffect(() => {
+ if (!mediaRef.current) return;
+ if (localVolume === null || localVolume === undefined) return;
+
+ mediaRef.current.volume = localVolume;
+ }, [localVolume]);
+
useEffect(() => {
const mediaEl = mediaRef.current;
@@ -187,7 +195,8 @@ export const useSpatialMediaStream = (
audioOutputDevice: string,
audioContext: AudioContext,
audioDestination: AudioNode,
- mute = false
+ mute = false,
+ localVolume: number
): [RefObject, RefObject] => {
const tileRef = useRef();
const [spatialAudio] = useSpatialAudio();
@@ -195,9 +204,11 @@ export const useSpatialMediaStream = (
const mediaRef = useMediaStream(
stream,
audioOutputDevice,
- spatialAudio || mute
+ spatialAudio || mute,
+ localVolume
);
+ const gainNodeRef = useRef();
const pannerNodeRef = useRef();
const sourceRef = useRef();
@@ -214,12 +225,18 @@ export const useSpatialMediaStream = (
refDistance: 3,
});
}
+ if (!gainNodeRef.current) {
+ gainNodeRef.current = new GainNode(audioContext, {
+ gain: localVolume,
+ });
+ }
if (!sourceRef.current) {
sourceRef.current = audioContext.createMediaStreamSource(stream);
}
const tile = tileRef.current;
const source = sourceRef.current;
+ const gainNode = gainNodeRef.current;
const pannerNode = pannerNodeRef.current;
const updatePosition = () => {
@@ -234,8 +251,9 @@ export const useSpatialMediaStream = (
pannerNodeRef.current.positionZ.value = -2;
};
+ gainNode.gain.value = localVolume;
updatePosition();
- source.connect(pannerNode).connect(audioDestination);
+ source.connect(gainNode).connect(pannerNode).connect(audioDestination);
// HACK: We abuse the CSS transitionrun event to detect when the tile
// moves, because useMeasure, IntersectionObserver, etc. all have no
// ability to track changes in the CSS transform property
@@ -244,10 +262,11 @@ export const useSpatialMediaStream = (
return () => {
tile.removeEventListener("transitionrun", updatePosition);
source.disconnect();
+ gainNode.disconnect();
pannerNode.disconnect();
};
}
- }, [stream, spatialAudio, audioContext, audioDestination, mute]);
+ }, [stream, spatialAudio, audioContext, audioDestination, mute, localVolume]);
return [tileRef, mediaRef];
};
diff --git a/yarn.lock b/yarn.lock
index 3d0a625c..3629bb99 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8392,11 +8392,12 @@ matrix-events-sdk@^0.0.1-beta.7:
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1-beta.7.tgz#5ffe45eba1f67cc8d7c2377736c728b322524934"
integrity sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA==
-"matrix-js-sdk@github:matrix-org/matrix-js-sdk#984dd26a138411ef73903ff4e635f2752e0829f2":
- version "18.1.0"
- resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/984dd26a138411ef73903ff4e635f2752e0829f2"
+"matrix-js-sdk@github:matrix-org/matrix-js-sdk#8ba2d257ae24bbed61cd7fe99af081324337161c":
+ version "19.0.0"
+ resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/8ba2d257ae24bbed61cd7fe99af081324337161c"
dependencies:
"@babel/runtime" "^7.12.5"
+ "@types/sdp-transform" "^2.4.5"
another-json "^0.2.0"
browser-request "^0.3.3"
bs58 "^4.0.1"
@@ -8406,6 +8407,7 @@ matrix-events-sdk@^0.0.1-beta.7:
p-retry "^4.5.0"
qs "^6.9.6"
request "^2.88.2"
+ sdp-transform "^2.14.1"
unhomoglyph "^1.0.6"
matrix-widget-api@^0.1.0-beta.18: