import {
ClosedCaptioningIcon,
CornersOutIcon,
PauseIcon,
PlayIcon,
SpeakerHighIcon,
SpeakerXIcon,
} from "@phosphor-icons/react";
import { useEffect, useRef, useState, type CSSProperties, type KeyboardEvent, type PointerEvent } from "react";
import { useHlsSource, useScrubManifest } from "~/hooks/use-video";
import { scrubFrameAt, videoUrl, type ScrubManifest } from "~/lib/video";
import { cn } from "~/utils/cn";
type Caption = { src: string; srcLang: string; label: string };
type Props = { id: string; version: string; title: string; captions?: Caption[]; className?: string };
const SEEK_STEP_SECONDS = 5;
const THUMB_WIDTH = 160;
function clock(seconds: number): string {
if (!Number.isFinite(seconds)) return "0:00";
const whole = Math.floor(seconds);
return `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, "0")}`;
}
function spriteStyle(manifest: ScrubManifest, id: string, version: string, time: number): CSSProperties {
const frame = scrubFrameAt(manifest, time);
const column = frame.x / manifest.frameWidth;
const row = frame.y / manifest.frameHeight;
const percent = (index: number, count: number) => (count > 1 ? (index / (count - 1)) * 100 : 0);
return {
backgroundImage: `url(${videoUrl(id, version, frame.sheet)})`,
backgroundSize: `${manifest.columns * 100}% ${manifest.rows * 100}%`,
backgroundPosition: `${percent(column, manifest.columns)}% ${percent(row, manifest.rows)}%`,
};
}
/** HLS player served from R2 via /videos; dragging the timeline shows sprite frames instantly and seeks on release. */
export function VideoPlayer({ id, version, title, captions = [], className }: Props) {
const [video, setVideo] = useState<HTMLVideoElement | null>(null);