Improved video control handling
This commit is contained in:
parent
d18701b984
commit
fdae0c77e8
3 changed files with 236 additions and 223 deletions
|
@ -3,7 +3,12 @@ import * as React from "react";
|
|||
import * as ReactDOM from "react-dom";
|
||||
import {ChannelVideoRenderer} from "tc-shared/ui/frames/video/Renderer";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {ChannelVideo, ChannelVideoEvents, kLocalVideoId} from "tc-shared/ui/frames/video/Definitions";
|
||||
import {
|
||||
ChannelVideoEvents,
|
||||
ChannelVideoStreamState,
|
||||
kLocalVideoId,
|
||||
VideoStreamState
|
||||
} from "tc-shared/ui/frames/video/Definitions";
|
||||
import {
|
||||
LocalVideoBroadcastState,
|
||||
VideoBroadcastState,
|
||||
|
@ -27,7 +32,7 @@ interface ClientVideoController {
|
|||
|
||||
notifyVideoInfo();
|
||||
notifyVideo(forceSend: boolean);
|
||||
notifyMuteState();
|
||||
notifyVideoStream(type: VideoBroadcastType);
|
||||
}
|
||||
|
||||
class RemoteClientVideoController implements ClientVideoController {
|
||||
|
@ -45,7 +50,8 @@ class RemoteClientVideoController implements ClientVideoController {
|
|||
camera: false
|
||||
};
|
||||
|
||||
private cachedVideoStatus: ChannelVideo;
|
||||
private cachedCameraState: ChannelVideoStreamState;
|
||||
private cachedScreenState: ChannelVideoStreamState;
|
||||
|
||||
constructor(client: ClientEntry, eventRegistry: Registry<ChannelVideoEvents>, videoId?: string) {
|
||||
this.client = client;
|
||||
|
@ -90,7 +96,7 @@ class RemoteClientVideoController implements ClientVideoController {
|
|||
this.dismissed[event.broadcastType] = false;
|
||||
}
|
||||
this.notifyVideo(false);
|
||||
this.notifyMuteState();
|
||||
this.notifyVideoStream(event.broadcastType);
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -119,6 +125,8 @@ class RemoteClientVideoController implements ClientVideoController {
|
|||
/* TODO: Propagate error? */
|
||||
});
|
||||
}
|
||||
|
||||
this.notifyVideo(false);
|
||||
}
|
||||
|
||||
dismissVideo(type: VideoBroadcastType) {
|
||||
|
@ -143,61 +151,36 @@ class RemoteClientVideoController implements ClientVideoController {
|
|||
}
|
||||
|
||||
notifyVideo(forceSend: boolean) {
|
||||
let cameraState: ChannelVideoStreamState = "none";
|
||||
let screenState: ChannelVideoStreamState = "none";
|
||||
|
||||
let broadcasting = false;
|
||||
let status: ChannelVideo;
|
||||
if(this.hasVideoSupport()) {
|
||||
let initializing = false;
|
||||
|
||||
let cameraStream, desktopStream;
|
||||
|
||||
const stateCamera = this.getBroadcastState("camera");
|
||||
if(stateCamera === VideoBroadcastState.Available) {
|
||||
cameraStream = "available";
|
||||
} else if(stateCamera === VideoBroadcastState.Running) {
|
||||
cameraStream = this.getBroadcastStream("camera")
|
||||
} else if(stateCamera === VideoBroadcastState.Initializing) {
|
||||
initializing = true;
|
||||
cameraState = this.dismissed["camera"] ? "ignored" : "available";
|
||||
} else if(stateCamera === VideoBroadcastState.Running || stateCamera === VideoBroadcastState.Initializing) {
|
||||
cameraState = "streaming";
|
||||
}
|
||||
|
||||
const stateScreen = this.getBroadcastState("screen");
|
||||
if(stateScreen === VideoBroadcastState.Available) {
|
||||
desktopStream = "available";
|
||||
} else if(stateScreen === VideoBroadcastState.Running) {
|
||||
desktopStream = this.getBroadcastStream("screen");
|
||||
} else if(stateScreen === VideoBroadcastState.Initializing) {
|
||||
initializing = true;
|
||||
screenState = this.dismissed["screen"] ? "ignored" : "available";
|
||||
} else if(stateScreen === VideoBroadcastState.Running || stateScreen === VideoBroadcastState.Initializing) {
|
||||
screenState = "streaming";
|
||||
}
|
||||
|
||||
if(cameraStream || desktopStream) {
|
||||
broadcasting = true;
|
||||
status = {
|
||||
status: "connected",
|
||||
|
||||
desktopStream: desktopStream,
|
||||
cameraStream: cameraStream,
|
||||
|
||||
dismissed: this.dismissed
|
||||
};
|
||||
} else if(initializing) {
|
||||
broadcasting = true;
|
||||
status = { status: "initializing" };
|
||||
} else {
|
||||
status = {
|
||||
status: "connected",
|
||||
|
||||
cameraStream: undefined,
|
||||
desktopStream: undefined,
|
||||
|
||||
dismissed: this.dismissed
|
||||
};
|
||||
}
|
||||
} else {
|
||||
status = { status: "no-video" };
|
||||
broadcasting = cameraState !== "none" || screenState !== "none";
|
||||
}
|
||||
|
||||
if(forceSend || !_.isEqual(this.cachedVideoStatus, status)) {
|
||||
this.cachedVideoStatus = status;
|
||||
this.events.fire_react("notify_video", { videoId: this.videoId, status: status });
|
||||
if(forceSend || !_.isEqual(this.cachedCameraState, cameraState) || !_.isEqual(this.cachedScreenState, screenState)) {
|
||||
this.cachedCameraState = cameraState;
|
||||
this.cachedScreenState = screenState;
|
||||
this.events.fire_react("notify_video", {
|
||||
videoId: this.videoId,
|
||||
cameraStream: cameraState,
|
||||
screenStream: screenState
|
||||
});
|
||||
}
|
||||
|
||||
if(broadcasting !== this.currentBroadcastState) {
|
||||
|
@ -208,13 +191,29 @@ class RemoteClientVideoController implements ClientVideoController {
|
|||
}
|
||||
}
|
||||
|
||||
notifyMuteState() {
|
||||
this.events.fire_react("notify_video_mute_status", {
|
||||
videoId: this.videoId,
|
||||
status: {
|
||||
camera: this.getBroadcastState("camera") === VideoBroadcastState.Available ? "muted" : this.getBroadcastState("camera") === VideoBroadcastState.Stopped ? "unset" : "available",
|
||||
screen: this.getBroadcastState("screen") === VideoBroadcastState.Available ? "muted" : this.getBroadcastState("screen") === VideoBroadcastState.Stopped ? "unset" : "available",
|
||||
notifyVideoStream(type: VideoBroadcastType) {
|
||||
let state: VideoStreamState;
|
||||
|
||||
const streamState = this.getBroadcastState(type);
|
||||
if(streamState === VideoBroadcastState.Stopped) {
|
||||
state = { state: "disconnected" };
|
||||
} else if(streamState === VideoBroadcastState.Initializing) {
|
||||
state = { state: "connecting" };
|
||||
} else if(streamState === VideoBroadcastState.Available) {
|
||||
state = { state: "available" };
|
||||
} else if(streamState === VideoBroadcastState.Buffering || streamState === VideoBroadcastState.Running) {
|
||||
const stream = this.getBroadcastStream(type);
|
||||
if(!stream) {
|
||||
state = { state: "failed", reason: tr("Missing video stream") };
|
||||
} else {
|
||||
state = { state: "connected", stream: stream };
|
||||
}
|
||||
}
|
||||
|
||||
this.events.fire_react("notify_video_stream", {
|
||||
videoId: this.videoId,
|
||||
broadcastType: type,
|
||||
state: state
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -421,14 +420,14 @@ class ChannelVideoController {
|
|||
controller.notifyVideo(true);
|
||||
});
|
||||
|
||||
this.events.on("query_video_mute_status", event => {
|
||||
this.events.on("query_video_stream", 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);
|
||||
logWarn(LogCategory.VIDEO, tr("Tried to query video stream for a non existing video id (%s)."), event.videoId);
|
||||
return;
|
||||
}
|
||||
|
||||
controller.notifyMuteState();
|
||||
controller.notifyVideoStream(event.broadcastType);
|
||||
});
|
||||
|
||||
const channelTree = this.connection.channelTree;
|
||||
|
|
|
@ -5,23 +5,7 @@ export const kLocalVideoId = "__local__video__";
|
|||
export const kLocalBroadcastChannels: VideoBroadcastType[] = ["screen", "camera"];
|
||||
|
||||
export type ChannelVideoInfo = { clientName: string, clientUniqueId: string, clientId: number, statusIcon: ClientIcon };
|
||||
export type ChannelVideoStream = "available" | MediaStream | undefined;
|
||||
|
||||
export type ChannelVideo ={
|
||||
status: "initializing",
|
||||
} | {
|
||||
status: "connected",
|
||||
|
||||
cameraStream: ChannelVideoStream,
|
||||
desktopStream: ChannelVideoStream,
|
||||
|
||||
dismissed: {[T in VideoBroadcastType]: boolean}
|
||||
} | {
|
||||
status: "error",
|
||||
message: string
|
||||
} | {
|
||||
status: "no-video"
|
||||
};
|
||||
export type ChannelVideoStreamState = "available" | "streaming" | "ignored" | "muted" | "none";
|
||||
|
||||
export type VideoStatistics = {
|
||||
type: "sender",
|
||||
|
@ -51,6 +35,21 @@ export type VideoStatistics = {
|
|||
codec: { name: string, payloadType: number }
|
||||
};
|
||||
|
||||
export type VideoStreamState = {
|
||||
state: "disconnected"
|
||||
} | {
|
||||
state: "available"
|
||||
} | {
|
||||
state: "connecting"
|
||||
} | {
|
||||
/* like join failed or whatever */
|
||||
state: "failed",
|
||||
reason?: string
|
||||
} | {
|
||||
state: "connected",
|
||||
stream: MediaStream
|
||||
};
|
||||
|
||||
/**
|
||||
* "muted": The video has been muted locally
|
||||
* "unset": The video will be normally played
|
||||
|
@ -73,7 +72,7 @@ export interface ChannelVideoEvents {
|
|||
query_video_info: { videoId: string },
|
||||
query_video_statistics: { videoId: string, broadcastType: VideoBroadcastType },
|
||||
query_spotlight: {},
|
||||
query_video_mute_status: { videoId: string }
|
||||
query_video_stream: { videoId: string, broadcastType: VideoBroadcastType },
|
||||
|
||||
notify_expended: { expended: boolean },
|
||||
notify_videos: {
|
||||
|
@ -81,7 +80,9 @@ export interface ChannelVideoEvents {
|
|||
},
|
||||
notify_video: {
|
||||
videoId: string,
|
||||
status: ChannelVideo
|
||||
|
||||
cameraStream: ChannelVideoStreamState,
|
||||
screenStream: ChannelVideoStreamState,
|
||||
},
|
||||
notify_video_info: {
|
||||
videoId: string,
|
||||
|
@ -103,8 +104,9 @@ export interface ChannelVideoEvents {
|
|||
broadcastType: VideoBroadcastType,
|
||||
statistics: VideoStatistics
|
||||
},
|
||||
notify_video_mute_status: {
|
||||
notify_video_stream: {
|
||||
videoId: string,
|
||||
status: {[T in VideoBroadcastType] : "muted" | "available" | "unset"}
|
||||
broadcastType: VideoBroadcastType,
|
||||
state: VideoStreamState
|
||||
}
|
||||
}
|
|
@ -4,11 +4,10 @@ import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
|
|||
import {ClientIcon} from "svg-sprites/client-icons";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {
|
||||
ChannelVideo,
|
||||
ChannelVideoEvents,
|
||||
ChannelVideoInfo,
|
||||
ChannelVideoStream,
|
||||
kLocalVideoId
|
||||
ChannelVideoStreamState,
|
||||
kLocalVideoId, VideoStreamState
|
||||
} from "tc-shared/ui/frames/video/Definitions";
|
||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||
|
@ -159,123 +158,115 @@ const VideoAvailableRenderer = (props: { callbackEnable: () => void, callbackIg
|
|||
</div>
|
||||
);
|
||||
|
||||
const VideoStreamRenderer = (props: { stream: ChannelVideoStream, callbackEnable: () => void, callbackIgnore: () => void, videoTitle: string, className?: string }) => {
|
||||
if(props.stream === "available") {
|
||||
return <VideoAvailableRenderer callbackEnable={props.callbackEnable} callbackIgnore={props.callbackIgnore} className={props.className} key={"available"} />;
|
||||
} else if(props.stream === undefined) {
|
||||
return (
|
||||
<div className={cssStyle.text} key={"no-video-stream"}>
|
||||
<div><Translatable>No Video</Translatable></div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <VideoStreamReplay stream={props.stream} className={props.className} title={props.videoTitle} key={"video-renderer"} />;
|
||||
const VideoStreamRenderer = (props: { videoId: string, streamType: VideoBroadcastType, className?: string }) => {
|
||||
const events = useContext(EventContext);
|
||||
const [ state, setState ] = useState<VideoStreamState>(() => {
|
||||
events.fire("query_video_stream", { videoId: props.videoId, broadcastType: props.streamType });
|
||||
return {
|
||||
state: "disconnected",
|
||||
}
|
||||
});
|
||||
events.reactUse("notify_video_stream", event => {
|
||||
if(event.videoId === props.videoId && event.broadcastType === props.streamType) {
|
||||
setState(event.state);
|
||||
}
|
||||
});
|
||||
|
||||
switch (state.state) {
|
||||
case "disconnected":
|
||||
return (
|
||||
<div className={cssStyle.text} key={"no-video-stream"}>
|
||||
<div><Translatable>No video stream</Translatable></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "connecting":
|
||||
return (
|
||||
<div className={cssStyle.text} key={"info-initializing"}>
|
||||
<div><Translatable>connecting</Translatable> <LoadingDots /></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "connected":
|
||||
return <VideoStreamReplay stream={state.stream} className={props.className} title={props.streamType === "camera" ? tr("Camera") : tr("Screen")} key={"connected"} />;
|
||||
|
||||
case "failed":
|
||||
return (
|
||||
<div className={cssStyle.text + " " + cssStyle.error} key={"error"}>
|
||||
<div><Translatable>Stream replay failed</Translatable></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "available":
|
||||
return (
|
||||
<div className={cssStyle.text} key={"no-video-stream"}>
|
||||
<div><Translatable>Video available</Translatable></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const VideoPlayer = React.memo((props: { videoId: string }) => {
|
||||
const VideoPlayer = React.memo((props: { videoId: string, cameraState: ChannelVideoStreamState, screenState: ChannelVideoStreamState }) => {
|
||||
const events = useContext(EventContext);
|
||||
const [ state, setState ] = useState<"loading" | ChannelVideo>(() => {
|
||||
events.fire("query_video", { videoId: props.videoId });
|
||||
return "loading";
|
||||
});
|
||||
|
||||
events.reactUse("notify_video", event => {
|
||||
if(event.videoId === props.videoId) {
|
||||
setState(event.status);
|
||||
}
|
||||
});
|
||||
const streamElements = [];
|
||||
const streamClasses = [cssStyle.videoPrimary, cssStyle.videoSecondary];
|
||||
|
||||
if(state === "loading") {
|
||||
return (
|
||||
<div className={cssStyle.text} key={"info-loading"}>
|
||||
<div><Translatable>loading</Translatable> <LoadingDots /></div>
|
||||
</div>
|
||||
);
|
||||
} else if(state.status === "initializing") {
|
||||
return (
|
||||
<div className={cssStyle.text} key={"info-initializing"}>
|
||||
<div><Translatable>connecting</Translatable> <LoadingDots /></div>
|
||||
</div>
|
||||
);
|
||||
} else if(state.status === "error") {
|
||||
return (
|
||||
<div className={cssStyle.error + " " + cssStyle.text} key={"info-error"}>
|
||||
<div>{state.message}</div>
|
||||
</div>
|
||||
);
|
||||
} else if(state.status === "connected") {
|
||||
const streamElements = [];
|
||||
const streamClasses = [cssStyle.videoPrimary, cssStyle.videoSecondary];
|
||||
if(props.cameraState === "none" && props.screenState === "none") {
|
||||
/* No video available. Will be handled bellow */
|
||||
} else if(props.cameraState !== "streaming" && props.screenState !== "streaming") {
|
||||
/* We're not streaming any video nor we don't have any video. Show general show video button. */
|
||||
streamElements.push(
|
||||
<VideoAvailableRenderer
|
||||
key={"video-available"}
|
||||
callbackEnable={() => {
|
||||
if(props.screenState !== "streaming" && props.screenState !== "none") {
|
||||
events.fire("action_toggle_mute", { broadcastType: "screen", muted: false, videoId: props.videoId })
|
||||
}
|
||||
|
||||
if(state.desktopStream === "available" && (state.cameraStream === "available" || state.cameraStream === undefined) ||
|
||||
state.cameraStream === "available" && (state.desktopStream === "available" || state.desktopStream === undefined)
|
||||
) {
|
||||
/* One or both streams are available. Showing just one box. */
|
||||
if(props.cameraState !== "streaming" && props.cameraState !== "none") {
|
||||
events.fire("action_toggle_mute", { broadcastType: "camera", muted: false, videoId: props.videoId })
|
||||
}
|
||||
}}
|
||||
className={streamClasses.pop_front()}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
if(props.screenState === "available") {
|
||||
streamElements.push(
|
||||
<VideoAvailableRenderer
|
||||
key={"video-available"}
|
||||
callbackEnable={() => {
|
||||
if(state.desktopStream === "available") {
|
||||
events.fire("action_toggle_mute", { broadcastType: "screen", muted: false, videoId: props.videoId })
|
||||
}
|
||||
|
||||
if(state.cameraStream === "available") {
|
||||
events.fire("action_toggle_mute", { broadcastType: "camera", muted: false, videoId: props.videoId })
|
||||
}
|
||||
}}
|
||||
key={"video-available-screen"}
|
||||
callbackEnable={() => events.fire("action_toggle_mute", { broadcastType: "screen", muted: false, videoId: props.videoId })}
|
||||
callbackIgnore={() => events.fire("action_dismiss", { broadcastType: "screen", videoId: props.videoId })}
|
||||
className={streamClasses.pop_front()}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
if(state.desktopStream) {
|
||||
if(!state.dismissed["screen"] || state.desktopStream !== "available") {
|
||||
streamElements.push(
|
||||
<VideoStreamRenderer
|
||||
key={"screen"}
|
||||
stream={state.desktopStream}
|
||||
callbackEnable={() => events.fire("action_toggle_mute", { broadcastType: "screen", muted: false, videoId: props.videoId })}
|
||||
callbackIgnore={() => events.fire("action_dismiss", { broadcastType: "screen", videoId: props.videoId })}
|
||||
videoTitle={tr("Screen")}
|
||||
className={streamClasses.pop_front()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if(state.cameraStream) {
|
||||
if(!state.dismissed["camera"] || state.cameraStream !== "available") {
|
||||
streamElements.push(
|
||||
<VideoStreamRenderer
|
||||
key={"camera"}
|
||||
stream={state.cameraStream}
|
||||
callbackEnable={() => events.fire("action_toggle_mute", { broadcastType: "camera", muted: false, videoId: props.videoId })}
|
||||
callbackIgnore={() => events.fire("action_dismiss", { broadcastType: "camera", videoId: props.videoId })}
|
||||
videoTitle={tr("Camera")}
|
||||
className={streamClasses.pop_front()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(streamElements.length === 0){
|
||||
return (
|
||||
<div className={cssStyle.text} key={"no-video-stream"}>
|
||||
<div>
|
||||
{props.videoId === kLocalVideoId ?
|
||||
<Translatable key={"own"}>You're not broadcasting video</Translatable> :
|
||||
<Translatable key={"general"}>No Video</Translatable>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
} else if(props.screenState === "streaming") {
|
||||
streamElements.push(
|
||||
<VideoStreamRenderer key={"stream-screen"} videoId={props.videoId} streamType={"screen"} className={streamClasses.pop_front()} />
|
||||
);
|
||||
}
|
||||
|
||||
return <>{streamElements}</>;
|
||||
} else if(state.status === "no-video") {
|
||||
|
||||
if(props.cameraState === "available") {
|
||||
streamElements.push(
|
||||
<VideoAvailableRenderer
|
||||
key={"video-available-camera"}
|
||||
callbackEnable={() => events.fire("action_toggle_mute", { broadcastType: "camera", muted: false, videoId: props.videoId })}
|
||||
callbackIgnore={() => events.fire("action_dismiss", { broadcastType: "camera", videoId: props.videoId })}
|
||||
className={streamClasses.pop_front()}
|
||||
/>
|
||||
);
|
||||
} else if(props.cameraState === "streaming") {
|
||||
streamElements.push(
|
||||
<VideoStreamRenderer key={"stream-camera"} videoId={props.videoId} streamType={"camera"} className={streamClasses.pop_front()} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if(streamElements.length === 0){
|
||||
return (
|
||||
<div className={cssStyle.text} key={"no-video"}>
|
||||
<div className={cssStyle.text} key={"no-video-stream"}>
|
||||
<div>
|
||||
{props.videoId === kLocalVideoId ?
|
||||
<Translatable key={"own"}>You're not broadcasting video</Translatable> :
|
||||
|
@ -286,7 +277,53 @@ const VideoPlayer = React.memo((props: { videoId: string }) => {
|
|||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
return <>{streamElements}</>;
|
||||
});
|
||||
|
||||
const VideoControlButtons = React.memo((props: {
|
||||
videoId: string,
|
||||
cameraState: ChannelVideoStreamState,
|
||||
screenState: ChannelVideoStreamState,
|
||||
isSpotlight: boolean,
|
||||
fullscreenMode: "none" | "unavailable" | "set"
|
||||
}) => {
|
||||
const events = useContext(EventContext);
|
||||
|
||||
const screenShown = props.screenState !== "none" && props.videoId !== kLocalVideoId;
|
||||
const cameraShown = props.cameraState !== "none" && props.videoId !== kLocalVideoId;
|
||||
|
||||
const screenDisabled = props.screenState === "ignored" || props.screenState === "muted" || props.screenState === "available";
|
||||
const cameraDisabled = props.cameraState === "ignored" || props.cameraState === "muted" || props.cameraState === "available";
|
||||
|
||||
return (
|
||||
<div className={cssStyle.actionIcons}>
|
||||
<div className={cssStyle.iconContainer + " " + cssStyle.toggle + " " + (screenShown ? "" : cssStyle.hidden) + " " + (screenDisabled ? cssStyle.disabled : "")}
|
||||
onClick={() => events.fire("action_toggle_mute", { videoId: props.videoId, broadcastType: "screen", muted: !screenDisabled })}
|
||||
title={props.screenState === "muted" ? tr("Unmute screen video") : tr("Mute screen video")}
|
||||
>
|
||||
<ClientIconRenderer className={cssStyle.icon} icon={ClientIcon.ShareScreen} />
|
||||
</div>
|
||||
<div className={cssStyle.iconContainer + " " + cssStyle.toggle + " " + (cameraShown ? "" : cssStyle.hidden) + " " + (cameraDisabled ? cssStyle.disabled : "")}
|
||||
onClick={() => events.fire("action_toggle_mute", { videoId: props.videoId, broadcastType: "camera", muted: !cameraDisabled })}
|
||||
title={props.cameraState === "muted" ? tr("Unmute camera video") : tr("Mute camera video")}
|
||||
>
|
||||
<ClientIconRenderer className={cssStyle.icon} icon={ClientIcon.VideoMuted} />
|
||||
</div>
|
||||
<div className={cssStyle.iconContainer + " " + (props.fullscreenMode === "unavailable" ? cssStyle.hidden : "")}
|
||||
onClick={() => {
|
||||
if(props.isSpotlight) {
|
||||
events.fire("action_set_fullscreen", { videoId: props.fullscreenMode === "set" ? undefined : props.videoId });
|
||||
} else {
|
||||
events.fire("action_set_spotlight", { videoId: props.videoId, expend: true });
|
||||
events.fire("action_focus_spotlight", { });
|
||||
}
|
||||
}}
|
||||
title={props.isSpotlight ? tr("Toggle fullscreen") : tr("Toggle spotlight")}
|
||||
>
|
||||
<ClientIconRenderer className={cssStyle.icon} icon={ClientIcon.Fullscreen} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const VideoContainer = React.memo((props: { videoId: string, isSpotlight: boolean }) => {
|
||||
|
@ -295,14 +332,17 @@ const VideoContainer = React.memo((props: { videoId: string, isSpotlight: boolea
|
|||
const fullscreenCapable = "requestFullscreen" in HTMLElement.prototype;
|
||||
|
||||
const [ isFullscreen, setFullscreen ] = useState(false);
|
||||
const [ muteState, setMuteState ] = useState<{[T in VideoBroadcastType]: "muted" | "available" | "unset"}>(() => {
|
||||
events.fire("query_video_mute_status", { videoId: props.videoId });
|
||||
return { camera: "unset", screen: "unset" };
|
||||
|
||||
const [ cameraState, setCameraState ] = useState<ChannelVideoStreamState>("none");
|
||||
const [ screenState, setScreenState ] = useState<ChannelVideoStreamState>(() => {
|
||||
events.fire("query_video", { videoId: props.videoId });
|
||||
return "none";
|
||||
});
|
||||
|
||||
events.reactUse("notify_video_mute_status", event => {
|
||||
events.reactUse("notify_video", event => {
|
||||
if(event.videoId === props.videoId) {
|
||||
setMuteState(event.status);
|
||||
setCameraState(event.cameraStream);
|
||||
setScreenState(event.screenStream);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -342,14 +382,6 @@ const VideoContainer = React.memo((props: { videoId: string, isSpotlight: boolea
|
|||
}
|
||||
});
|
||||
|
||||
const toggleClass = (type: VideoBroadcastType) => {
|
||||
if(props.videoId === kLocalVideoId || muteState[type] === "unset") {
|
||||
return cssStyle.hidden;
|
||||
}
|
||||
|
||||
return muteState[type] === "muted" ? cssStyle.disabled : "";
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cssStyle.videoContainer}
|
||||
|
@ -390,35 +422,15 @@ const VideoContainer = React.memo((props: { videoId: string, isSpotlight: boolea
|
|||
}}
|
||||
ref={refContainer}
|
||||
>
|
||||
<VideoPlayer videoId={props.videoId} />
|
||||
<VideoPlayer videoId={props.videoId} cameraState={cameraState} screenState={screenState} />
|
||||
<VideoInfo videoId={props.videoId} />
|
||||
<div className={cssStyle.actionIcons}>
|
||||
<div className={cssStyle.iconContainer + " " + cssStyle.toggle + " " + toggleClass("screen")}
|
||||
onClick={() => events.fire("action_toggle_mute", { videoId: props.videoId, broadcastType: "screen", muted: muteState.screen === "available" })}
|
||||
title={muteState["screen"] === "muted" ? tr("Unmute screen video") : tr("Mute screen video")}
|
||||
>
|
||||
<ClientIconRenderer className={cssStyle.icon} icon={ClientIcon.ShareScreen} />
|
||||
</div>
|
||||
<div className={cssStyle.iconContainer + " " + cssStyle.toggle + " " + toggleClass("camera")}
|
||||
onClick={() => events.fire("action_toggle_mute", { videoId: props.videoId, broadcastType: "camera", muted: muteState.camera === "available" })}
|
||||
title={muteState["camera"] === "muted" ? tr("Unmute camera video") : tr("Mute camera video")}
|
||||
>
|
||||
<ClientIconRenderer className={cssStyle.icon} icon={ClientIcon.VideoMuted} />
|
||||
</div>
|
||||
<div className={cssStyle.iconContainer + " " + (!fullscreenCapable ? cssStyle.hidden : "")}
|
||||
onClick={() => {
|
||||
if(props.isSpotlight) {
|
||||
events.fire("action_set_fullscreen", { videoId: isFullscreen ? undefined : props.videoId });
|
||||
} else {
|
||||
events.fire("action_set_spotlight", { videoId: props.videoId, expend: true });
|
||||
events.fire("action_focus_spotlight", { });
|
||||
}
|
||||
}}
|
||||
title={props.isSpotlight ? tr("Toggle fullscreen") : tr("Toggle spotlight")}
|
||||
>
|
||||
<ClientIconRenderer className={cssStyle.icon} icon={ClientIcon.Fullscreen} />
|
||||
</div>
|
||||
</div>
|
||||
<VideoControlButtons
|
||||
videoId={props.videoId}
|
||||
cameraState={cameraState}
|
||||
screenState={screenState}
|
||||
isSpotlight={props.isSpotlight}
|
||||
fullscreenMode={fullscreenCapable ? isFullscreen ? "set" : "none" : "unavailable"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue