qhkr2z22o: Prefer hls.js and fall back to the thumbnail when video cannot play
This commit is contained in:
@@ -9,9 +9,12 @@ export interface VideoPlayerProps {
|
|||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UNPLAYABLE = "Video can't be played here.";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Real <video controls>. Native HLS where the browser has it, otherwise hls.js,
|
* Real <video controls>. hls.js (loaded only when a video card mounts) is
|
||||||
* loaded only when a video card mounts. Never autoplays.
|
* preferred; native HLS is used only where hls.js is unsupported. Never
|
||||||
|
* autoplays. Any failure falls back to the thumbnail with a short note.
|
||||||
*/
|
*/
|
||||||
export function VideoPlayer({ hlsUrl, poster, label }: VideoPlayerProps) {
|
export function VideoPlayer({ hlsUrl, poster, label }: VideoPlayerProps) {
|
||||||
const ref = useRef<HTMLVideoElement>(null);
|
const ref = useRef<HTMLVideoElement>(null);
|
||||||
@@ -22,28 +25,26 @@ export function VideoPlayer({ hlsUrl, poster, label }: VideoPlayerProps) {
|
|||||||
if (!video) return;
|
if (!video) return;
|
||||||
let destroy: (() => void) | undefined;
|
let destroy: (() => void) | undefined;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
const native = supportsNativeHls(video);
|
|
||||||
if (native) {
|
|
||||||
if (choosePlayback(hlsUrl, { nativeHls: true, hlsJsSupported: false }) === "native") {
|
|
||||||
video.src = hlsUrl;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
import("hls.js")
|
import("hls.js")
|
||||||
.then(({ default: Hls }) => {
|
.then(({ default: Hls }) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
const mode = choosePlayback(hlsUrl, { nativeHls: false, hlsJsSupported: Hls.isSupported() });
|
const mode = choosePlayback(hlsUrl, {
|
||||||
if (mode !== "hlsjs") {
|
nativeHls: supportsNativeHls(video),
|
||||||
setFailed(true);
|
hlsJsSupported: Hls.isSupported(),
|
||||||
return;
|
|
||||||
}
|
|
||||||
const hls = new Hls();
|
|
||||||
hls.on(Hls.Events.ERROR, (_e, data) => {
|
|
||||||
if (data.fatal) setFailed(true);
|
|
||||||
});
|
});
|
||||||
hls.loadSource(hlsUrl);
|
if (mode === "hlsjs") {
|
||||||
hls.attachMedia(video);
|
const hls = new Hls();
|
||||||
destroy = () => hls.destroy();
|
hls.on(Hls.Events.ERROR, (_e, data) => {
|
||||||
|
if (data.fatal) setFailed(true);
|
||||||
|
});
|
||||||
|
hls.loadSource(hlsUrl);
|
||||||
|
hls.attachMedia(video);
|
||||||
|
destroy = () => hls.destroy();
|
||||||
|
} else if (mode === "native") {
|
||||||
|
video.src = hlsUrl;
|
||||||
|
} else {
|
||||||
|
setFailed(true);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(() => setFailed(true));
|
.catch(() => setFailed(true));
|
||||||
return () => {
|
return () => {
|
||||||
@@ -52,9 +53,16 @@ export function VideoPlayer({ hlsUrl, poster, label }: VideoPlayerProps) {
|
|||||||
};
|
};
|
||||||
}, [hlsUrl]);
|
}, [hlsUrl]);
|
||||||
|
|
||||||
if (failed && poster) {
|
if (failed) {
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
return (
|
||||||
return <img src={poster} alt="Video thumbnail" loading="lazy" />;
|
<div>
|
||||||
|
{poster ? (
|
||||||
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
|
<img src={poster} alt="Video thumbnail" loading="lazy" />
|
||||||
|
) : null}
|
||||||
|
<p role="status">{UNPLAYABLE}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<video
|
<video
|
||||||
@@ -64,6 +72,7 @@ export function VideoPlayer({ hlsUrl, poster, label }: VideoPlayerProps) {
|
|||||||
preload="none"
|
preload="none"
|
||||||
poster={poster ?? undefined}
|
poster={poster ?? undefined}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
|
onError={() => setFailed(true)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,15 @@ export interface PlaybackEnv {
|
|||||||
hlsJsSupported: boolean;
|
hlsJsSupported: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** No hlsUrl: thumbnail only. Native HLS wins; hls.js is the fallback. */
|
/**
|
||||||
|
* No hlsUrl: thumbnail only. hls.js (Media Source Extensions) is preferred
|
||||||
|
* because Chromium reports partial native HLS support it cannot honour; native
|
||||||
|
* HLS is only chosen where hls.js is unsupported (Safari on iOS).
|
||||||
|
*/
|
||||||
export function choosePlayback(hlsUrl: string | null | undefined, env: PlaybackEnv): Playback {
|
export function choosePlayback(hlsUrl: string | null | undefined, env: PlaybackEnv): Playback {
|
||||||
if (!hlsUrl) return "none";
|
if (!hlsUrl) return "none";
|
||||||
if (env.nativeHls) return "native";
|
if (env.hlsJsSupported) return "hlsjs";
|
||||||
return env.hlsJsSupported ? "hlsjs" : "none";
|
return env.nativeHls ? "native" : "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function supportsNativeHls(video: Pick<HTMLVideoElement, "canPlayType">): boolean {
|
export function supportsNativeHls(video: Pick<HTMLVideoElement, "canPlayType">): boolean {
|
||||||
|
|||||||
@@ -19,10 +19,13 @@ describe("choosePlayback", () => {
|
|||||||
expect(choosePlayback(null, { nativeHls: true, hlsJsSupported: true })).toBe("none");
|
expect(choosePlayback(null, { nativeHls: true, hlsJsSupported: true })).toBe("none");
|
||||||
expect(choosePlayback(undefined, { nativeHls: false, hlsJsSupported: true })).toBe("none");
|
expect(choosePlayback(undefined, { nativeHls: false, hlsJsSupported: true })).toBe("none");
|
||||||
});
|
});
|
||||||
it("uses native HLS where supported, hls.js otherwise", () => {
|
it("prefers hls.js whenever it is supported, even if native HLS is reported", () => {
|
||||||
expect(choosePlayback(HLS, { nativeHls: true, hlsJsSupported: true })).toBe("native");
|
expect(choosePlayback(HLS, { nativeHls: true, hlsJsSupported: true })).toBe("hlsjs");
|
||||||
expect(choosePlayback(HLS, { nativeHls: false, hlsJsSupported: true })).toBe("hlsjs");
|
expect(choosePlayback(HLS, { nativeHls: false, hlsJsSupported: true })).toBe("hlsjs");
|
||||||
});
|
});
|
||||||
|
it("uses native HLS only when hls.js is unsupported", () => {
|
||||||
|
expect(choosePlayback(HLS, { nativeHls: true, hlsJsSupported: false })).toBe("native");
|
||||||
|
});
|
||||||
it("falls back to none when neither is available", () => {
|
it("falls back to none when neither is available", () => {
|
||||||
expect(choosePlayback(HLS, { nativeHls: false, hlsJsSupported: false })).toBe("none");
|
expect(choosePlayback(HLS, { nativeHls: false, hlsJsSupported: false })).toBe("none");
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user