feat: player overlay styles

This commit is contained in:
2023-12-05 16:32:54 +00:00
parent 296789978c
commit 130c6048a2
19 changed files with 147 additions and 146 deletions

View File

@ -2,9 +2,10 @@
import Hls from "hls.js";
import { useEffect, useMemo, useRef, useState } from "react";
import { FormattedMessage } from "react-intl";
import { StatePill } from "./state-pill";
import { StreamState } from "..";
import { Icon } from "./icon";
import { ProgressBar } from "./progress-bar";
import { Menu, MenuItem } from "@szhsin/react-menu";
export enum VideoStatus {
Online = "online",
@ -52,12 +53,16 @@ export default function LiveVideoPlayer(props: VideoPlayerProps) {
});
hls.on(Hls.Events.MANIFEST_PARSED, () => {
setStatus(VideoStatus.Online);
setLevels(
hls.levels.map((a, i) => ({
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);
@ -106,31 +111,6 @@ export default function LiveVideoPlayer(props: VideoPlayerProps) {
}
}, [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);
}
}
function seek(e: React.MouseEvent) {
if (e.currentTarget === e.target) {
const bb = (e.target as HTMLDivElement).getBoundingClientRect();
const x = e.clientX - bb.x;
const pos = Math.max(0, Math.min(1.0, x / bb.width));
if (video.current && maxPosition) {
const ct = maxPosition * pos;
video.current.currentTime = ct;
setPosition(ct);
}
}
}
function playStateToIcon() {
switch (playState) {
case "playing":
@ -141,68 +121,87 @@ export default function LiveVideoPlayer(props: VideoPlayerProps) {
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 <FormattedMessage defaultMessage="AUTO" id="o8pHw3" />;
} else {
const h = levels?.find(a => a.level === l)?.height;
return <FormattedMessage defaultMessage="{n}p" id="YagVIe" values={{ n: h }} />;
}
}
return (
<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 === "playing") {
video.current.pause();
} else if (playState === "paused") {
video.current.play();
}
}
}}>
className="absolute opacity-0 hover:opacity-90 transition-opacity w-full h-full z-20 bg-[#00000055]"
onClick={() => togglePlay()}>
<div className="absolute w-full h-full flex items-center justify-center pointer">
<Icon
name={playStateToIcon()}
size={80}
className={playState === "loading" ? "animate-spin" : "animate-ping-once"}
/>
<Icon name={playStateToIcon()} size={80} className={playState === "loading" ? "animate-spin" : ""} />
</div>
<div className="absolute flex gap-1 bottom-0 w-full bg-[rgba(0,0,0,0.5)]" onClick={e => e.stopPropagation()}>
<div className="flex grow gap-1">
<StatePill state={props.status as StreamState} />
{props.status === StreamState.Ended && playState && maxPosition && position && (
<div className="relative w-full h-full border" onClick={seek}>
<div
className="absolute h-full w-[4px] bg-white"
style={{
width: `${((position / maxPosition) * 100).toFixed(1)}%`,
}}></div>
</div>
<div
className="absolute flex items-center gap-1 bottom-0 w-full bg-primary h-[40px]"
onClick={e => e.stopPropagation()}>
<div className="flex grow gap-1 items-center">
<div className="px-5 py-2 pointer" onClick={() => togglePlay()}>
<Icon name={playStateToIcon()} className={playState === "loading" ? "animate-spin" : ""} />
</div>
<div className="px-3 py-2 uppercase font-bold tracking-wide hover:bg-primary-hover">{props.status}</div>
{props.status === StreamState.Ended && maxPosition !== undefined && position !== undefined && (
<ProgressBar
value={position / maxPosition}
setValue={v => {
const ct = maxPosition * v;
if (video.current) {
video.current.currentTime = ct;
}
setPosition(ct);
}}
marker={<div className="w-[16px] h-[16px] mt-[-8px] rounded-full bg-white"></div>}
style={{ width: "100%", height: "4px" }}
/>
)}
</div>
<div className="flex gap-1 items-center">
<div className="flex gap-1 items-center h-full py-2">
<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>
<ProgressBar value={volume} setValue={v => setVolume(v)} style={{ width: "100px", height: "100%" }} />
</div>
<div>
<select onChange={e => setLevel(Number(e.target.value))}>
<option value={-1}>
<FormattedMessage defaultMessage="Auto" id="NXI/XL" />
</option>
<Menu
direction="top"
align="center"
menuButton={<div className="px-3 py-2 tracking-wide pointer">{levelName(level)}</div>}
menuClassName="bg-primary w-fit">
{levels?.map(v => (
<option value={v.level} key={v.level}>
<FormattedMessage defaultMessage="{n}p" id="YagVIe" values={{ n: v.height }} />
</option>
<MenuItem
value={v.level}
key={v.level}
onClick={() => setLevel(v.level)}
className="bg-primary px-3 py-2 text-white">
{levelName(v.level)}
</MenuItem>
))}
</select>
</Menu>
</div>
<div
className="px-3 py-2 pointer"
onClick={() => {
if (video.current) {
video.current.requestFullscreen();
}
}}>
<Icon name="fullscreen" size={24} />
</div>
</div>
</div>