diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index 1a4c5e87d..bb04eec3f 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -1,5 +1,6 @@ import React, { KeyboardEvent, + useCallback, useContext, useEffect, useMemo, @@ -159,7 +160,6 @@ function getMarkerTitle(marker: MarkerFragment) { } interface IScenePlayerProps { - className?: string; scene: GQL.SceneDataFragment | undefined | null; hideScrubberOverride: boolean; autoplay?: boolean; @@ -172,7 +172,6 @@ interface IScenePlayerProps { } export const ScenePlayer: React.FC = ({ - className, scene, hideScrubberOverride, autoplay, @@ -186,15 +185,14 @@ export const ScenePlayer: React.FC = ({ const { configuration } = useContext(ConfigurationContext); const interfaceConfig = configuration?.interface; const uiConfig = configuration?.ui as IUIConfig | undefined; - const videoRef = useRef(null); - const playerRef = useRef(); + const videoRef = useRef(null); + const [_player, setPlayer] = useState(); const sceneId = useRef(); const [sceneSaveActivity] = useSceneSaveActivity(); const [sceneIncrementPlayCount] = useSceneIncrementPlayCount(); const [time, setTime] = useState(0); const [ready, setReady] = useState(false); - const [sessionInitialised, setSessionInitialised] = useState(false); // tracks play session. This is reset whenever ScenePlayer page is exited const { interactive: interactiveClient, @@ -230,6 +228,12 @@ export const ScenePlayer: React.FC = ({ [file, permitLoop, maxLoopDuration] ); + const getPlayer = useCallback(() => { + if (!_player) return null; + if (_player.isDisposed()) return null; + return _player; + }, [_player]); + useEffect(() => { if (hideScrubberOverride || fullscreen) { setShowScrubber(false); @@ -249,18 +253,19 @@ export const ScenePlayer: React.FC = ({ useEffect(() => { sendSetTimestamp((value: number) => { - const player = playerRef.current; + const player = getPlayer(); if (player && value >= 0) { player.play()?.then(() => { player.currentTime(value); }); } }); - }, [sendSetTimestamp]); + }, [sendSetTimestamp, getPlayer]); // Initialize VideoJS player useEffect(() => { const options: VideoJsPlayerOptions = { + id: VIDEO_PLAYER_ID, controls: true, controlBar: { pictureInPictureToggle: false, @@ -292,6 +297,7 @@ export const ScenePlayer: React.FC = ({ playbackRates: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2], inactivityTimeout: 2000, preload: "none", + playsinline: true, userActions: { hotkeys: function (this: VideoJsPlayer, event) { handleHotkeys(this, event); @@ -314,33 +320,42 @@ export const ScenePlayer: React.FC = ({ }, }; - const player = videojs(videoRef.current!, options); + const videoEl = document.createElement("video-js"); + videoEl.setAttribute("data-vjs-player", "true"); + videoEl.classList.add("vjs-big-play-centered"); + videoRef.current!.appendChild(videoEl); + + const vjs = videojs(videoEl, options); /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - const settings = (player as any).textTrackSettings; + const settings = (vjs as any).textTrackSettings; settings.setValues({ backgroundColor: "#000", backgroundOpacity: "0.5", }); settings.updateDisplay(); - player.focus(); - playerRef.current = player; + vjs.focus(); + setPlayer(vjs); // Video player destructor return () => { - playerRef.current = undefined; - player.dispose(); + vjs.dispose(); + videoEl.remove(); + setPlayer(undefined); + + // reset sceneId to force reload sources + sceneId.current = undefined; }; }, []); useEffect(() => { - const player = playerRef.current; + const player = getPlayer(); if (!player) return; const skipButtons = player.skipButtons(); skipButtons.setForwardHandler(onNext); skipButtons.setBackwardHandler(onPrevious); - }, [onNext, onPrevious]); + }, [getPlayer, onNext, onPrevious]); useEffect(() => { if (scene?.interactive && interactiveInitialised) { @@ -358,6 +373,9 @@ export const ScenePlayer: React.FC = ({ // Player event handlers useEffect(() => { + const player = getPlayer(); + if (!player) return; + function canplay(this: VideoJsPlayer) { if (initialTimestamp.current !== -1) { this.currentTime(initialTimestamp.current); @@ -381,9 +399,6 @@ export const ScenePlayer: React.FC = ({ setFullscreen(this.isFullscreen()); } - const player = playerRef.current; - if (!player) return; - player.on("canplay", canplay); player.on("playing", playing); player.on("loadstart", loadstart); @@ -395,9 +410,12 @@ export const ScenePlayer: React.FC = ({ player.off("loadstart", loadstart); player.off("fullscreenchange", fullscreenchange); }; - }, []); + }, [getPlayer]); useEffect(() => { + const player = getPlayer(); + if (!player) return; + function onplay(this: VideoJsPlayer) { this.persistVolume().enabled = true; if (scene?.interactive && interactiveReady.current) { @@ -424,9 +442,6 @@ export const ScenePlayer: React.FC = ({ setTime(this.currentTime()); } - const player = playerRef.current; - if (!player) return; - player.on("play", onplay); player.on("pause", pause); player.on("seeking", seeking); @@ -438,26 +453,22 @@ export const ScenePlayer: React.FC = ({ player.off("seeking", seeking); player.off("timeupdate", timeupdate); }; - }, [interactiveClient, scene]); + }, [getPlayer, interactiveClient, scene]); useEffect(() => { - const player = playerRef.current; + const player = getPlayer(); if (!player) return; // don't re-initialise the player unless the scene has changed if (!scene || !file || scene.id === sceneId.current) return; - // if new scene was picked from playlist - if (playerRef.current && sceneId.current) { - if (trackActivity) { - playerRef.current.trackActivity().reset(); - } - } - sceneId.current = scene.id; setReady(false); + // reset on new scene + player.trackActivity().reset(); + // always stop the interactive client on initialisation interactiveClient.pause(); interactiveReady.current = false; @@ -546,19 +557,19 @@ export const ScenePlayer: React.FC = ({ const alwaysStartFromBeginning = uiConfig?.alwaysStartFromBeginning ?? false; + const resumeTime = scene.resume_time ?? 0; let startPosition = _initialTimestamp; if ( !startPosition && - !(alwaysStartFromBeginning || sessionInitialised) && - file.duration > scene.resume_time! + !alwaysStartFromBeginning && + file.duration > resumeTime ) { - startPosition = scene.resume_time!; + startPosition = resumeTime; } initialTimestamp.current = startPosition; setTime(startPosition); - setSessionInitialised(true); player.load(); player.focus(); @@ -574,11 +585,10 @@ export const ScenePlayer: React.FC = ({ interactiveClient.pause(); }; }, [ + getPlayer, file, scene, - trackActivity, interactiveClient, - sessionInitialised, autoplay, interfaceConfig?.autostartVideo, uiConfig?.alwaysStartFromBeginning, @@ -586,7 +596,7 @@ export const ScenePlayer: React.FC = ({ ]); useEffect(() => { - const player = playerRef.current; + const player = getPlayer(); if (!player || !scene) return; const markers = player.markers(); @@ -603,10 +613,10 @@ export const ScenePlayer: React.FC = ({ } else { player.poster(""); } - }, [scene]); + }, [getPlayer, scene]); useEffect(() => { - const player = playerRef.current; + const player = getPlayer(); if (!player) return; async function saveActivity(resumeTime: number, playDuration: number) { @@ -637,6 +647,7 @@ export const ScenePlayer: React.FC = ({ activity.minimumPlayPercent = minimumPlayPercent; activity.setEnabled(trackActivity); }, [ + getPlayer, scene, trackActivity, minimumPlayPercent, @@ -645,15 +656,16 @@ export const ScenePlayer: React.FC = ({ ]); useEffect(() => { - const player = playerRef.current; + const player = getPlayer(); if (!player) return; player.loop(looping); interactiveClient.setLooping(looping); - }, [interactiveClient, looping]); + }, [getPlayer, interactiveClient, looping]); useEffect(() => { - if (!scene || !ready || !auto.current) { + const player = getPlayer(); + if (!player || !scene || !ready || !auto.current) { return; } @@ -666,9 +678,6 @@ export const ScenePlayer: React.FC = ({ return; } - const player = playerRef.current; - if (!player) return; - player.play()?.catch(() => { // Browser probably blocking non-muted autoplay, so mute and try again player.persistVolume().enabled = false; @@ -677,35 +686,36 @@ export const ScenePlayer: React.FC = ({ player.play(); }); auto.current = false; - }, [scene, ready, interactiveClient, currentScript]); + }, [getPlayer, scene, ready, interactiveClient, currentScript]); + // Attach handler for onComplete event useEffect(() => { - // Attach handler for onComplete event - const player = playerRef.current; + const player = getPlayer(); if (!player) return; player.on("ended", onComplete); return () => player.off("ended"); - }, [onComplete]); + }, [getPlayer, onComplete]); - const onScrubberScroll = () => { + function onScrubberScroll() { if (started.current) { - playerRef.current?.pause(); + getPlayer()?.pause(); } - }; - const onScrubberSeek = (seconds: number) => { + } + + function onScrubberSeek(seconds: number) { if (started.current) { - playerRef.current?.currentTime(seconds); + getPlayer()?.currentTime(seconds); } else { initialTimestamp.current = seconds; setTime(seconds); } - }; + } // Override spacebar to always pause/play function onKeyDown(this: HTMLDivElement, event: KeyboardEvent) { - const player = playerRef.current; + const player = getPlayer(); if (!player) return; if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) { @@ -730,17 +740,10 @@ export const ScenePlayer: React.FC = ({ className={cx("VideoPlayer", { portrait: isPortrait })} onKeyDownCapture={onKeyDown} > -
-
+
{scene?.interactive && (interactiveState !== ConnectionState.Ready || - playerRef.current?.paused()) && } + getPlayer()?.paused()) && } {scene && file && showScrubber && ( src === source); if (this.selectedIndex === -1) return; + const loadSrc = this.sources[this.selectedIndex]; + const currentTime = player.currentTime(); - - // put the selected source at the top of the list - const loadSources = [...this.sources]; - const selectedSrc = loadSources.splice(this.selectedIndex, 1)[0]; - loadSources.unshift(selectedSrc); - const paused = player.paused(); - player.src(loadSources); + + player.src(loadSrc); player.one("canplay", () => { if (paused) { player.pause(); @@ -128,10 +125,15 @@ class SourceSelectorPlugin extends videojs.getPlugin("plugin") { if (!player.videoWidth() && !player.videoHeight()) { // Occurs during preload when videos with supported audio/unsupported video are preloaded. // Treat this as a decoding error and try the next source without playing. - // However on Safari we get an media event when m3u8 is loaded which needs to be ignored. + // However on Safari we get an media event when m3u8 or mpd is loaded which needs to be ignored. if (player.error() !== null) return; + const currentSrc = player.currentSrc(); - if (currentSrc !== null && !currentSrc.includes(".m3u8")) { + if (currentSrc === null) return; + + if (currentSrc.includes(".m3u8") || currentSrc.includes(".mpd")) { + player.play(); + } else { player.error(MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED); return; } @@ -156,7 +158,7 @@ class SourceSelectorPlugin extends videojs.getPlugin("plugin") { console.log(`Trying next source in playlist: '${newSource.label}'`); this.menu.setSources(this.sources); this.selectedIndex = 0; - player.src(this.sources); + player.src(newSource); player.load(); player.play(); } else { @@ -179,7 +181,7 @@ class SourceSelectorPlugin extends videojs.getPlugin("plugin") { } this.sources = sources; - this.player.src(this.sources); + this.player.src(sources[0]); } get textTracks(): HTMLTrackElement[] { diff --git a/ui/v2.5/src/components/ScenePlayer/styles.scss b/ui/v2.5/src/components/ScenePlayer/styles.scss index 010ed1dcc..c8bee39ea 100644 --- a/ui/v2.5/src/components/ScenePlayer/styles.scss +++ b/ui/v2.5/src/components/ScenePlayer/styles.scss @@ -16,12 +16,12 @@ $sceneTabWidth: 450px; height: 100vh; } - &.portrait .video-js { + &.portrait .video-wrapper { height: 177.78vw; } } -.video-js { +.video-wrapper { height: 56.25vw; overflow: hidden; width: 100%; @@ -29,6 +29,11 @@ $sceneTabWidth: 450px; @media (min-width: 1200px) { height: 100%; } +} + +.video-js { + height: 100%; + width: 100%; .vjs-button { outline: none; @@ -109,7 +114,7 @@ $sceneTabWidth: 450px; width: 100%; .vjs-progress-holder { - margin: 0 1rem; + margin: 0 15px; } } diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx index 847237cd6..5d6bb3690 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx @@ -765,7 +765,6 @@ const SceneLoader: React.FC = () => {
{ let plugins = [ - react(), + react({ + babel: { + compact: true, + }, + }), tsconfigPaths(), viteCompression({ algorithm: "gzip",