import * as log from "tc-shared/log"; import {LogCategory} from "tc-shared/log"; import {Translatable} from "tc-shared/ui/react-elements/i18n"; import * as React from "react"; import {useEffect, useRef, useState} from "react"; import {Registry, RegistryMap} from "tc-shared/events"; import {PlayerStatus, VideoViewerEvents} from "./Definitions"; import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots"; import ReactPlayer from 'react-player' import {HTMLRenderer} from "tc-shared/ui/react-elements/HTMLRenderer"; import {Button} from "tc-shared/ui/react-elements/Button"; import "tc-shared/file/RemoteAvatars"; import {AvatarRenderer} from "tc-shared/ui/react-elements/Avatar"; import {getGlobalAvatarManagerFactory} from "tc-shared/file/Avatars"; import {Settings, settings} from "tc-shared/settings"; import {AbstractModal} from "tc-shared/ui/react-elements/ModalDefinitions"; const iconNavbar = require("./icon-navbar.svg"); const cssStyle = require("./Renderer.scss"); const kLogPlayerEvents = true; const PlaytimeRenderer = React.memo((props: { time: number }) => { const [ revision, setRevision ] = useState(0); useEffect(() => { const id = setTimeout(() => setRevision(revision + 1), 950); return () => clearTimeout(id); }); let seconds = Math.floor((Date.now() - props.time) / 1000); let hours = Math.floor(seconds / 3600); seconds %= 3600; let minutes = Math.floor(seconds / 60); seconds %= 60; let time = ("0" + hours).substr(-2) + ":" + ("0" + minutes).substr(-2) + ":" + ("0" + seconds).substr(-2); return <>{time}; }); const PlayerStatusRenderer = (props: { status: PlayerStatus | undefined, timestamp: number }) => { switch (props.status?.status) { case "paused": return ( Replay paused ); case "buffering": return ( Buffering  ); case "stopped": return ( Video ended  ); case "playing": return ( Playing  {props.timestamp === -1 ? undefined : <>()} ); case undefined: return ( loading  ); default: return ( unknown player status ({(props as any).status?.status}) ); } }; const WatcherInfo = React.memo((props: { events: Registry, watcherId: string, handlerId: string, isFollowing?: boolean, type: "watcher" | "follower" }) => { const [ clientInfo, setClientInfo ] = useState<"loading" | { uniqueId: string, clientId: number, clientName: string, ownClient: boolean }>(() => { props.events.fire("query_watcher_status", { watcherId: props.watcherId }); return "loading"; }); const [ status, setStatus ] = useState(() => { props.events.fire("query_watcher_info", { watcherId: props.watcherId }); return undefined; }); let renderedAvatar; if(clientInfo === "loading") { renderedAvatar = ; } else { const avatar = getGlobalAvatarManagerFactory().getManager(props.handlerId).resolveClientAvatar({ id: clientInfo.clientId, clientUniqueId: clientInfo.uniqueId }); renderedAvatar = ; } let renderedClientName; if(clientInfo !== "loading") { renderedClientName = {clientInfo.clientName}; } else { renderedClientName = ( loading  ); } props.events.reactUse("notify_watcher_info", event => { if(event.watcherId !== props.watcherId) return; setClientInfo({ uniqueId: event.clientUniqueId, clientId: event.clientId, clientName: event.clientName, ownClient: event.isOwnClient }); }); props.events.reactUse("notify_watcher_status", event => { if(event.watcherId !== props.watcherId) return; if(status?.status === "playing" && event.status.status === "playing") { const expectedPlaytime = (Date.now() - status.timestamp) / 1000 + status.timestampPlay; const currentPlaytime = event.status.timestampPlay; if(Math.abs(expectedPlaytime - currentPlaytime) > 2) { setStatus(Object.assign({ timestamp: Date.now() }, event.status)); } else { /* keep the last value, its still close enough */ setStatus({ status: "playing", timestamp: status.timestamp, timestampBuffer: 0, timestampPlay: status.timestampPlay }); } } else { setStatus(Object.assign({ timestamp: Date.now() }, event.status)); } }); return (
{ if(clientInfo === "loading") return; if(clientInfo.ownClient || props.isFollowing) return; props.events.fire("action_follow", { watcherId: props.watcherId }); }} >
{renderedAvatar}
); }); const WatcherEntry = React.memo((props: { events: Registry, watcherId: string, handlerId: string, isFollowing: boolean }) => { return (
); }); const FollowerList = React.memo((props: { events: Registry, watcherId: string, handlerId: string }) => { const [ followers, setFollowers ] = useState(() => { props.events.fire("query_followers", { watcherId: props.watcherId }); return []; }); const [ followerRevision, setFollowerRevision ] = useState(0); props.events.reactUse("notify_follower_list", event => { if(event.watcherId !== props.watcherId) return; setFollowers(event.followerIds.slice(0)); }); props.events.reactUse("notify_follower_added", event => { if(event.watcherId !== props.watcherId) return; if(followers.indexOf(event.followerId) !== -1) return; console.error("Added follower"); followers.push(event.followerId); setFollowerRevision(followerRevision + 1); }); props.events.reactUse("notify_follower_removed", event => { if(event.watcherId !== props.watcherId) return; const index = followers.indexOf(event.followerId); if(index === -1) return; console.error("Removed follower"); followers.splice(index, 1); setFollowerRevision(followerRevision + 1); }); return (
{followers.map(followerId => )}
); }); const WatcherList = (props: { events: Registry, handlerId: string }) => { const [ watchers, setWatchers ] = useState(() => { props.events.fire("query_watchers"); return []; }); const [ following, setFollowing ] = useState(undefined); props.events.reactUse("notify_watcher_list", event => { setWatchers(event.watcherIds.slice(0)); setFollowing(event.followingWatcher); }); props.events.reactUse("notify_following", event => setFollowing(event.watcherId)); return (
{watchers.map(watcherId => )}
); }; const ToggleSidebarButton = (props: { events: Registry }) => { const [ visible, setVisible ] = useState(settings.global(Settings.KEY_W2G_SIDEBAR_COLLAPSED)); props.events.reactUse("action_toggle_side_bar", event => setVisible(!event.shown)); return (
props.events.fire("action_toggle_side_bar", { shown: true })}> {iconNavbar}
); }; const ButtonUnfollow = (props: { events: Registry }) => { const [ following, setFollowing ] = useState(false); props.events.reactUse("notify_following", event => setFollowing(event.watcherId !== undefined)); props.events.reactUse("notify_watcher_list", event => setFollowing(event.followingWatcher !== undefined)); return ( ); }; const Sidebar = (props: { events: Registry, handlerId: string }) => { const [ visible, setVisible ] = useState(!settings.global(Settings.KEY_W2G_SIDEBAR_COLLAPSED)); props.events.reactUse("action_toggle_side_bar", event => setVisible(event.shown)); return (
props.events.fire("action_toggle_side_bar", { shown: false })} />
) }; const PlayerController = React.memo((props: { events: Registry }) => { const player = useRef(); const [ mode, setMode ] = useState<"watcher" | "follower">("watcher"); const [ videoUrl, setVideoUrl ] = useState<"querying" | string>(() => { props.events.fire_react("query_video"); return "querying"; }); const playerState = useRef<"playing" | "buffering" | "paused" | "stopped">("paused"); const currentTime = useRef<{ play: number, buffer: number }>({ play: -1, buffer: -1 }); const [ masterPlayerState, setWatcherPlayerState ] = useState<"playing" | "buffering" | "paused" | "stopped">("stopped"); const watcherTimestamp = useRef(); const videoEnded = useRef(false); const [ forcePause, setForcePause ] = useState(false); props.events.reactUse("notify_following", event => setMode(event.watcherId === undefined ? "watcher" : "follower")); props.events.reactUse("notify_watcher_list", event => setMode(event.followingWatcher === undefined ? "watcher" : "follower")); props.events.reactUse("notify_following_status", event => { if(mode !== "follower") return; setWatcherPlayerState(event.status.status); if(event.status.status === "playing" && player.current) { const distance = Math.abs(player.current.getCurrentTime() - event.status.timestampPlay); const doSeek = distance > 7; log.trace(LogCategory.GENERAL, tr("Follower sync. Remote timestamp %d, Local timestamp: %d. Difference: %d, Do seek: %o"), player.current.getCurrentTime(), event.status.timestampPlay, distance, doSeek ); if(doSeek) { player.current.seekTo(event.status.timestampPlay, "seconds"); } watcherTimestamp.current = Date.now() - event.status.timestampPlay * 1000; } }); props.events.reactUse("notify_video", event => setVideoUrl(event.url)); useEffect(() => { if(forcePause) setForcePause(false); }); /* TODO: Some kind of overlay if the video url is loading? */ return ( console.log("onError(%o, %o, %o, %o)", error, data, hlsInstance, hlsGlobal)} onBuffer={() => { kLogPlayerEvents && log.trace(LogCategory.GENERAL, tr("ReactPlayer::onBuffer()")); playerState.current = "buffering"; props.events.fire("notify_local_status", { status: { status: "buffering" } }); }} onBufferEnd={() => { if(playerState.current === "buffering") playerState.current = "playing"; kLogPlayerEvents && log.trace(LogCategory.GENERAL, tr("ReactPlayer::onBufferEnd()")); }} onDisablePIP={() => { /* console.log("onDisabledPIP()") */ }} onEnablePIP={() => { /* console.log("onEnablePIP()") */ }} onDuration={duration => { kLogPlayerEvents && log.trace(LogCategory.GENERAL, tr("ReactPlayer::onDuration(%d)"), duration); }} onEnded={() => { kLogPlayerEvents && log.trace(LogCategory.GENERAL, tr("ReactPlayer::onEnded()")); playerState.current = "stopped"; props.events.fire("notify_local_status", { status: { status: "stopped" } }); videoEnded.current = true; player.current.seekTo(0, "seconds"); }} onPause={() => { kLogPlayerEvents && log.trace(LogCategory.GENERAL, tr("ReactPlayer::onPause()")); if(videoEnded.current) { videoEnded.current = false; return; } playerState.current = "paused"; props.events.fire("notify_local_status", { status: { status: "paused" } }); }} onPlay={() => { kLogPlayerEvents && log.trace(LogCategory.GENERAL, tr("ReactPlayer::onPlay()")); if(videoEnded.current) { /* it's just the seek to the beginning */ return; } if(mode === "follower") { if(masterPlayerState !== "playing") { setForcePause(true); return; } const currentSeconds = player.current.getCurrentTime(); const expectedSeconds = (Date.now() - watcherTimestamp.current) / 1000; const doSync = Math.abs(currentSeconds - expectedSeconds) > 5; log.debug(LogCategory.GENERAL, tr("Player started, at second %d. Watcher is at %s. So sync: %o"), currentSeconds, expectedSeconds, doSync); doSync && player.current.seekTo(expectedSeconds, "seconds"); } playerState.current = "playing"; props.events.fire("notify_local_status", { status: { status: "playing", timestampBuffer: currentTime.current.buffer, timestampPlay: currentTime.current.play } }); }} onProgress={state => { kLogPlayerEvents && log.trace(LogCategory.GENERAL, tr("ReactPlayer::onProgress %d seconds played, %d seconds buffered. Player state: %s"), state.playedSeconds, state.loadedSeconds, playerState.current); currentTime.current = { buffer: state.loadedSeconds, play: state.playedSeconds }; if(playerState.current !== "playing") return; props.events.fire("notify_local_status", { status: { status: "playing", timestampBuffer: Math.floor(state.loadedSeconds), timestampPlay: Math.floor(state.playedSeconds) } }) }} onReady={() => { kLogPlayerEvents && log.trace(LogCategory.GENERAL, tr("ReactPlayer::onReady()")); }} onSeek={seconds => { kLogPlayerEvents && log.trace(LogCategory.GENERAL, tr("ReactPlayer::onSeek(%d)"), seconds); }} onStart={() => { kLogPlayerEvents && log.trace(LogCategory.GENERAL, tr("ReactPlayer::onStart()")); }} controls={true} loop={false} light={false} config={{ youtube: { playerVars: { rel: 0 } } }} playing={mode === "watcher" ? undefined : masterPlayerState === "playing" || forcePause} /> ); }); const TitleRenderer = (props: { events: Registry }) => { const [ followId, setFollowing ] = useState(undefined); const [ followingName, setFollowingName ] = useState(undefined); props.events.reactUse("notify_following", event => setFollowing(event.watcherId)); props.events.reactUse("notify_watcher_list", event => setFollowing(event.followingWatcher)); props.events.reactUse("notify_watcher_info", event => { if(event.watcherId !== followId) return; setFollowingName(event.clientName); }); useEffect(() => { if(followingName === undefined && followId) props.events.fire("query_watcher_info", { watcherId: followId }); }); if(followId && followingName) { return W2G - Following {followingName}; } else { return W2G - Watcher; } }; class ModalVideoPopout extends AbstractModal { readonly events: Registry; readonly handlerId: string; constructor(registryMap: RegistryMap, userData: any) { super(); this.handlerId = userData.handlerId; this.events = registryMap["default"] as any; } title(): string | React.ReactElement { return ; } renderBody(): React.ReactElement { return
; } } export = ModalVideoPopout;