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",
"captions": "Captions",
"captionPreferences": {
"title": "Caption Preferences",
"title": "Customize",
"delay": "Delay",
"fontSize": "Size",
"opacity": "Opacity",

View File

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

View File

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

View File

@ -4,8 +4,6 @@ export interface CaptionStyleSettings {
* Range is [10, 30]
*/
fontSize: number;
fontFamily: string;
textShadow: 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 { CastingTextAction } from "@/video/components/actions/CastingTextAction";
import { PictureInPictureAction } from "@/video/components/actions/PictureInPictureAction";
import { CaptionRenderer } from "./CaptionRenderer";
import { CaptionRendererAction } from "./actions/CaptionRendererAction";
import { SettingsAction } from "./actions/SettingsAction";
import { DividerAction } from "./actions/DividerAction";
@ -166,7 +166,7 @@ export function VideoPlayer(props: Props) {
</Transition>
{show ? <PopoutProviderAction /> : null}
</BackdropAction>
<CaptionRenderer isControlsShown={show} />
<CaptionRendererAction isControlsShown={show} />
{props.children}
</VideoPlayerError>
</>

View File

@ -1,14 +1,36 @@
import { Transition } from "@/components/Transition";
import { useSettings } from "@/state/settings";
import { sanitize } from "@/backend/helpers/captions";
import { parse, Cue } from "node-webvtt";
import { useRef } from "react";
import { useAsync } from "react-use";
import { useVideoPlayerDescriptor } from "../state/hooks";
import { useProgress } from "../state/logic/progress";
import { useSource } from "../state/logic/source";
import { Caption } from "./Caption";
import { useVideoPlayerDescriptor } from "../../state/hooks";
import { useProgress } from "../../state/logic/progress";
import { useSource } from "../../state/logic/source";
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: boolean;
@ -44,7 +66,7 @@ export function CaptionRenderer({
return (
<Transition
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",
].join(" ")}
animation="slide-up"
@ -53,7 +75,7 @@ export function CaptionRenderer({
{captions.current.map(
({ identifier, end, start, text }) =>
isVisible(start, end) && (
<Caption key={identifier || `${start}-${end}`} text={text} />
<CaptionCue key={identifier || `${start}-${end}`} text={text} />
)
)}
</Transition>

View File

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