/* eslint-disable @typescript-eslint/ban-ts-comment */ import Hls from "hls.js"; import { useEffect, useMemo, useRef, useState } from "react"; import { FormattedMessage } from "react-intl"; import { StreamState } from ".."; import { Icon } from "./icon"; import { ProgressBar } from "./progress-bar"; import { Menu, MenuItem } from "@szhsin/react-menu"; export enum VideoStatus { Online = "online", Offline = "offline", } export interface VideoPlayerProps { stream?: string; status?: string; poster?: string; } export default function LiveVideoPlayer(props: VideoPlayerProps) { const video = useRef(null); const hlsObj = useRef(null); const streamCached = useMemo(() => props.stream, [props.stream]); const [status, setStatus] = useState(); const [src, setSrc] = useState(); const [levels, setLevels] = useState>(); const [level, setLevel] = useState(-1); const [playState, setPlayState] = useState<"loading" | "playing" | "paused">("loading"); const [volume, setVolume] = useState(1); const [position, setPosition] = useState(); const [maxPosition, setMaxPosition] = useState(); useEffect(() => { if (streamCached && video.current) { if (Hls.isSupported()) { try { const hls = new Hls({ enableWorker: true, lowLatencyMode: true, backBufferLength: 90, }); hls.loadSource(streamCached); hls.attachMedia(video.current); hls.on(Hls.Events.ERROR, (event, data) => { console.debug(event, data); const errorType = data.type; if (errorType === Hls.ErrorTypes.NETWORK_ERROR && data.fatal) { hls.stopLoad(); hls.detachMedia(); setStatus(VideoStatus.Offline); } }); hls.on(Hls.Events.MANIFEST_PARSED, () => { setStatus(VideoStatus.Online); setLevels([ { level: -1, height: 0, }, ...hls.levels.map((a, i) => ({ level: i, height: a.height, })), ]); }); hls.on(Hls.Events.LEVEL_SWITCHING, (_, l) => { console.debug("HLS Level Switch", l); setMaxPosition(l.details?.totalduration); }); // @ts-ignore Can write anyway hlsObj.current = hls; return () => { // @ts-ignore Can write anyway hlsObj.current = null; hls.destroy(); }; } catch (e) { console.error(e); setStatus(VideoStatus.Offline); } } else { setSrc(streamCached); setStatus(VideoStatus.Online); video.current.muted = true; video.current.load(); } } }, [video, streamCached, props.status]); useEffect(() => { if (hlsObj.current) { hlsObj.current.nextLevel = level; } }, [hlsObj, level]); useEffect(() => { if (video.current) { video.current.onplaying = () => setPlayState("playing"); video.current.onpause = () => setPlayState("paused"); video.current.onseeking = () => setPlayState("loading"); video.current.onplay = () => setPlayState("loading"); video.current.onvolumechange = () => setVolume(video.current?.volume ?? 1); video.current.ontimeupdate = () => setPosition(video.current?.currentTime); } }, [video]); useEffect(() => { if (video.current) { video.current.volume = volume; } }, [video, volume]); function playStateToIcon() { switch (playState) { case "playing": return "pause"; case "paused": return "play"; case "loading": return "loading"; } } function togglePlay() { if (video.current) { if (playState === "playing") { video.current.pause(); } else if (playState === "paused") { video.current.play(); } } } function levelName(l: number) { if (l === -1) { return ; } else { const h = levels?.find(a => a.level === l)?.height; return ; } } return (
{status === VideoStatus.Online && (
togglePlay()}>
e.stopPropagation()}>
togglePlay()}>
{props.status}
{props.status === StreamState.Ended && maxPosition !== undefined && position !== undefined && ( { const ct = maxPosition * v; if (video.current) { video.current.currentTime = ct; } setPosition(ct); }} marker={
} style={{ width: "100%", height: "4px" }} /> )}
setVolume(v)} style={{ width: "100px", height: "100%" }} />
{levelName(level)}
} menuClassName="bg-primary w-fit"> {levels?.map(v => ( setLevel(v.level)} className="bg-primary px-3 py-2 text-white"> {levelName(v.level)} ))}
{ if (video.current) { video.current.requestFullscreen(); } }}>
)} {status === VideoStatus.Offline && (
)}