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;