feat: custom player controls

closes #34
This commit is contained in:
2023-12-05 11:10:57 +00:00
parent 348759f652
commit 7c321c70a0
8 changed files with 141 additions and 81 deletions

View File

@ -1,6 +1,10 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import Hls from "hls.js";
import { useEffect, useMemo, useRef, useState } from "react";
import { WISH } from "@/wish";
import { FormattedMessage } from "react-intl";
import { StatePill } from "./state-pill";
import { StreamState } from "..";
import { Icon } from "./icon";
export enum VideoStatus {
Online = "online",
@ -15,9 +19,14 @@ export interface VideoPlayerProps {
export function LiveVideoPlayer(props: VideoPlayerProps) {
const video = useRef<HTMLVideoElement>(null);
const hlsObj = useRef<Hls>(null);
const streamCached = useMemo(() => props.stream, [props.stream]);
const [status, setStatus] = useState<VideoStatus>();
const [src, setSrc] = useState<string>();
const [levels, setLevels] = useState<Array<{ level: number, height: number }>>();
const [level, setLevel] = useState<number>(-1);
const [playState, setPlayState] = useState(true);
const [volume, setVolume] = useState(1);
useEffect(() => {
if (streamCached && video.current) {
@ -41,11 +50,21 @@ export function LiveVideoPlayer(props: VideoPlayerProps) {
});
hls.on(Hls.Events.MANIFEST_PARSED, () => {
setStatus(VideoStatus.Online);
setLevels(hls.levels.map((a, i) => ({
level: i,
height: a.height
})));
});
hls.on(Hls.Events.LEVEL_SWITCHING, (e, l) => {
hls.on(Hls.Events.LEVEL_SWITCHING, (_, l) => {
console.debug("HLS Level Switch", l);
});
return () => hls.destroy();
// @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);
@ -59,58 +78,91 @@ export function LiveVideoPlayer(props: VideoPlayerProps) {
}
}, [video, streamCached, props.status]);
useEffect(() => {
if (hlsObj.current) {
hlsObj.current.nextLevel = level;
}
}, [hlsObj, level]);
useEffect(() => {
if (video.current) {
video.current.onplaying = () => setPlayState(true);
video.current.onpause = () => setPlayState(false);
video.current.onvolumechange = () => setVolume(video.current?.volume ?? 1);
}
}, [video]);
useEffect(() => {
if (video.current) {
video.current.volume = volume;
}
}, [video, volume]);
function changeVolume(e: React.MouseEvent) {
if (e.currentTarget === e.target) {
const bb = (e.target as HTMLDivElement).getBoundingClientRect();
const x = e.clientX - bb.x;
const vol = Math.max(0, Math.min(1.0, x / bb.width));
setVolume(vol);
}
}
return (
<div className="video-overlay">
<div className={status}>
<div>{status}</div>
</div>
<div className="relative">
{status === VideoStatus.Online && <div className="absolute opacity-0 hover:opacity-100 transition-opacity w-full h-full z-20 bg-[#00000055]" onClick={() => {
if (video.current) {
if (playState) {
video.current.pause();
} else {
video.current.play();
}
}
}}>
<div className="absolute w-full h-full flex items-center justify-center pointer">
<Icon name={playState ? "pause" : "play"} size={80} />
</div>
<div className="absolute flex gap-1 bottom-0 w-full bg-[rgba(0,0,0,0.5)]" onClick={e => e.stopPropagation()}>
<div className="grow">
<StatePill state={StreamState.Live} />
</div>
<div className="flex gap-1 items-center">
<Icon name="volume" />
<div className="relative w-[104px] h-full border" onMouseDown={changeVolume} onMouseMove={e => {
if (e.buttons > 0) {
changeVolume(e);
}
}}>
<div className="absolute h-full w-[4px] bg-white" style={{
left: `${Math.floor(100 * volume)}px`
}}></div>
</div>
</div>
<div>
<select onChange={e => setLevel(Number(e.target.value))}>
<option value={-1}>
<FormattedMessage defaultMessage="Auto" id="NXI/XL" />
</option>
{levels?.map(v => <option value={v.level} key={v.level}>
<FormattedMessage defaultMessage="{n}p" id="YagVIe" values={{ n: v.height }} />
</option>)}
</select>
</div>
</div>
</div>}
{
status === VideoStatus.Offline && <div className="absolute w-full h-full z-20 bg-[#000000aa] flex items-center justify-center text-3xl font-bold uppercase">
<FormattedMessage defaultMessage="Offline" id="7UOvbT" />
</div>
}
<video
className="z-10"
ref={video}
autoPlay={true}
poster={props.poster}
src={src}
playsInline={true}
controls={status === VideoStatus.Online}
/>
</div>
</div >
);
}
export function WebRTCPlayer(props: VideoPlayerProps) {
const video = useRef<HTMLVideoElement>(null);
const streamCached = useMemo(
() => "https://customer-uu10flpvos4pfhgu.cloudflarestream.com/7634aee1af35a2de4ac13ca3d1718a8b/webRTC/play",
[props.stream]
);
const [status] = useState<VideoStatus>();
//https://customer-uu10flpvos4pfhgu.cloudflarestream.com/7634aee1af35a2de4ac13ca3d1718a8b/webRTC/play
useEffect(() => {
if (video.current && streamCached) {
const client = new WISH();
client.addEventListener("log", console.debug);
client.WithEndpoint(streamCached, true);
client
.Play()
.then(s => {
if (video.current) {
video.current.srcObject = s;
}
})
.catch(console.error);
return () => {
client.Disconnect().catch(console.error);
};
}
}, [video, streamCached]);
return (
<div className="video-overlay">
<div className={status}>
<div>{status}</div>
</div>
<video ref={video} autoPlay={true} poster={props.poster} controls={status === VideoStatus.Online} />
</div>
);
}
}