mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-22 22:29:30 +00:00
Add button to disable incoming video
This commit is contained in:
@@ -137,6 +137,7 @@
|
|||||||
"hangup_button_label": "End call",
|
"hangup_button_label": "End call",
|
||||||
"header_label": "Element Call Home",
|
"header_label": "Element Call Home",
|
||||||
"header_participants_label": "Participants",
|
"header_participants_label": "Participants",
|
||||||
|
"hide_other_videos_button_label": "Hide other participants' video",
|
||||||
"invite_modal": {
|
"invite_modal": {
|
||||||
"link_copied_toast": "Link copied to clipboard",
|
"link_copied_toast": "Link copied to clipboard",
|
||||||
"title": "Invite to this call"
|
"title": "Invite to this call"
|
||||||
@@ -248,6 +249,7 @@
|
|||||||
"resolution_label": "Resolution",
|
"resolution_label": "Resolution",
|
||||||
"screen_share_header": "Screen sharing"
|
"screen_share_header": "Screen sharing"
|
||||||
},
|
},
|
||||||
|
"show_other_videos_button_label": "Show other participants' video",
|
||||||
"star_rating_input_label_one": "{{count}} star",
|
"star_rating_input_label_one": "{{count}} star",
|
||||||
"star_rating_input_label_other": "{{count}} stars",
|
"star_rating_input_label_other": "{{count}} stars",
|
||||||
"start_new_call": "Start new call",
|
"start_new_call": "Start new call",
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import {
|
|||||||
OverflowVerticalIcon,
|
OverflowVerticalIcon,
|
||||||
VolumeOnSolidIcon,
|
VolumeOnSolidIcon,
|
||||||
VolumeOffSolidIcon,
|
VolumeOffSolidIcon,
|
||||||
|
VisibilityOnIcon,
|
||||||
|
VisibilityOffIcon,
|
||||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||||
|
|
||||||
import styles from "./Button.module.css";
|
import styles from "./Button.module.css";
|
||||||
@@ -103,6 +105,37 @@ export const VideoButton: FC<VideoButtonProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface DisableRemoteVideoButtonProps
|
||||||
|
extends ComponentPropsWithoutRef<"button"> {
|
||||||
|
/** Whether other participants' video is currently being shown. */
|
||||||
|
enabled: boolean;
|
||||||
|
size?: "md" | "lg";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DisableRemoteVideoButton: FC<DisableRemoteVideoButtonProps> = ({
|
||||||
|
enabled,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const Icon = enabled ? VisibilityOnIcon : VisibilityOffIcon;
|
||||||
|
const label = enabled
|
||||||
|
? t("hide_other_videos_button_label")
|
||||||
|
: t("show_other_videos_button_label");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip label={label}>
|
||||||
|
<CpdButton
|
||||||
|
iconOnly
|
||||||
|
Icon={Icon}
|
||||||
|
kind={enabled ? "secondary" : "primary"}
|
||||||
|
role="switch"
|
||||||
|
aria-checked={enabled}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface ShareScreenButtonProps extends ComponentPropsWithoutRef<"button"> {
|
interface ShareScreenButtonProps extends ComponentPropsWithoutRef<"button"> {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
size: "md" | "lg";
|
size: "md" | "lg";
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
ReactionToggleButton,
|
ReactionToggleButton,
|
||||||
LoudspeakerButton,
|
LoudspeakerButton,
|
||||||
SettingsIconButton,
|
SettingsIconButton,
|
||||||
|
DisableRemoteVideoButton,
|
||||||
type ReactionData,
|
type ReactionData,
|
||||||
} from "../button";
|
} from "../button";
|
||||||
import styles from "./CallFooter.module.css";
|
import styles from "./CallFooter.module.css";
|
||||||
@@ -58,6 +59,8 @@ export interface FooterActions {
|
|||||||
toggleVideo: (() => void) | undefined;
|
toggleVideo: (() => void) | undefined;
|
||||||
toggleBlur: (() => void) | undefined;
|
toggleBlur: (() => void) | undefined;
|
||||||
toggleScreenSharing: (() => void) | undefined;
|
toggleScreenSharing: (() => void) | undefined;
|
||||||
|
/** Also controls if the "hide other participants' video" button is visible */
|
||||||
|
toggleDisableRemoteVideo: (() => void) | undefined;
|
||||||
/** Also controls if the settings button is visible */
|
/** Also controls if the settings button is visible */
|
||||||
openSettings: (() => void) | undefined;
|
openSettings: (() => void) | undefined;
|
||||||
/** Also controls if the hangup button is visible */
|
/** Also controls if the hangup button is visible */
|
||||||
@@ -70,6 +73,8 @@ export interface FooterState {
|
|||||||
videoEnabled: boolean;
|
videoEnabled: boolean;
|
||||||
videoBusy: boolean;
|
videoBusy: boolean;
|
||||||
videoBlurEnabled: boolean;
|
videoBlurEnabled: boolean;
|
||||||
|
/** Whether other participants' video feeds are hidden to save bandwidth */
|
||||||
|
disableRemoteVideo: boolean;
|
||||||
showFooter: boolean;
|
showFooter: boolean;
|
||||||
|
|
||||||
/* This is needed for WindowMode = "flat" */
|
/* This is needed for WindowMode = "flat" */
|
||||||
@@ -146,6 +151,8 @@ export const CallFooter: FC<FooterProps> = ({
|
|||||||
const selectVideoButtonOption = useBehavior(vm.selectVideoButtonOption$);
|
const selectVideoButtonOption = useBehavior(vm.selectVideoButtonOption$);
|
||||||
const toggleBlur = useBehavior(vm.toggleBlur$);
|
const toggleBlur = useBehavior(vm.toggleBlur$);
|
||||||
const videoBlurEnabled = useBehavior(vm.videoBlurEnabled$);
|
const videoBlurEnabled = useBehavior(vm.videoBlurEnabled$);
|
||||||
|
const disableRemoteVideo = useBehavior(vm.disableRemoteVideo$);
|
||||||
|
const toggleDisableRemoteVideo = useBehavior(vm.toggleDisableRemoteVideo$);
|
||||||
const buttonSize = useBehavior(vm.buttonSize$);
|
const buttonSize = useBehavior(vm.buttonSize$);
|
||||||
const showLogo = useBehavior(vm.showLogo$);
|
const showLogo = useBehavior(vm.showLogo$);
|
||||||
|
|
||||||
@@ -224,6 +231,18 @@ export const CallFooter: FC<FooterProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (toggleDisableRemoteVideo !== undefined) {
|
||||||
|
buttons.push(
|
||||||
|
<DisableRemoteVideoButton
|
||||||
|
size={buttonSize}
|
||||||
|
key="disable_remote_video"
|
||||||
|
enabled={!disableRemoteVideo}
|
||||||
|
onClick={toggleDisableRemoteVideo}
|
||||||
|
data-testid="incall_disable_remote_video"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (toggleScreenSharing !== undefined) {
|
if (toggleScreenSharing !== undefined) {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<ShareScreenButton
|
<ShareScreenButton
|
||||||
|
|||||||
@@ -188,6 +188,9 @@ export function createCallFooterViewModel(
|
|||||||
sharingScreen$: callModel.sharingScreen$,
|
sharingScreen$: callModel.sharingScreen$,
|
||||||
toggleScreenSharing$: constant(callModel.toggleScreenSharing ?? undefined),
|
toggleScreenSharing$: constant(callModel.toggleScreenSharing ?? undefined),
|
||||||
|
|
||||||
|
disableRemoteVideo$: callModel.disableRemoteVideo$,
|
||||||
|
toggleDisableRemoteVideo$: constant(callModel.toggleDisableRemoteVideo),
|
||||||
|
|
||||||
audioOutputSwitcher$: scope.behavior(
|
audioOutputSwitcher$: scope.behavior(
|
||||||
callModel.audioOutputSwitcher$.pipe(
|
callModel.audioOutputSwitcher$.pipe(
|
||||||
map((switcher) => switcher ?? undefined),
|
map((switcher) => switcher ?? undefined),
|
||||||
@@ -249,10 +252,12 @@ export function createLobbyFooterViewModel(
|
|||||||
toggleAudio: undefined,
|
toggleAudio: undefined,
|
||||||
toggleVideo: undefined,
|
toggleVideo: undefined,
|
||||||
toggleScreenSharing: undefined,
|
toggleScreenSharing: undefined,
|
||||||
|
toggleDisableRemoteVideo: undefined,
|
||||||
audioEnabled: undefined,
|
audioEnabled: undefined,
|
||||||
audioBusy: false,
|
audioBusy: false,
|
||||||
videoEnabled: undefined,
|
videoEnabled: undefined,
|
||||||
videoBusy: false,
|
videoBusy: false,
|
||||||
|
disableRemoteVideo: false,
|
||||||
layoutSwitchVm: null,
|
layoutSwitchVm: null,
|
||||||
sharingScreen: false,
|
sharingScreen: false,
|
||||||
audioOutputSwitcher: undefined,
|
audioOutputSwitcher: undefined,
|
||||||
|
|||||||
@@ -268,6 +268,16 @@ export interface CallViewModel {
|
|||||||
*/
|
*/
|
||||||
sharingScreen$: Behavior<boolean>;
|
sharingScreen$: Behavior<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether other participants' video feeds are hidden (and not downloaded)
|
||||||
|
* to save bandwidth. Resets to false at the start of every call.
|
||||||
|
*/
|
||||||
|
disableRemoteVideo$: Behavior<boolean>;
|
||||||
|
/**
|
||||||
|
* Toggles disableRemoteVideo$.
|
||||||
|
*/
|
||||||
|
toggleDisableRemoteVideo: () => void;
|
||||||
|
|
||||||
// UI interactions
|
// UI interactions
|
||||||
/**
|
/**
|
||||||
* Callback for when the user taps the call view.
|
* Callback for when the user taps the call view.
|
||||||
@@ -730,6 +740,18 @@ export function createCallViewModel$(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether other participants' video feeds are hidden to save bandwidth.
|
||||||
|
* This is intentionally not persisted: it resets to false at the start of
|
||||||
|
* every call.
|
||||||
|
*/
|
||||||
|
const disableRemoteVideoToggle$ = new Subject<void>();
|
||||||
|
const disableRemoteVideo$ = createToggle$(
|
||||||
|
scope,
|
||||||
|
false,
|
||||||
|
disableRemoteVideoToggle$,
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of user media (camera feeds) that we want tiles for.
|
* List of user media (camera feeds) that we want tiles for.
|
||||||
*/
|
*/
|
||||||
@@ -771,6 +793,7 @@ export function createCallViewModel$(
|
|||||||
),
|
),
|
||||||
mediaDevices,
|
mediaDevices,
|
||||||
pretendToBeDisconnected$: localMembership.reconnecting$,
|
pretendToBeDisconnected$: localMembership.reconnecting$,
|
||||||
|
disableRemoteVideo$,
|
||||||
displayName$: scope.behavior(
|
displayName$: scope.behavior(
|
||||||
matrixMemberMetadataStore
|
matrixMemberMetadataStore
|
||||||
.createDisplayNameBehavior$(scope, userId)
|
.createDisplayNameBehavior$(scope, userId)
|
||||||
@@ -1786,6 +1809,8 @@ export function createCallViewModel$(
|
|||||||
leave: localMembership.requestDisconnect,
|
leave: localMembership.requestDisconnect,
|
||||||
toggleScreenSharing: toggleScreenSharing,
|
toggleScreenSharing: toggleScreenSharing,
|
||||||
sharingScreen$: sharingScreen$,
|
sharingScreen$: sharingScreen$,
|
||||||
|
disableRemoteVideo$: disableRemoteVideo$,
|
||||||
|
toggleDisableRemoteVideo: (): void => disableRemoteVideoToggle$.next(),
|
||||||
|
|
||||||
tapScreen: (): void => screenTap$.next(),
|
tapScreen: (): void => screenTap$.next(),
|
||||||
tapControls: (): void => controlsTap$.next(),
|
tapControls: (): void => controlsTap$.next(),
|
||||||
|
|||||||
@@ -34,11 +34,21 @@ export interface RemoteUserMediaInputs extends Omit<
|
|||||||
> {
|
> {
|
||||||
participant$: Behavior<RemoteParticipant | null>;
|
participant$: Behavior<RemoteParticipant | null>;
|
||||||
pretendToBeDisconnected$: Behavior<boolean>;
|
pretendToBeDisconnected$: Behavior<boolean>;
|
||||||
|
/**
|
||||||
|
* Whether other participants' video feeds should be hidden (and thus not
|
||||||
|
* downloaded, since LiveKit's adaptiveStream pauses tracks with no
|
||||||
|
* attached video element) to save bandwidth.
|
||||||
|
*/
|
||||||
|
disableRemoteVideo$: Behavior<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createRemoteUserMedia(
|
export function createRemoteUserMedia(
|
||||||
scope: ObservableScope,
|
scope: ObservableScope,
|
||||||
{ pretendToBeDisconnected$, ...inputs }: RemoteUserMediaInputs,
|
{
|
||||||
|
pretendToBeDisconnected$,
|
||||||
|
disableRemoteVideo$,
|
||||||
|
...inputs
|
||||||
|
}: RemoteUserMediaInputs,
|
||||||
): RemoteUserMediaViewModel {
|
): RemoteUserMediaViewModel {
|
||||||
const baseUserMedia = createBaseUserMedia(scope, {
|
const baseUserMedia = createBaseUserMedia(scope, {
|
||||||
...inputs,
|
...inputs,
|
||||||
@@ -68,6 +78,11 @@ export function createRemoteUserMedia(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
video$: scope.behavior(
|
||||||
|
combineLatest([baseUserMedia.video$, disableRemoteVideo$]).pipe(
|
||||||
|
map(([video, disabled]) => (disabled ? undefined : video)),
|
||||||
|
),
|
||||||
|
),
|
||||||
waitingForMedia$: scope.behavior(
|
waitingForMedia$: scope.behavior(
|
||||||
combineLatest(
|
combineLatest(
|
||||||
[inputs.livekitRoom$, inputs.participant$],
|
[inputs.livekitRoom$, inputs.participant$],
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ interface WrappedUserMediaInputs extends Omit<
|
|||||||
participant: TaggedParticipant;
|
participant: TaggedParticipant;
|
||||||
mediaDevices: MediaDevices;
|
mediaDevices: MediaDevices;
|
||||||
pretendToBeDisconnected$: Behavior<boolean>;
|
pretendToBeDisconnected$: Behavior<boolean>;
|
||||||
|
disableRemoteVideo$: Behavior<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createWrappedUserMedia(
|
export function createWrappedUserMedia(
|
||||||
@@ -97,6 +98,7 @@ export function createWrappedUserMedia(
|
|||||||
participant,
|
participant,
|
||||||
mediaDevices,
|
mediaDevices,
|
||||||
pretendToBeDisconnected$,
|
pretendToBeDisconnected$,
|
||||||
|
disableRemoteVideo$,
|
||||||
...inputs
|
...inputs
|
||||||
}: WrappedUserMediaInputs,
|
}: WrappedUserMediaInputs,
|
||||||
): WrappedUserMediaViewModel {
|
): WrappedUserMediaViewModel {
|
||||||
@@ -110,6 +112,7 @@ export function createWrappedUserMedia(
|
|||||||
: createRemoteUserMedia(scope, {
|
: createRemoteUserMedia(scope, {
|
||||||
participant$: participant.value$,
|
participant$: participant.value$,
|
||||||
pretendToBeDisconnected$,
|
pretendToBeDisconnected$,
|
||||||
|
disableRemoteVideo$,
|
||||||
...inputs,
|
...inputs,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user