Fix deceptive WEBM playback in Safari (#3676)

* Fix babel deoptimization warning in vite dev server
* Fix videojs HMR
* Fix fake WEBM support in Safari
This commit is contained in:
DingDongSoLong4
2023-05-04 05:33:39 +02:00
committed by GitHub
parent f3f7ee7fd2
commit b7d179e448
5 changed files with 95 additions and 82 deletions

View File

@@ -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<IScenePlayerProps> = ({
className,
scene,
hideScrubberOverride,
autoplay,
@@ -186,15 +185,14 @@ export const ScenePlayer: React.FC<IScenePlayerProps> = ({
const { configuration } = useContext(ConfigurationContext);
const interfaceConfig = configuration?.interface;
const uiConfig = configuration?.ui as IUIConfig | undefined;
const videoRef = useRef<HTMLVideoElement>(null);
const playerRef = useRef<VideoJsPlayer>();
const videoRef = useRef<HTMLDivElement>(null);
const [_player, setPlayer] = useState<VideoJsPlayer>();
const sceneId = useRef<string>();
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<IScenePlayerProps> = ({
[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<IScenePlayerProps> = ({
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<IScenePlayerProps> = ({
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<IScenePlayerProps> = ({
},
};
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<IScenePlayerProps> = ({
// 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<IScenePlayerProps> = ({
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<IScenePlayerProps> = ({
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<IScenePlayerProps> = ({
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<IScenePlayerProps> = ({
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<IScenePlayerProps> = ({
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<IScenePlayerProps> = ({
interactiveClient.pause();
};
}, [
getPlayer,
file,
scene,
trackActivity,
interactiveClient,
sessionInitialised,
autoplay,
interfaceConfig?.autostartVideo,
uiConfig?.alwaysStartFromBeginning,
@@ -586,7 +596,7 @@ export const ScenePlayer: React.FC<IScenePlayerProps> = ({
]);
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<IScenePlayerProps> = ({
} 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<IScenePlayerProps> = ({
activity.minimumPlayPercent = minimumPlayPercent;
activity.setEnabled(trackActivity);
}, [
getPlayer,
scene,
trackActivity,
minimumPlayPercent,
@@ -645,15 +656,16 @@ export const ScenePlayer: React.FC<IScenePlayerProps> = ({
]);
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<IScenePlayerProps> = ({
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<IScenePlayerProps> = ({
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<IScenePlayerProps> = ({
className={cx("VideoPlayer", { portrait: isPortrait })}
onKeyDownCapture={onKeyDown}
>
<div data-vjs-player className={cx("video-wrapper", className)}>
<video
playsInline
ref={videoRef}
id={VIDEO_PLAYER_ID}
className="video-js vjs-big-play-centered"
/>
</div>
<div className="video-wrapper" ref={videoRef} />
{scene?.interactive &&
(interactiveState !== ConnectionState.Ready ||
playerRef.current?.paused()) && <SceneInteractiveStatus />}
getPlayer()?.paused()) && <SceneInteractiveStatus />}
{scene && file && showScrubber && (
<ScenePlayerScrubber
file={file}

View File

@@ -99,15 +99,12 @@ class SourceSelectorPlugin extends videojs.getPlugin("plugin") {
this.selectedIndex = this.sources.findIndex((src) => 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[] {

View File

@@ -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;
}
}

View File

@@ -765,7 +765,6 @@ const SceneLoader: React.FC = () => {
<div className={`scene-player-container ${collapsed ? "expanded" : ""}`}>
<ScenePlayer
key="ScenePlayer"
className="w-100 m-sm-auto no-gutter"
scene={scene}
hideScrubberOverride={hideScrubber}
autoplay={autoplay}

View File

@@ -10,7 +10,11 @@ const sourcemap = process.env.VITE_APP_SOURCEMAPS === "true";
// https://vitejs.dev/config/
export default defineConfig(() => {
let plugins = [
react(),
react({
babel: {
compact: true,
},
}),
tsconfigPaths(),
viteCompression({
algorithm: "gzip",