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 * as ReactDOM from "react-dom";
|
||||||
import {ChannelVideoRenderer} from "tc-shared/ui/frames/video/Renderer";
|
import {ChannelVideoRenderer} from "tc-shared/ui/frames/video/Renderer";
|
||||||
import {Registry} from "tc-shared/events";
|
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 {
|
import {
|
||||||
LocalVideoBroadcastState,
|
LocalVideoBroadcastState,
|
||||||
VideoBroadcastState,
|
VideoBroadcastState,
|
||||||
|
@ -27,7 +32,7 @@ interface ClientVideoController {
|
||||||
|
|
||||||
notifyVideoInfo();
|
notifyVideoInfo();
|
||||||
notifyVideo(forceSend: boolean);
|
notifyVideo(forceSend: boolean);
|
||||||
notifyMuteState();
|
notifyVideoStream(type: VideoBroadcastType);
|
||||||
}
|
}
|
||||||
|
|
||||||
class RemoteClientVideoController implements ClientVideoController {
|
class RemoteClientVideoController implements ClientVideoController {
|
||||||
|
@ -45,7 +50,8 @@ class RemoteClientVideoController implements ClientVideoController {
|
||||||
camera: false
|
camera: false
|
||||||
};
|
};
|
||||||
|
|
||||||
private cachedVideoStatus: ChannelVideo;
|
private cachedCameraState: ChannelVideoStreamState;
|
||||||
|
private cachedScreenState: ChannelVideoStreamState;
|
||||||
|
|
||||||
constructor(client: ClientEntry, eventRegistry: Registry<ChannelVideoEvents>, videoId?: string) {
|
constructor(client: ClientEntry, eventRegistry: Registry<ChannelVideoEvents>, videoId?: string) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
|
@ -90,7 +96,7 @@ class RemoteClientVideoController implements ClientVideoController {
|
||||||
this.dismissed[event.broadcastType] = false;
|
this.dismissed[event.broadcastType] = false;
|
||||||
}
|
}
|
||||||
this.notifyVideo(false);
|
this.notifyVideo(false);
|
||||||
this.notifyMuteState();
|
this.notifyVideoStream(event.broadcastType);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,6 +125,8 @@ class RemoteClientVideoController implements ClientVideoController {
|
||||||
/* TODO: Propagate error? */
|
/* TODO: Propagate error? */
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.notifyVideo(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
dismissVideo(type: VideoBroadcastType) {
|
dismissVideo(type: VideoBroadcastType) {
|
||||||
|
@ -143,61 +151,36 @@ class RemoteClientVideoController implements ClientVideoController {
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyVideo(forceSend: boolean) {
|
notifyVideo(forceSend: boolean) {
|
||||||
|
let cameraState: ChannelVideoStreamState = "none";
|
||||||
|
let screenState: ChannelVideoStreamState = "none";
|
||||||
|
|
||||||
let broadcasting = false;
|
let broadcasting = false;
|
||||||
let status: ChannelVideo;
|
|
||||||
if(this.hasVideoSupport()) {
|
if(this.hasVideoSupport()) {
|
||||||
let initializing = false;
|
|
||||||
|
|
||||||
let cameraStream, desktopStream;
|
|
||||||
|
|
||||||
const stateCamera = this.getBroadcastState("camera");
|
const stateCamera = this.getBroadcastState("camera");
|
||||||
if(stateCamera === VideoBroadcastState.Available) {
|
if(stateCamera === VideoBroadcastState.Available) {
|
||||||
cameraStream = "available";
|
cameraState = this.dismissed["camera"] ? "ignored" : "available";
|
||||||
} else if(stateCamera === VideoBroadcastState.Running) {
|
} else if(stateCamera === VideoBroadcastState.Running || stateCamera === VideoBroadcastState.Initializing) {
|
||||||
cameraStream = this.getBroadcastStream("camera")
|
cameraState = "streaming";
|
||||||
} else if(stateCamera === VideoBroadcastState.Initializing) {
|
|
||||||
initializing = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const stateScreen = this.getBroadcastState("screen");
|
const stateScreen = this.getBroadcastState("screen");
|
||||||
if(stateScreen === VideoBroadcastState.Available) {
|
if(stateScreen === VideoBroadcastState.Available) {
|
||||||
desktopStream = "available";
|
screenState = this.dismissed["screen"] ? "ignored" : "available";
|
||||||
} else if(stateScreen === VideoBroadcastState.Running) {
|
} else if(stateScreen === VideoBroadcastState.Running || stateScreen === VideoBroadcastState.Initializing) {
|
||||||
desktopStream = this.getBroadcastStream("screen");
|
screenState = "streaming";
|
||||||
} else if(stateScreen === VideoBroadcastState.Initializing) {
|
|
||||||
initializing = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(cameraStream || desktopStream) {
|
broadcasting = cameraState !== "none" || screenState !== "none";
|
||||||
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" };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(forceSend || !_.isEqual(this.cachedVideoStatus, status)) {
|
if(forceSend || !_.isEqual(this.cachedCameraState, cameraState) || !_.isEqual(this.cachedScreenState, screenState)) {
|
||||||
this.cachedVideoStatus = status;
|
this.cachedCameraState = cameraState;
|
||||||
this.events.fire_react("notify_video", { videoId: this.videoId, status: status });
|
this.cachedScreenState = screenState;
|
||||||
|
this.events.fire_react("notify_video", {
|
||||||
|
videoId: this.videoId,
|
||||||
|
cameraStream: cameraState,
|
||||||
|
screenStream: screenState
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if(broadcasting !== this.currentBroadcastState) {
|
if(broadcasting !== this.currentBroadcastState) {
|
||||||
|
@ -208,13 +191,29 @@ class RemoteClientVideoController implements ClientVideoController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyMuteState() {
|
notifyVideoStream(type: VideoBroadcastType) {
|
||||||
this.events.fire_react("notify_video_mute_status", {
|
let state: VideoStreamState;
|
||||||
videoId: this.videoId,
|
|
||||||
status: {
|
const streamState = this.getBroadcastState(type);
|
||||||
camera: this.getBroadcastState("camera") === VideoBroadcastState.Available ? "muted" : this.getBroadcastState("camera") === VideoBroadcastState.Stopped ? "unset" : "available",
|
if(streamState === VideoBroadcastState.Stopped) {
|
||||||
screen: this.getBroadcastState("screen") === VideoBroadcastState.Available ? "muted" : this.getBroadcastState("screen") === VideoBroadcastState.Stopped ? "unset" : "available",
|
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);
|
controller.notifyVideo(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.events.on("query_video_mute_status", event => {
|
this.events.on("query_video_stream", event => {
|
||||||
const controller = this.findVideoById(event.videoId);
|
const controller = this.findVideoById(event.videoId);
|
||||||
if(!controller) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.notifyMuteState();
|
controller.notifyVideoStream(event.broadcastType);
|
||||||
});
|
});
|
||||||
|
|
||||||
const channelTree = this.connection.channelTree;
|
const channelTree = this.connection.channelTree;
|
||||||
|
|
|
@ -5,23 +5,7 @@ export const kLocalVideoId = "__local__video__";
|
||||||
export const kLocalBroadcastChannels: VideoBroadcastType[] = ["screen", "camera"];
|
export const kLocalBroadcastChannels: VideoBroadcastType[] = ["screen", "camera"];
|
||||||
|
|
||||||
export type ChannelVideoInfo = { clientName: string, clientUniqueId: string, clientId: number, statusIcon: ClientIcon };
|
export type ChannelVideoInfo = { clientName: string, clientUniqueId: string, clientId: number, statusIcon: ClientIcon };
|
||||||
export type ChannelVideoStream = "available" | MediaStream | undefined;
|
export type ChannelVideoStreamState = "available" | "streaming" | "ignored" | "muted" | "none";
|
||||||
|
|
||||||
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 VideoStatistics = {
|
export type VideoStatistics = {
|
||||||
type: "sender",
|
type: "sender",
|
||||||
|
@ -51,6 +35,21 @@ export type VideoStatistics = {
|
||||||
codec: { name: string, payloadType: number }
|
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
|
* "muted": The video has been muted locally
|
||||||
* "unset": The video will be normally played
|
* "unset": The video will be normally played
|
||||||
|
@ -73,7 +72,7 @@ export interface ChannelVideoEvents {
|
||||||
query_video_info: { videoId: string },
|
query_video_info: { videoId: string },
|
||||||
query_video_statistics: { videoId: string, broadcastType: VideoBroadcastType },
|
query_video_statistics: { videoId: string, broadcastType: VideoBroadcastType },
|
||||||
query_spotlight: {},
|
query_spotlight: {},
|
||||||
query_video_mute_status: { videoId: string }
|
query_video_stream: { videoId: string, broadcastType: VideoBroadcastType },
|
||||||
|
|
||||||
notify_expended: { expended: boolean },
|
notify_expended: { expended: boolean },
|
||||||
notify_videos: {
|
notify_videos: {
|
||||||
|
@ -81,7 +80,9 @@ export interface ChannelVideoEvents {
|
||||||
},
|
},
|
||||||
notify_video: {
|
notify_video: {
|
||||||
videoId: string,
|
videoId: string,
|
||||||
status: ChannelVideo
|
|
||||||
|
cameraStream: ChannelVideoStreamState,
|
||||||
|
screenStream: ChannelVideoStreamState,
|
||||||
},
|
},
|
||||||
notify_video_info: {
|
notify_video_info: {
|
||||||
videoId: string,
|
videoId: string,
|
||||||
|
@ -103,8 +104,9 @@ export interface ChannelVideoEvents {
|
||||||
broadcastType: VideoBroadcastType,
|
broadcastType: VideoBroadcastType,
|
||||||
statistics: VideoStatistics
|
statistics: VideoStatistics
|
||||||
},
|
},
|
||||||
notify_video_mute_status: {
|
notify_video_stream: {
|
||||||
videoId: string,
|
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 {ClientIcon} from "svg-sprites/client-icons";
|
||||||
import {Registry} from "tc-shared/events";
|
import {Registry} from "tc-shared/events";
|
||||||
import {
|
import {
|
||||||
ChannelVideo,
|
|
||||||
ChannelVideoEvents,
|
ChannelVideoEvents,
|
||||||
ChannelVideoInfo,
|
ChannelVideoInfo,
|
||||||
ChannelVideoStream,
|
ChannelVideoStreamState,
|
||||||
kLocalVideoId
|
kLocalVideoId, VideoStreamState
|
||||||
} from "tc-shared/ui/frames/video/Definitions";
|
} from "tc-shared/ui/frames/video/Definitions";
|
||||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||||
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||||
|
@ -159,123 +158,115 @@ const VideoAvailableRenderer = (props: { callbackEnable: () => void, callbackIg
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const VideoStreamRenderer = (props: { stream: ChannelVideoStream, callbackEnable: () => void, callbackIgnore: () => void, videoTitle: string, className?: string }) => {
|
const VideoStreamRenderer = (props: { videoId: string, streamType: VideoBroadcastType, className?: string }) => {
|
||||||
if(props.stream === "available") {
|
const events = useContext(EventContext);
|
||||||
return <VideoAvailableRenderer callbackEnable={props.callbackEnable} callbackIgnore={props.callbackIgnore} className={props.className} key={"available"} />;
|
const [ state, setState ] = useState<VideoStreamState>(() => {
|
||||||
} else if(props.stream === undefined) {
|
events.fire("query_video_stream", { videoId: props.videoId, broadcastType: props.streamType });
|
||||||
return (
|
return {
|
||||||
<div className={cssStyle.text} key={"no-video-stream"}>
|
state: "disconnected",
|
||||||
<div><Translatable>No Video</Translatable></div>
|
}
|
||||||
</div>
|
});
|
||||||
);
|
events.reactUse("notify_video_stream", event => {
|
||||||
} else {
|
if(event.videoId === props.videoId && event.broadcastType === props.streamType) {
|
||||||
return <VideoStreamReplay stream={props.stream} className={props.className} title={props.videoTitle} key={"video-renderer"} />;
|
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 events = useContext(EventContext);
|
||||||
const [ state, setState ] = useState<"loading" | ChannelVideo>(() => {
|
|
||||||
events.fire("query_video", { videoId: props.videoId });
|
|
||||||
return "loading";
|
|
||||||
});
|
|
||||||
|
|
||||||
events.reactUse("notify_video", event => {
|
const streamElements = [];
|
||||||
if(event.videoId === props.videoId) {
|
const streamClasses = [cssStyle.videoPrimary, cssStyle.videoSecondary];
|
||||||
setState(event.status);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if(state === "loading") {
|
if(props.cameraState === "none" && props.screenState === "none") {
|
||||||
return (
|
/* No video available. Will be handled bellow */
|
||||||
<div className={cssStyle.text} key={"info-loading"}>
|
} else if(props.cameraState !== "streaming" && props.screenState !== "streaming") {
|
||||||
<div><Translatable>loading</Translatable> <LoadingDots /></div>
|
/* We're not streaming any video nor we don't have any video. Show general show video button. */
|
||||||
</div>
|
streamElements.push(
|
||||||
);
|
<VideoAvailableRenderer
|
||||||
} else if(state.status === "initializing") {
|
key={"video-available"}
|
||||||
return (
|
callbackEnable={() => {
|
||||||
<div className={cssStyle.text} key={"info-initializing"}>
|
if(props.screenState !== "streaming" && props.screenState !== "none") {
|
||||||
<div><Translatable>connecting</Translatable> <LoadingDots /></div>
|
events.fire("action_toggle_mute", { broadcastType: "screen", muted: false, videoId: props.videoId })
|
||||||
</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(state.desktopStream === "available" && (state.cameraStream === "available" || state.cameraStream === undefined) ||
|
if(props.cameraState !== "streaming" && props.cameraState !== "none") {
|
||||||
state.cameraStream === "available" && (state.desktopStream === "available" || state.desktopStream === undefined)
|
events.fire("action_toggle_mute", { broadcastType: "camera", muted: false, videoId: props.videoId })
|
||||||
) {
|
}
|
||||||
/* One or both streams are available. Showing just one box. */
|
}}
|
||||||
|
className={streamClasses.pop_front()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if(props.screenState === "available") {
|
||||||
streamElements.push(
|
streamElements.push(
|
||||||
<VideoAvailableRenderer
|
<VideoAvailableRenderer
|
||||||
key={"video-available"}
|
key={"video-available-screen"}
|
||||||
callbackEnable={() => {
|
callbackEnable={() => events.fire("action_toggle_mute", { broadcastType: "screen", muted: false, videoId: props.videoId })}
|
||||||
if(state.desktopStream === "available") {
|
callbackIgnore={() => events.fire("action_dismiss", { broadcastType: "screen", videoId: props.videoId })}
|
||||||
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 })
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={streamClasses.pop_front()}
|
className={streamClasses.pop_front()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else if(props.screenState === "streaming") {
|
||||||
if(state.desktopStream) {
|
streamElements.push(
|
||||||
if(!state.dismissed["screen"] || state.desktopStream !== "available") {
|
<VideoStreamRenderer key={"stream-screen"} videoId={props.videoId} streamType={"screen"} className={streamClasses.pop_front()} />
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<div className={cssStyle.text} key={"no-video"}>
|
<div className={cssStyle.text} key={"no-video-stream"}>
|
||||||
<div>
|
<div>
|
||||||
{props.videoId === kLocalVideoId ?
|
{props.videoId === kLocalVideoId ?
|
||||||
<Translatable key={"own"}>You're not broadcasting video</Translatable> :
|
<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 }) => {
|
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 fullscreenCapable = "requestFullscreen" in HTMLElement.prototype;
|
||||||
|
|
||||||
const [ isFullscreen, setFullscreen ] = useState(false);
|
const [ isFullscreen, setFullscreen ] = useState(false);
|
||||||
const [ muteState, setMuteState ] = useState<{[T in VideoBroadcastType]: "muted" | "available" | "unset"}>(() => {
|
|
||||||
events.fire("query_video_mute_status", { videoId: props.videoId });
|
const [ cameraState, setCameraState ] = useState<ChannelVideoStreamState>("none");
|
||||||
return { camera: "unset", screen: "unset" };
|
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) {
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cssStyle.videoContainer}
|
className={cssStyle.videoContainer}
|
||||||
|
@ -390,35 +422,15 @@ const VideoContainer = React.memo((props: { videoId: string, isSpotlight: boolea
|
||||||
}}
|
}}
|
||||||
ref={refContainer}
|
ref={refContainer}
|
||||||
>
|
>
|
||||||
<VideoPlayer videoId={props.videoId} />
|
<VideoPlayer videoId={props.videoId} cameraState={cameraState} screenState={screenState} />
|
||||||
<VideoInfo videoId={props.videoId} />
|
<VideoInfo videoId={props.videoId} />
|
||||||
<div className={cssStyle.actionIcons}>
|
<VideoControlButtons
|
||||||
<div className={cssStyle.iconContainer + " " + cssStyle.toggle + " " + toggleClass("screen")}
|
videoId={props.videoId}
|
||||||
onClick={() => events.fire("action_toggle_mute", { videoId: props.videoId, broadcastType: "screen", muted: muteState.screen === "available" })}
|
cameraState={cameraState}
|
||||||
title={muteState["screen"] === "muted" ? tr("Unmute screen video") : tr("Mute screen video")}
|
screenState={screenState}
|
||||||
>
|
isSpotlight={props.isSpotlight}
|
||||||
<ClientIconRenderer className={cssStyle.icon} icon={ClientIcon.ShareScreen} />
|
fullscreenMode={fullscreenCapable ? isFullscreen ? "set" : "none" : "unavailable"}
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue