import { useMemo } from "react"; import { captionIsVisible, makeQueId, parseSubtitles, sanitize, } from "@/components/player/utils/captions"; import { Transition } from "@/components/utils/Transition"; import { usePlayerStore } from "@/stores/player/store"; import { SubtitleStyling, useSubtitleStore } from "@/stores/subtitles"; // Clean-up function to remove unwanted subtitle tags function cleanSubtitleText(text: string): string { // Remove unwanted tags like \an1, \pos, \i1, etc. return text.replace(/\\[a-zA-Z0-9(),\s\-_]+/g, "").replace(/\}\s*/g, ""); } const wordOverrides: Record = { i: "I", }; export function CaptionCue({ text, styling, overrideCasing, }: { text?: string; styling: SubtitleStyling; overrideCasing: boolean; }) { const parsedHtml = useMemo(() => { let textToUse = text; if (overrideCasing && text) { textToUse = text.slice(0, 1) + text.slice(1).toLowerCase(); } const textWithNewlines = (textToUse || "") .split(" ") .map((word) => wordOverrides[word] ?? word) .join(" ") .replaceAll(/ i'/g, " I'") .replaceAll(/\r?\n/g, "
"); // Clean the subtitle text before sanitizing it const cleanedText = cleanSubtitleText(textWithNewlines); // https://www.w3.org/TR/webvtt1/#dom-construction-rules // added a
for newlines const html = sanitize(cleanedText, { ALLOWED_TAGS: ["c", "b", "i", "u", "span", "ruby", "rt", "br"], ADD_TAGS: ["v", "lang"], ALLOWED_ATTR: ["title", "lang"], }); return html; }, [text, overrideCasing]); const getTextEffectStyles = () => { switch (styling.fontStyle) { case "raised": return { textShadow: "0 2px 0 rgba(0,0,0,0.8), 0 1.5px 1.5px rgba(0,0,0,0.9)", }; case "depressed": return { textShadow: "0 -2px 0 rgba(0,0,0,0.8), 0 -1.5px 1.5px rgba(0,0,0,0.9)", }; case "uniform": return { textShadow: "1.5px 1.5px 1.5px rgba(0,0,0,0.8), -1.5px -1.5px 1.5px rgba(0,0,0,0.8), 1.5px -1.5px 1.5px rgba(0,0,0,0.8), -1.5px 1.5px 1.5px rgba(0,0,0,0.8)", }; case "dropShadow": return { textShadow: "2.5px 2.5px 4.5px rgba(0,0,0,0.9)" }; case "default": default: return { textShadow: "0 2px 4px rgba(0,0,0,0.5)" }; // Default is a light drop shadow } }; const textEffectStyles = getTextEffectStyles(); return (

); } export function SubtitleRenderer() { const videoTime = usePlayerStore((s) => s.progress.time); const srtData = usePlayerStore((s) => s.caption.selected?.srtData); const language = usePlayerStore((s) => s.caption.selected?.language); const styling = useSubtitleStore((s) => s.styling); const overrideCasing = useSubtitleStore((s) => s.overrideCasing); const delay = useSubtitleStore((s) => s.delay); const playbackRate = usePlayerStore((s) => s.mediaPlaying.playbackRate); const parsedCaptions = useMemo( () => (srtData ? parseSubtitles(srtData, language) : []), [srtData, language], ); const visibileCaptions = useMemo( () => parsedCaptions.filter(({ start, end }) => captionIsVisible(start, end, delay, videoTime, playbackRate), ), [parsedCaptions, videoTime, delay, playbackRate], ); return (
{visibileCaptions.map(({ start, end, content }, i) => ( ))}
); } export function SubtitleView(props: { controlsShown: boolean }) { const caption = usePlayerStore((s) => s.caption.selected); const captionAsTrack = usePlayerStore((s) => s.caption.asTrack); const display = usePlayerStore((s) => s.display); const isCasting = display?.getType() === "casting"; const styling = useSubtitleStore((s) => s.styling); if (captionAsTrack || !caption || isCasting) return null; return (
); }