fine-tune caption rendering

This commit is contained in:
mrjvs 2023-03-19 19:58:30 +01:00
parent 5664540acc
commit 01f46ce23c
9 changed files with 43 additions and 85 deletions

View File

@ -70,7 +70,7 @@
"seasons": "Seasons", "seasons": "Seasons",
"captions": "Captions", "captions": "Captions",
"captionPreferences": { "captionPreferences": {
"title": "Caption Preferences", "title": "Customize",
"delay": "Delay", "delay": "Delay",
"fontSize": "Size", "fontSize": "Size",
"opacity": "Opacity", "opacity": "Opacity",

View File

@ -8,8 +8,6 @@ interface MWSettingsDataSetters {
setCaptionDelay(delay: number): void; setCaptionDelay(delay: number): void;
setCaptionColor(color: string): void; setCaptionColor(color: string): void;
setCaptionFontSize(size: number): void; setCaptionFontSize(size: number): void;
setCaptionFontFamily(fontFamily: string): void;
setCaptionTextShadow(textShadow: string): void;
setCaptionBackgroundColor(backgroundColor: string): void; setCaptionBackgroundColor(backgroundColor: string): void;
} }
type MWSettingsDataWrapper = MWSettingsData & MWSettingsDataSetters; type MWSettingsDataWrapper = MWSettingsData & MWSettingsDataSetters;
@ -50,23 +48,7 @@ export function SettingsProvider(props: { children: ReactNode }) {
setCaptionFontSize(size) { setCaptionFontSize(size) {
setSettings((oldSettings) => { setSettings((oldSettings) => {
const style = oldSettings.captionSettings.style; const style = oldSettings.captionSettings.style;
style.fontSize = enforceRange(10, size, 30); style.fontSize = enforceRange(10, size, 60);
const newSettings = oldSettings;
return newSettings;
});
},
setCaptionFontFamily(fontFamily) {
setSettings((oldSettings) => {
const captionStyle = oldSettings.captionSettings.style;
captionStyle.fontFamily = fontFamily;
const newSettings = oldSettings;
return newSettings;
});
},
setCaptionTextShadow(textShadow) {
setSettings((oldSettings) => {
const captionStyle = oldSettings.captionSettings.style;
captionStyle.textShadow = textShadow;
const newSettings = oldSettings; const newSettings = oldSettings;
return newSettings; return newSettings;
}); });

View File

@ -5,20 +5,18 @@ export const SettingsStore = createVersionedStore<MWSettingsData>()
.setKey("mw-settings") .setKey("mw-settings")
.addVersion({ .addVersion({
version: 0, version: 0,
create() { create(): MWSettingsData {
return { return {
language: "en", language: "en",
captionSettings: { captionSettings: {
delay: 0, delay: 0,
style: { style: {
color: "#ffffff", color: "#ffffff",
fontSize: 20, fontSize: 25,
fontFamily: "inherit", backgroundColor: "#00000096",
textShadow: "2px 2px 2px black",
backgroundColor: "#000000ff",
}, },
}, },
} as MWSettingsData; };
}, },
}) })
.build(); .build();

View File

@ -4,8 +4,6 @@ export interface CaptionStyleSettings {
* Range is [10, 30] * Range is [10, 30]
*/ */
fontSize: number; fontSize: number;
fontFamily: string;
textShadow: string;
backgroundColor: string; backgroundColor: string;
} }

View File

@ -1,25 +0,0 @@
import { sanitize } from "@/backend/helpers/captions";
import { useSettings } from "@/state/settings";
export function Caption({ text }: { text?: string }) {
const { captionSettings } = useSettings();
return (
<span
className="pointer-events-none mb-1 select-none px-1 text-center"
dir="auto"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: sanitize(text || "", {
// https://www.w3.org/TR/webvtt1/#dom-construction-rules
ALLOWED_TAGS: ["c", "b", "i", "u", "span", "ruby", "rt"],
ADD_TAGS: ["v", "lang"],
ALLOWED_ATTR: ["title", "lang"],
}),
}}
style={{
whiteSpace: "pre-line",
...captionSettings.style,
}}
/>
);
}

View File

@ -28,7 +28,7 @@ import { PopoutProviderAction } from "@/video/components/popouts/PopoutProviderA
import { ChromecastAction } from "@/video/components/actions/ChromecastAction"; import { ChromecastAction } from "@/video/components/actions/ChromecastAction";
import { CastingTextAction } from "@/video/components/actions/CastingTextAction"; import { CastingTextAction } from "@/video/components/actions/CastingTextAction";
import { PictureInPictureAction } from "@/video/components/actions/PictureInPictureAction"; import { PictureInPictureAction } from "@/video/components/actions/PictureInPictureAction";
import { CaptionRenderer } from "./CaptionRenderer"; import { CaptionRendererAction } from "./actions/CaptionRendererAction";
import { SettingsAction } from "./actions/SettingsAction"; import { SettingsAction } from "./actions/SettingsAction";
import { DividerAction } from "./actions/DividerAction"; import { DividerAction } from "./actions/DividerAction";
@ -166,7 +166,7 @@ export function VideoPlayer(props: Props) {
</Transition> </Transition>
{show ? <PopoutProviderAction /> : null} {show ? <PopoutProviderAction /> : null}
</BackdropAction> </BackdropAction>
<CaptionRenderer isControlsShown={show} /> <CaptionRendererAction isControlsShown={show} />
{props.children} {props.children}
</VideoPlayerError> </VideoPlayerError>
</> </>

View File

@ -1,14 +1,36 @@
import { Transition } from "@/components/Transition"; import { Transition } from "@/components/Transition";
import { useSettings } from "@/state/settings"; import { useSettings } from "@/state/settings";
import { sanitize } from "@/backend/helpers/captions";
import { parse, Cue } from "node-webvtt"; import { parse, Cue } from "node-webvtt";
import { useRef } from "react"; import { useRef } from "react";
import { useAsync } from "react-use"; import { useAsync } from "react-use";
import { useVideoPlayerDescriptor } from "../state/hooks"; import { useVideoPlayerDescriptor } from "../../state/hooks";
import { useProgress } from "../state/logic/progress"; import { useProgress } from "../../state/logic/progress";
import { useSource } from "../state/logic/source"; import { useSource } from "../../state/logic/source";
import { Caption } from "./Caption";
export function CaptionRenderer({ function CaptionCue({ text }: { text?: string }) {
const { captionSettings } = useSettings();
return (
<span
className="pointer-events-none mb-1 select-none whitespace-pre-line rounded px-4 py-1 text-center [text-shadow:0_2px_4px_rgba(0,0,0,0.5)]"
dir="auto"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: sanitize(text || "", {
// https://www.w3.org/TR/webvtt1/#dom-construction-rules
ALLOWED_TAGS: ["c", "b", "i", "u", "span", "ruby", "rt"],
ADD_TAGS: ["v", "lang"],
ALLOWED_ATTR: ["title", "lang"],
}),
}}
style={{
...captionSettings.style,
}}
/>
);
}
export function CaptionRendererAction({
isControlsShown, isControlsShown,
}: { }: {
isControlsShown: boolean; isControlsShown: boolean;
@ -44,7 +66,7 @@ export function CaptionRenderer({
return ( return (
<Transition <Transition
className={[ className={[
"absolute flex w-full flex-col items-center transition-[bottom]", "pointer-events-none absolute flex w-full flex-col items-center transition-[bottom]",
isControlsShown ? "bottom-24" : "bottom-12", isControlsShown ? "bottom-24" : "bottom-12",
].join(" ")} ].join(" ")}
animation="slide-up" animation="slide-up"
@ -53,7 +75,7 @@ export function CaptionRenderer({
{captions.current.map( {captions.current.map(
({ identifier, end, start, text }) => ({ identifier, end, start, text }) =>
isVisible(start, end) && ( isVisible(start, end) && (
<Caption key={identifier || `${start}-${end}`} text={text} /> <CaptionCue key={identifier || `${start}-${end}`} text={text} />
) )
)} )}
</Transition> </Transition>

View File

@ -44,11 +44,7 @@ function VideoElement(props: Props) {
muted={mediaPlaying.volume === 0} muted={mediaPlaying.volume === 0}
playsInline playsInline
className="h-full w-full" className="h-full w-full"
> />
{/* {source.source?.caption ? (
<track default kind="captions" src={source.source.caption.url} />
) : null} */}
</video>
); );
} }

View File

@ -14,12 +14,10 @@ export type SliderProps = {
value: number; value: number;
valueDisplay?: string; valueDisplay?: string;
onChange: ChangeEventHandler<HTMLInputElement>; onChange: ChangeEventHandler<HTMLInputElement>;
stops?: number[];
}; };
export function Slider(props: SliderProps) { export function Slider(props: SliderProps) {
const ref = useRef<HTMLInputElement>(null); const ref = useRef<HTMLInputElement>(null);
const stops = props.stops ?? [Math.floor((props.max + props.min) / 2)];
useEffect(() => { useEffect(() => {
const e = ref.current as HTMLInputElement; const e = ref.current as HTMLInputElement;
e.style.setProperty("--value", e.value); e.style.setProperty("--value", e.value);
@ -41,13 +39,7 @@ export function Slider(props: SliderProps) {
max={props.max} max={props.max}
min={props.min} min={props.min}
step={props.step} step={props.step}
list="stops"
/> />
<datalist id="stops">
{stops.map((s) => (
<option value={s} />
))}
</datalist>
</div> </div>
<div className="mt-1 aspect-[2/1] h-8 rounded-sm bg-[#1C161B] pt-1"> <div className="mt-1 aspect-[2/1] h-8 rounded-sm bg-[#1C161B] pt-1">
<div className="text-center font-bold text-white"> <div className="text-center font-bold text-white">
@ -88,13 +80,12 @@ export function CaptionSettingsPopout(props: {
valueDisplay={`${captionSettings.delay.toFixed(1)}s`} valueDisplay={`${captionSettings.delay.toFixed(1)}s`}
value={captionSettings.delay} value={captionSettings.delay}
onChange={(e) => setCaptionDelay(e.target.valueAsNumber)} onChange={(e) => setCaptionDelay(e.target.valueAsNumber)}
stops={[-5, 0, 5]}
/> />
<Slider <Slider
label="Size" label="Size"
min={10} min={14}
step={1} step={1}
max={30} max={60}
value={captionSettings.style.fontSize} value={captionSettings.style.fontSize}
onChange={(e) => setCaptionFontSize(e.target.valueAsNumber)} onChange={(e) => setCaptionFontSize(e.target.valueAsNumber)}
/> />
@ -131,20 +122,16 @@ export function CaptionSettingsPopout(props: {
<div className="flex flex-row gap-2"> <div className="flex flex-row gap-2">
{colors.map((color) => ( {colors.map((color) => (
<div <div
className={`flex h-8 w-8 items-center justify-center rounded ${ className={`flex h-8 w-8 items-center justify-center rounded transition-[background-color,transform] duration-100 hover:bg-[#1c161b79] active:scale-110 ${
color === captionSettings.style.color ? "bg-[#1C161B]" : "" color === captionSettings.style.color ? "bg-[#1C161B]" : ""
}`} }`}
onClick={() => setCaptionColor(color)}
> >
<input <div
className="h-4 w-4 cursor-pointer appearance-none rounded-full" className="h-4 w-4 cursor-pointer appearance-none rounded-full"
type="radio"
name="color"
key={color}
value={color}
style={{ style={{
backgroundColor: color, backgroundColor: color,
}} }}
onChange={(e) => setCaptionColor(e.target.value)}
/> />
<Icon <Icon
className={[ className={[