264 lines
10 KiB
TypeScript
264 lines
10 KiB
TypeScript
![]() |
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 {ChannelVideo, ChannelVideoEvents, ChannelVideoInfo, kLocalVideoId} 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";
|
||
|
import {ClientTag} from "tc-shared/ui/tree/EntryTags";
|
||
|
import ResizeObserver from "resize-observer-polyfill";
|
||
|
|
||
|
const EventContext = React.createContext<Registry<ChannelVideoEvents>>(undefined);
|
||
|
const HandlerIdContext = React.createContext<string>(undefined);
|
||
|
|
||
|
const cssStyle = require("./Renderer.scss");
|
||
|
|
||
|
const ExpendArrow = () => {
|
||
|
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 (
|
||
|
<div className={cssStyle.expendArrow} onClick={() => events.fire("action_toggle_expended", { expended: !expended })}>
|
||
|
<ClientIconRenderer icon={ClientIcon.DoubleArrow} className={cssStyle.icon} />
|
||
|
</div>
|
||
|
)
|
||
|
};
|
||
|
|
||
|
const VideoInfo = React.memo((props: { videoId: string }) => {
|
||
|
const events = useContext(EventContext);
|
||
|
const handlerId = useContext(HandlerIdContext);
|
||
|
|
||
|
const localVideo = props.videoId === kLocalVideoId;
|
||
|
const nameClassList = cssStyle.name + " " + (localVideo ? cssStyle.local : "");
|
||
|
|
||
|
const [ info, setInfo ] = useState<"loading" | ChannelVideoInfo>(() => {
|
||
|
events.fire("query_video_info", { videoId: props.videoId });
|
||
|
return "loading";
|
||
|
});
|
||
|
|
||
|
const [ statusIcon, setStatusIcon ] = useState<ClientIcon>(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 = <div className={nameClassList} key={"loading"}><Translatable>loading</Translatable> {props.videoId} <LoadingDots /></div>;
|
||
|
} else {
|
||
|
clientName = <ClientTag clientName={info.clientName} clientUniqueId={info.clientUniqueId} clientId={info.clientId} handlerId={handlerId} className={nameClassList} key={"loaded"} />;
|
||
|
}
|
||
|
|
||
|
return (
|
||
|
<div className={cssStyle.info}>
|
||
|
<ClientIconRenderer icon={statusIcon} className={cssStyle.icon} />
|
||
|
{clientName}
|
||
|
</div>
|
||
|
);
|
||
|
});
|
||
|
|
||
|
const VideoStreamReplay = React.memo((props: { stream: MediaStream | undefined, className: string }) => {
|
||
|
const refVideo = useRef<HTMLVideoElement>();
|
||
|
|
||
|
useEffect(() => {
|
||
|
if(props.stream) {
|
||
|
refVideo.current.style.opacity = "1";
|
||
|
refVideo.current.srcObject = props.stream;
|
||
|
} else {
|
||
|
refVideo.current.style.opacity = "0";
|
||
|
}
|
||
|
}, [ props.stream ]);
|
||
|
|
||
|
return (
|
||
|
<video ref={refVideo} autoPlay={true} className={cssStyle.video + " " + props.className} />
|
||
|
)
|
||
|
});
|
||
|
|
||
|
const VideoPlayer = React.memo((props: { videoId: string }) => {
|
||
|
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);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
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") {
|
||
|
if(state.desktopStream && state.cameraStream) {
|
||
|
/* TODO: Select primary and secondary and display them */
|
||
|
return (
|
||
|
<VideoStreamReplay stream={state.desktopStream} key={"replay-multi"} className={cssStyle.videoPrimary} />
|
||
|
);
|
||
|
} else {
|
||
|
const stream = state.desktopStream || state.cameraStream;
|
||
|
if(stream) {
|
||
|
return (
|
||
|
<VideoStreamReplay stream={stream} key={"replay-single"} className={cssStyle.videoPrimary} />
|
||
|
);
|
||
|
} else {
|
||
|
return (
|
||
|
<div className={cssStyle.text} key={"no-video-stream"}>
|
||
|
<div><Translatable>No Video</Translatable></div>
|
||
|
</div>
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
} else if(state.status === "no-video") {
|
||
|
return (
|
||
|
<div className={cssStyle.text} key={"no-video"}>
|
||
|
<div><Translatable>No Video</Translatable></div>
|
||
|
</div>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
});
|
||
|
|
||
|
const VideoContainer = React.memo((props: { videoId: string }) => (
|
||
|
<div className={cssStyle.videoContainer}>
|
||
|
<VideoPlayer videoId={props.videoId} />
|
||
|
<VideoInfo videoId={props.videoId} />
|
||
|
</div>
|
||
|
));
|
||
|
|
||
|
const VideoBarArrow = React.memo((props: { direction: "left" | "right", containerRef: React.RefObject<HTMLDivElement> }) => {
|
||
|
const events = useContext(EventContext);
|
||
|
const [ shown, setShown ] = useState(false);
|
||
|
events.reactUse("notify_video_arrows", event => setShown(event[props.direction]));
|
||
|
|
||
|
return (
|
||
|
<div className={cssStyle.arrow + " " + cssStyle[props.direction] + " " + (shown ? "" : cssStyle.hidden)} ref={props.containerRef}>
|
||
|
<div className={cssStyle.iconContainer} onClick={() => events.fire("action_video_scroll", { direction: props.direction })}>
|
||
|
<ClientIconRenderer icon={ClientIcon.SimpleArrow} className={cssStyle.icon} />
|
||
|
</div>
|
||
|
</div>
|
||
|
);
|
||
|
})
|
||
|
|
||
|
const VideoBar = () => {
|
||
|
const events = useContext(EventContext);
|
||
|
const refVideos = useRef<HTMLDivElement>();
|
||
|
const refArrowRight = useRef<HTMLDivElement>();
|
||
|
const refArrowLeft = useRef<HTMLDivElement>();
|
||
|
|
||
|
const [ videos, setVideos ] = useState<"loading" | string[]>(() => {
|
||
|
events.fire("query_videos");
|
||
|
return "loading";
|
||
|
});
|
||
|
events.reactUse("notify_videos", event => setVideos(event.videoIds));
|
||
|
|
||
|
const updateScrollButtons = useCallback(() => {
|
||
|
const container = refVideos.current;
|
||
|
if(!container) { return; }
|
||
|
|
||
|
const rightEndReached = container.scrollLeft + container.clientWidth + 1 >= container.scrollWidth;
|
||
|
const leftEndReached = container.scrollLeft <= .9;
|
||
|
events.fire("notify_video_arrows", { left: !leftEndReached, right: !rightEndReached });
|
||
|
}, [ refVideos ]);
|
||
|
|
||
|
events.reactUse("action_video_scroll", event => {
|
||
|
const container = refVideos.current;
|
||
|
const arrowLeft = refArrowLeft.current;
|
||
|
const arrowRight = refArrowRight.current;
|
||
|
if(container && arrowLeft && arrowRight) {
|
||
|
const children = [...container.children] as HTMLElement[];
|
||
|
if(event.direction === "left") {
|
||
|
const currentCutOff = container.scrollLeft;
|
||
|
const element = children.filter(element => element.offsetLeft >= currentCutOff)
|
||
|
.sort((a, b) => a.offsetLeft - b.offsetLeft)[0];
|
||
|
|
||
|
container.scrollLeft = (element.offsetLeft + element.clientWidth) - (container.clientWidth - arrowRight.clientWidth);
|
||
|
} else {
|
||
|
const currentCutOff = container.scrollLeft + container.clientWidth;
|
||
|
const element = children.filter(element => element.offsetLeft <= currentCutOff)
|
||
|
.sort((a, b) => a.offsetLeft - b.offsetLeft)
|
||
|
.last();
|
||
|
|
||
|
container.scrollLeft = element.offsetLeft - arrowLeft.clientWidth;
|
||
|
}
|
||
|
}
|
||
|
updateScrollButtons();
|
||
|
}, undefined, [ updateScrollButtons ]);
|
||
|
|
||
|
useEffect(() => {
|
||
|
updateScrollButtons();
|
||
|
}, [ videos ]);
|
||
|
|
||
|
useEffect(() => {
|
||
|
const animationRequest = { current: 0 };
|
||
|
const observer = new ResizeObserver(() => {
|
||
|
if(animationRequest.current) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
animationRequest.current = requestAnimationFrame(() => {
|
||
|
animationRequest.current = 0;
|
||
|
updateScrollButtons();
|
||
|
})
|
||
|
});
|
||
|
observer.observe(refVideos.current);
|
||
|
return () => observer.disconnect();
|
||
|
}, [ refVideos ]);
|
||
|
|
||
|
return (
|
||
|
<div className={cssStyle.videoBar}>
|
||
|
<div className={cssStyle.videos} ref={refVideos}>
|
||
|
{videos === "loading" ? undefined :
|
||
|
videos.map(videoId => <VideoContainer videoId={videoId} key={videoId} />)
|
||
|
}
|
||
|
</div>
|
||
|
<VideoBarArrow direction={"left"} containerRef={refArrowLeft} />
|
||
|
<VideoBarArrow direction={"right"} containerRef={refArrowRight} />
|
||
|
</div>
|
||
|
)
|
||
|
};
|
||
|
|
||
|
export const ChannelVideoRenderer = (props: { handlerId: string, events: Registry<ChannelVideoEvents> }) => {
|
||
|
return (
|
||
|
<EventContext.Provider value={props.events}>
|
||
|
<HandlerIdContext.Provider value={props.handlerId}>
|
||
|
<div className={cssStyle.panel}>
|
||
|
<VideoBar />
|
||
|
<ExpendArrow />
|
||
|
</div>
|
||
|
</HandlerIdContext.Provider>
|
||
|
</EventContext.Provider>
|
||
|
)
|
||
|
};
|