diff --git a/components/player/AspectOverlay.tsx b/components/player/AspectOverlay.tsx new file mode 100644 index 0000000..729fee2 --- /dev/null +++ b/components/player/AspectOverlay.tsx @@ -0,0 +1,131 @@ +"use client"; + +import { useEffect, useState, type RefObject } from "react"; +import { + blankingInsets, + findAspectPreset, + isFullFrame, + letterboxRect, + type BlankingMode, +} from "@/lib/aspect-overlays"; + +interface AspectOverlayProps { + /** Preset id, or "off" — anything unknown draws nothing. */ + presetId: string; + mode: BlankingMode; + videoRef: RefObject; +} + +/** + * Delivery blanking drawn over the video. Purely visual: it never touches the + * video element, sits under the annotation canvas, and passes every pointer + * event through so click-to-play still works underneath it. + */ +export function AspectOverlay({ presetId, mode, videoRef }: AspectOverlayProps) { + const preset = findAspectPreset(presetId); + const [box, setBox] = useState<{ + top: number; + bottom: number; + left: number; + right: number; + rect: { left: number; top: number; width: number; height: number }; + } | null>(null); + + useEffect(() => { + const video = videoRef.current; + if (!video || !preset) { + setBox(null); + return; + } + + const measure = () => { + const rect = letterboxRect( + video.clientWidth, + video.clientHeight, + video.videoWidth, + video.videoHeight + ); + // Before metadata lands videoWidth is 0 and there is nothing to measure. + if (!rect) { + setBox(null); + return; + } + const insets = blankingInsets(rect, preset.ratio); + setBox(isFullFrame(insets) ? null : { ...insets, rect }); + }; + + measure(); + + // The picture rect moves on layout changes (panel resize, fullscreen) and + // on metadata, which is when intrinsic dimensions first exist. + const observer = new ResizeObserver(measure); + observer.observe(video); + video.addEventListener("loadedmetadata", measure); + + return () => { + observer.disconnect(); + video.removeEventListener("loadedmetadata", measure); + }; + }, [preset, videoRef]); + + if (!preset || !box) return null; + + const { rect } = box; + const barClass = + mode === "solid" + ? "absolute bg-black" + : "absolute bg-black/60 ring-1 ring-inset ring-white/25"; + + // Each bar is positioned against the picture rect, not the element box, so + // blanking lands on the image itself whatever shape the player is. + const horizontal = box.top > 0; + + return ( + // No z-index: the annotation canvas is also absolutely positioned and comes + // later in the DOM, so it stays on top and marks drawn over a blanked area + // remain visible. +