6 changed files with 62 additions and 142 deletions
@ -1,109 +0,0 @@
@@ -1,109 +0,0 @@
|
||||
import { ReactElement, useEffect, useRef, useState } from "react"; |
||||
import Hls from "hls.js"; |
||||
import { IconPatch } from "@/components/buttons/IconPatch"; |
||||
import { Icons } from "@/components/Icon"; |
||||
import { Loading } from "@/components/layout/Loading"; |
||||
import { MWMediaCaption, MWMediaStream } from "@/providers"; |
||||
|
||||
export interface VideoPlayerProps { |
||||
source: MWMediaStream; |
||||
captions: MWMediaCaption[]; |
||||
startAt?: number; |
||||
onProgress?: (event: ProgressEvent) => void; |
||||
} |
||||
|
||||
export function SkeletonVideoPlayer(props: { error?: boolean }) { |
||||
return ( |
||||
<div className="flex aspect-video w-full items-center justify-center bg-denim-200 lg:rounded-xl"> |
||||
{props.error ? ( |
||||
<div className="flex flex-col items-center"> |
||||
<IconPatch icon={Icons.WARNING} className="text-red-400" /> |
||||
<p className="mt-5 text-white">Couldn't get your stream</p> |
||||
</div> |
||||
) : ( |
||||
<div className="flex flex-col items-center"> |
||||
<Loading /> |
||||
<p className="mt-3 text-white">Getting your stream...</p> |
||||
</div> |
||||
)} |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
export function VideoPlayer(props: VideoPlayerProps) { |
||||
const videoRef = useRef<HTMLVideoElement | null>(null); |
||||
const [hasErrored, setErrored] = useState(false); |
||||
const [isLoading, setLoading] = useState(true); |
||||
const showVideo = !isLoading && !hasErrored; |
||||
const mustUseHls = props.source.type === "m3u8"; |
||||
|
||||
// reset if stream url changes
|
||||
useEffect(() => { |
||||
setLoading(true); |
||||
setErrored(false); |
||||
|
||||
// hls support
|
||||
if (mustUseHls) { |
||||
if (!videoRef.current) return; |
||||
|
||||
if (!Hls.isSupported()) { |
||||
setLoading(false); |
||||
setErrored(true); |
||||
return; |
||||
} |
||||
|
||||
const hls = new Hls(); |
||||
|
||||
if (videoRef.current.canPlayType("application/vnd.apple.mpegurl")) { |
||||
videoRef.current.src = props.source.url; |
||||
return; |
||||
} |
||||
|
||||
hls.attachMedia(videoRef.current); |
||||
hls.loadSource(props.source.url); |
||||
|
||||
hls.on(Hls.Events.ERROR, (event, data) => { |
||||
setErrored(true); |
||||
console.error(data); |
||||
}); |
||||
} |
||||
}, [props.source.url, videoRef, mustUseHls]); |
||||
|
||||
let skeletonUi: null | ReactElement = null; |
||||
if (hasErrored) { |
||||
skeletonUi = <SkeletonVideoPlayer error />; |
||||
} else if (isLoading) { |
||||
skeletonUi = <SkeletonVideoPlayer />; |
||||
} |
||||
|
||||
return ( |
||||
<> |
||||
{skeletonUi} |
||||
<video |
||||
className={`w-full rounded-xl bg-black ${!showVideo ? "hidden" : ""}`} |
||||
ref={videoRef} |
||||
onProgress={(e) => |
||||
props.onProgress && props.onProgress(e.nativeEvent as ProgressEvent) |
||||
} |
||||
onLoadedData={(e) => { |
||||
setLoading(false); |
||||
if (props.startAt) |
||||
(e.target as HTMLVideoElement).currentTime = props.startAt; |
||||
}} |
||||
onError={(e) => { |
||||
console.error("failed to playback stream", e); |
||||
setErrored(true); |
||||
}} |
||||
controls |
||||
autoPlay |
||||
> |
||||
{!mustUseHls ? ( |
||||
<source src={props.source.url} type="video/mp4" /> |
||||
) : null} |
||||
{props.captions.map((v) => ( |
||||
<track key={v.id} kind="captions" label={v.label} src={v.url} /> |
||||
))} |
||||
</video> |
||||
</> |
||||
); |
||||
} |
Loading…
Reference in new issue