import * as React from "react"; import {useCallback, useContext, useEffect, useRef, useState} from "react"; import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons"; import {ClientIcon} from "svg-sprites/client-icons"; import {Registry} from "tc-shared/events"; import { ChannelVideoEvents, ChannelVideoInfo, ChannelVideoStreamState, kLocalVideoId, makeVideoAutoplay, VideoStreamState, VideoSubscribeInfo } from "./Definitions"; import {Translatable} from "tc-shared/ui/react-elements/i18n"; import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots"; import ResizeObserver from "resize-observer-polyfill"; import {LogCategory, logTrace, logWarn} from "tc-shared/log"; import {spawnContextMenu} from "tc-shared/ui/ContextMenu"; import {VideoBroadcastType} from "tc-shared/connection/VideoConnection"; import {ErrorBoundary} from "tc-shared/ui/react-elements/ErrorBoundary"; import {joinClassList, useTr} from "tc-shared/ui/react-elements/Helper"; import {Spotlight, SpotlightDimensions, SpotlightDimensionsContext} from "./RendererSpotlight"; import * as _ from "lodash"; import {ClientTag} from "tc-shared/ui/tree/EntryTags"; import {tra} from "tc-shared/i18n/localize"; const SubscribeContext = React.createContext(undefined); const EventContext = React.createContext>(undefined); const HandlerIdContext = React.createContext(undefined); export const VideoIdContext = React.createContext(undefined); export const RendererVideoEventContext = EventContext; const cssStyle = require("./Renderer.scss"); const ExpendArrow = React.memo(() => { const events = useContext(EventContext); const [ expended, setExpended ] = useState(() => { events.fire("query_expended"); return false; }); events.reactUse("notify_expended", event => setExpended(event.expended), undefined, [ setExpended ]); return (
events.fire("action_toggle_expended", { expended: !expended })}>
); }); const VideoViewerCount = React.memo(() => { const videoId = useContext(VideoIdContext); const events = useContext(EventContext); if(videoId !== kLocalVideoId) { /* Currently one we can see our own video viewer */ return null; } const [ viewer, setViewer ] = useState<{ camera: number | undefined, screen: number | undefined }>(() => { events.fire("query_viewer_count"); return { screen: undefined, camera: undefined }; }); events.reactUse("notify_viewer_count", event => setViewer({ camera: event.camera, screen: event.screen })); let info = []; if(typeof viewer.camera === "number") { info.push(
{viewer.camera}
); } if(typeof viewer.screen === "number") { info.push(
{viewer.screen}
); } if(info.length === 0) { /* We're not streaming any video */ return null; } return (
events.fire("action_show_viewers")} onDoubleClick={event => event.preventDefault()} > {info}
) }); const VideoClientInfo = React.memo((props: { videoId: string }) => { const events = useContext(EventContext); const handlerId = useContext(HandlerIdContext); const [ info, setInfo ] = useState<"loading" | ChannelVideoInfo>(() => { events.fire("query_video_info", { videoId: props.videoId }); return "loading"; }); const [ statusIcon, setStatusIcon ] = useState(ClientIcon.PlayerOff); events.reactUse("notify_video_info", event => { if(event.videoId === props.videoId) { setInfo(event.info); setStatusIcon(event.info.statusIcon); } }); events.reactUse("notify_video_info_status", event => { if(event.videoId === props.videoId) { setStatusIcon(event.statusIcon); } }); let clientName; if(info === "loading") { clientName = (
loading {props.videoId}
); } else { clientName = ; } return (
{clientName}
); }); const VideoSubscribeContextProvider = (props: { children?: React.ReactElement | React.ReactElement[] }) => { const events = useContext(EventContext); const [ subscribeInfo, setSubscribeInfo ] = useState(() => { events.fire("query_subscribe_info"); return { totalSubscriptions: 0, subscribedStreams: { screen: 0, camera: 0 }, subscribeLimits: {}, maxSubscriptions: undefined }; }); events.reactUse("notify_subscribe_info", event => setSubscribeInfo(event.info)); return ( {props.children} ); } const canSubscribe = (subscribeInfo: VideoSubscribeInfo, target: VideoBroadcastType) : boolean => { if(typeof subscribeInfo.maxSubscriptions === "number" && subscribeInfo.maxSubscriptions <= subscribeInfo.totalSubscriptions) { return false; } return typeof subscribeInfo.subscribeLimits[target] !== "number" || subscribeInfo.subscribeLimits[target] > subscribeInfo.subscribedStreams[target]; }; const VideoGeneralAvailableRenderer = (props: { videoId: string, haveScreen: boolean, haveCamera: boolean, className?: string }) => { const events = useContext(EventContext); const subscribeInfo = useContext(SubscribeContext); if((props.haveCamera && canSubscribe(subscribeInfo, "camera")) || (props.haveScreen && canSubscribe(subscribeInfo, "screen"))) { return (
Video available
events.fire("action_toggle_mute", { videoId: props.videoId, broadcastType: undefined, muted: false })}> Watch
); } else { return (
Stream subscribe limit reached {/* TODO: Name the failed permission */}
); } }; const VideoStreamAvailableRenderer = (props: { videoId: string, mode: VideoBroadcastType , className?: string }) => { const events = useContext(EventContext); const subscribeInfo = useContext(SubscribeContext); if(canSubscribe(subscribeInfo, props.mode)) { return (
Video available
events.fire("action_toggle_mute", { videoId: props.videoId, broadcastType: props.mode, muted: false })}> Watch
events.fire("action_dismiss", { videoId: props.videoId, broadcastType: props.mode })}> Ignore
); } else { return (
Stream subscribe limit reached {/* TODO: Name the failed permission */}
); } }; const MediaStreamVideoRenderer = React.memo((props: { stream: MediaStream | undefined, className: string, title: string }) => { const refVideo = useRef(); useEffect(() => { let cancelAutoplay; const video = refVideo.current; if(props.stream) { video.style.opacity = "1"; video.srcObject = props.stream; video.muted = true; cancelAutoplay = makeVideoAutoplay(video); } else { video.style.opacity = "0"; } return () => { const video = refVideo.current; if(video) { video.onpause = undefined; video.onended = undefined; } if(cancelAutoplay) { cancelAutoplay(); } } }, [ props.stream ]); return (