From 02a939da1511ae8337164622452f22df40e62dc5 Mon Sep 17 00:00:00 2001 From: WolverinDEV Date: Sun, 22 Nov 2020 19:08:19 +0100 Subject: [PATCH] Added the option to mute/unmute remote video and some minor bugfixing --- .../js/events/ClientGlobalControlHandler.ts | 2 +- shared/js/ui/frames/video/Controller.ts | 72 ++++++++- shared/js/ui/frames/video/Definitions.ts | 19 ++- shared/js/ui/frames/video/Renderer.scss | 55 ++++--- shared/js/ui/frames/video/Renderer.tsx | 139 ++++++++++++++---- web/app/rtc/Connection.ts | 11 +- web/app/rtc/RemoteTrack.ts | 2 + web/app/rtc/video/Connection.ts | 25 +++- web/app/ui/context-menu/ReactRenderer.scss | 4 +- 9 files changed, 268 insertions(+), 61 deletions(-) diff --git a/shared/js/events/ClientGlobalControlHandler.ts b/shared/js/events/ClientGlobalControlHandler.ts index ca9b6d9c..fca41d03 100644 --- a/shared/js/events/ClientGlobalControlHandler.ts +++ b/shared/js/events/ClientGlobalControlHandler.ts @@ -183,7 +183,7 @@ export function initialize(event_registry: Registry) if(!source) { return; } try { - event.connection.getServerConnection().getVideoConnection().startBroadcasting("camera", source) + event.connection.getServerConnection().getVideoConnection().startBroadcasting(event.broadcastType, source) .catch(error => { logError(LogCategory.VIDEO, tr("Failed to start %s broadcasting: %o"), event.broadcastType, error); if(typeof error !== "string") { diff --git a/shared/js/ui/frames/video/Controller.ts b/shared/js/ui/frames/video/Controller.ts index 59d8ffdb..b9eb0b3c 100644 --- a/shared/js/ui/frames/video/Controller.ts +++ b/shared/js/ui/frames/video/Controller.ts @@ -13,8 +13,11 @@ const cssStyle = require("./Renderer.scss"); let videoIdIndex = 0; interface ClientVideoController { destroy(); + toggleMuteState(type: VideoBroadcastType, state: boolean); + notifyVideoInfo(); notifyVideo(); + notifyMuteState(); } class RemoteClientVideoController implements ClientVideoController { @@ -26,6 +29,11 @@ class RemoteClientVideoController implements ClientVideoController { protected eventListener: (() => void)[]; protected eventListenerVideoClient: (() => void)[]; + protected mutedState: {[T in VideoBroadcastType]: boolean} = { + screen: false, + camera: false + }; + private currentBroadcastState: boolean; constructor(client: ClientEntry, eventRegistry: Registry, videoId?: string) { @@ -56,7 +64,10 @@ class RemoteClientVideoController implements ClientVideoController { const videoClient = this.client.getVideoClient(); if(videoClient) { - events.push(videoClient.getEvents().on("notify_broadcast_state_changed", () => this.notifyVideo())); + events.push(videoClient.getEvents().on("notify_broadcast_state_changed", () => { + this.notifyVideo(); + this.notifyMuteState(); + })); } } @@ -73,6 +84,14 @@ class RemoteClientVideoController implements ClientVideoController { return videoClient && (videoClient.getVideoState("camera") !== VideoBroadcastState.Stopped || videoClient.getVideoState("screen") !== VideoBroadcastState.Stopped); } + toggleMuteState(type: VideoBroadcastType, state: boolean) { + if(this.mutedState[type] === state) { return; } + + this.mutedState[type] = state; + this.notifyVideo(); + this.notifyMuteState(); + } + notifyVideoInfo() { this.events.fire_react("notify_video_info", { videoId: this.videoId, @@ -88,31 +107,39 @@ class RemoteClientVideoController implements ClientVideoController { notifyVideo() { let broadcasting = false; if(this.isVideoActive()) { - let streams = []; let initializing = false; + let cameraStream, desktopStream; + const stateCamera = this.getBroadcastState("camera"); if(stateCamera === VideoBroadcastState.Running) { - streams.push(this.getBroadcastStream("camera")); + cameraStream = this.getBroadcastStream("camera") + if(cameraStream && this.mutedState["camera"]) { + cameraStream = "muted"; + } } else if(stateCamera === VideoBroadcastState.Initializing) { initializing = true; } const stateScreen = this.getBroadcastState("screen"); if(stateScreen === VideoBroadcastState.Running) { - streams.push(this.getBroadcastStream("screen")); + desktopStream = this.getBroadcastStream("screen"); + if(desktopStream && this.mutedState["screen"]) { + desktopStream = "muted"; + } } else if(stateScreen === VideoBroadcastState.Initializing) { initializing = true; } - if(streams.length > 0) { + if(cameraStream || desktopStream) { broadcasting = true; this.events.fire_react("notify_video", { videoId: this.videoId, status: { status: "connected", - desktopStream: streams[1], - cameraStream: streams[0] + + desktopStream: desktopStream, + cameraStream: cameraStream, } }); } else if(initializing) { @@ -126,6 +153,7 @@ class RemoteClientVideoController implements ClientVideoController { videoId: this.videoId, status: { status: "connected", + cameraStream: undefined, desktopStream: undefined } @@ -146,6 +174,16 @@ class RemoteClientVideoController implements ClientVideoController { } } + notifyMuteState() { + this.events.fire_react("notify_video_mute_status", { + videoId: this.videoId, + status: { + camera: this.getBroadcastStream("camera") ? this.mutedState["camera"] ? "muted" : "available" : "unset", + screen: this.getBroadcastStream("screen") ? this.mutedState["screen"] ? "muted" : "available" : "unset", + } + }); + } + protected isVideoActive() : boolean { return typeof this.client.getVideoClient() !== "undefined"; } @@ -256,6 +294,16 @@ class ChannelVideoController { } }); + this.events.on("action_toggle_mute", event => { + const controller = this.findVideoById(event.videoId); + if(!controller) { + logWarn(LogCategory.VIDEO, tr("Tried to toggle video mute state for a non existing video id (%s)."), event.videoId); + return; + } + + controller.toggleMuteState(event.broadcastType, event.muted); + }); + this.events.on("query_expended", () => this.events.fire_react("notify_expended", { expended: this.expended })); this.events.on("query_videos", () => this.notifyVideoList()); this.events.on("query_spotlight", () => this.notifySpotlight()); @@ -280,6 +328,16 @@ class ChannelVideoController { controller.notifyVideo(); }); + this.events.on("query_video_mute_status", event => { + const controller = this.findVideoById(event.videoId); + if(!controller) { + logWarn(LogCategory.VIDEO, tr("Tried to query mute state for a non existing video id (%s)."), event.videoId); + return; + } + + controller.notifyMuteState(); + }); + const channelTree = this.connection.channelTree; events.push(channelTree.events.on("notify_tree_reset", () => { this.resetClientVideos(); diff --git a/shared/js/ui/frames/video/Definitions.ts b/shared/js/ui/frames/video/Definitions.ts index 1dee17c0..e3130130 100644 --- a/shared/js/ui/frames/video/Definitions.ts +++ b/shared/js/ui/frames/video/Definitions.ts @@ -9,8 +9,9 @@ export type ChannelVideo ={ status: "initializing", } | { status: "connected", - cameraStream: MediaStream | undefined, - desktopStream: MediaStream | undefined, + + cameraStream: "muted" | MediaStream | undefined, + desktopStream: "muted" | MediaStream | undefined } | { status: "error", message: string @@ -46,10 +47,19 @@ export type VideoStatistics = { codec: { name: string, payloadType: number } }; +/** + * "muted": The video has been muted locally + * "unset": The video will be normally played + * "empty": No video available + */ +export type LocalVideoState = "muted" | "unset" | "empty"; + export interface ChannelVideoEvents { action_toggle_expended: { expended: boolean }, action_video_scroll: { direction: "left" | "right" }, action_set_spotlight: { videoId: string | undefined, expend: boolean }, + action_set_fullscreen: { videoId: string | undefined }, + action_toggle_mute: { videoId: string, broadcastType: VideoBroadcastType, muted: boolean }, query_expended: {}, query_videos: {}, @@ -57,6 +67,7 @@ export interface ChannelVideoEvents { query_video_info: { videoId: string }, query_video_statistics: { videoId: string, broadcastType: VideoBroadcastType }, query_spotlight: {}, + query_video_mute_status: { videoId: string } notify_expended: { expended: boolean }, notify_videos: { @@ -85,5 +96,9 @@ export interface ChannelVideoEvents { videoId: string | undefined, broadcastType: VideoBroadcastType, statistics: VideoStatistics + }, + notify_video_mute_status: { + videoId: string, + status: {[T in VideoBroadcastType] : "muted" | "available" | "unset"} } } \ No newline at end of file diff --git a/shared/js/ui/frames/video/Renderer.scss b/shared/js/ui/frames/video/Renderer.scss index 2ba61a20..11be9e97 100644 --- a/shared/js/ui/frames/video/Renderer.scss +++ b/shared/js/ui/frames/video/Renderer.scss @@ -163,11 +163,6 @@ $small_height: 10em; &:hover { background-color: #3c3d3e; } - - .icon { - align-self: center; - font-size: 2em; - } } &.right { @@ -195,7 +190,7 @@ $small_height: 10em; flex-shrink: 1; flex-grow: 1; - .videoContainer .requestFullscreen { + .videoContainer .actionIcons { opacity: .5; } } @@ -225,16 +220,23 @@ $small_height: 10em; .video { opacity: 1; - height: 100%; - width: 100%; align-self: center; } .videoPrimary { - + height: 100%; + width: 100%; } - .videoSecondary { + .videoSecondary { + position: absolute; + + top: 0; + right: 0; + max-width: 50%; + max-height: 50%; + + border-bottom-left-radius: .2em; } .text { @@ -289,7 +291,7 @@ $small_height: 10em; } } - .requestFullscreen { + .actionIcons { position: absolute; bottom: 0; @@ -301,33 +303,52 @@ $small_height: 10em; border-top-left-radius: .2em; background-color: #353535; - padding: .25em; + padding: .2em .3em; opacity: 0; @include transition(all $button_hover_animation_time ease-in-out); - &.hidden { - display: none; - } - .iconContainer { align-self: center; display: flex; padding: .2em; + margin-top: -1px; + margin-bottom: calc(-.1em - 1px); cursor: pointer; border-radius: .1em; + border: 1px solid transparent; @include transition(all $button_hover_animation_time ease-in-out); &:hover { background-color: #ffffff1e; } + + &:not(:first-of-type) { + margin-left: .2em; + } + + &.toggle { + &.disabled { + background-color: var(--menu-bar-button-background-activated-red); + border-color: var(--menu-bar-button-border-activated-red); + } + } + + &.hidden { + display: none; + } + } + + .icon { + flex-shrink: 0; + align-self: center; } } &:hover { - .requestFullscreen { + .actionIcons { opacity: 1; } } diff --git a/shared/js/ui/frames/video/Renderer.tsx b/shared/js/ui/frames/video/Renderer.tsx index f64a2e14..31a5895c 100644 --- a/shared/js/ui/frames/video/Renderer.tsx +++ b/shared/js/ui/frames/video/Renderer.tsx @@ -9,6 +9,8 @@ import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots"; import {ClientTag} from "tc-shared/ui/tree/EntryTags"; import ResizeObserver from "resize-observer-polyfill"; import {LogCategory, logWarn} from "tc-shared/log"; +import {spawnContextMenu} from "tc-shared/ui/ContextMenu"; +import {VideoBroadcastType} from "tc-shared/connection/VideoConnection"; const EventContext = React.createContext>(undefined); const HandlerIdContext = React.createContext(undefined); @@ -74,7 +76,7 @@ const VideoInfo = React.memo((props: { videoId: string }) => { ); }); -const VideoStreamReplay = React.memo((props: { stream: MediaStream | undefined, className: string }) => { +const VideoStreamReplay = React.memo((props: { stream: MediaStream | undefined, className: string, title: string }) => { const refVideo = useRef(); useEffect(() => { @@ -83,14 +85,15 @@ const VideoStreamReplay = React.memo((props: { stream: MediaStream | undefined, video.style.opacity = "1"; video.srcObject = props.stream; video.autoplay = true; - video.play().then(undefined); + video.muted = true; + video.play().then(undefined).catch(undefined); } else { video.style.opacity = "0"; } }, [ props.stream ]); return ( -