Started own captions

This commit is contained in:
WardPearce
2026-02-18 18:14:06 +13:00
parent 8e0a9a0145
commit 6fce303904
8 changed files with 172 additions and 46 deletions
+20 -2
View File
@@ -1,12 +1,12 @@
{
"name": "materialious",
"version": "1.16.1",
"version": "1.16.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "materialious",
"version": "1.16.1",
"version": "1.16.2",
"hasInstallScript": true,
"dependencies": {
"@capacitor-community/electron": "^5.0.1",
@@ -21,6 +21,7 @@
"@capacitor/share": "^8.0.0",
"@macfja/serializer": "^1.1.4",
"@macfja/svelte-persistent-store": "^2.4.2",
"@neodrag/svelte": "^3.0.0-next.8",
"altcha": "^2.3.0",
"altcha-lib": "^1.4.1",
"beercss": "^4.0.2",
@@ -3523,6 +3524,23 @@
"node": ">= 10"
}
},
"node_modules/@neodrag/core": {
"version": "3.0.0-next.8",
"resolved": "https://registry.npmjs.org/@neodrag/core/-/core-3.0.0-next.8.tgz",
"integrity": "sha512-RNFwbo2w6M7BEzCh7pls68X2h3n+ofFDW1XnGyIzhtJ2bb8J/2VqixOpOlw9NfSJ61xgUC5ymGQbQFg5BKsWBA==",
"license": "MIT",
"peer": true
},
"node_modules/@neodrag/svelte": {
"version": "3.0.0-next.8",
"resolved": "https://registry.npmjs.org/@neodrag/svelte/-/svelte-3.0.0-next.8.tgz",
"integrity": "sha512-VkTnOqiHpAaEK5jp2PqQPEEx5AaNGVoKeoC2TuWd2xuteGOVq7NqQjNC4sxk0HAVysfYOC9YhYOsVyRiKo2LFw==",
"license": "MIT",
"peerDependencies": {
"@neodrag/core": "3.0.0-next.8",
"svelte": "^3 || ^4 || ^5"
}
},
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
+2 -1
View File
@@ -57,6 +57,7 @@
"@capacitor/share": "^8.0.0",
"@macfja/serializer": "^1.1.4",
"@macfja/svelte-persistent-store": "^2.4.2",
"@neodrag/svelte": "^3.0.0-next.8",
"altcha": "^2.3.0",
"altcha-lib": "^1.4.1",
"beercss": "^4.0.2",
@@ -96,4 +97,4 @@
"youtubei.js": "^16.0.1",
"zod": "^4.3.6"
}
}
}
@@ -0,0 +1,135 @@
<script lang="ts" module>
let trackVisible: boolean = $state(false);
let captionsCues: VTTCue[] = $state([]);
const captionTracks: Record<string, string> = {};
// Configurable settings for captions
let captionFontSize: string = $state('16px');
let captionColor: string = $state('white');
let captionBackgroundColor: string = $state('rgba(0, 0, 0, 0.7)');
let captionOpacity: number = $state(1);
let captionBorderThickness: string = $state('0.1px');
let captionBorderColor: string = $state('black');
export function setTextTrackVisibility(visible: boolean = true) {
trackVisible = visible;
}
// Function to update caption style settings
export function updateCaptionStyles({
fontSize,
color,
backgroundColor,
opacity,
borderThickness,
borderColor
}: {
fontSize?: string;
color?: string;
backgroundColor?: string;
opacity?: number;
borderThickness?: string;
borderColor?: string;
}) {
if (fontSize) captionFontSize = fontSize;
if (color) captionColor = color;
if (backgroundColor) captionBackgroundColor = backgroundColor;
if (opacity !== undefined) captionOpacity = opacity;
if (borderThickness) captionBorderThickness = borderThickness;
if (borderColor) captionBorderColor = borderColor;
}
// Fetch caption data for a selected language
export async function selectTextTrack(language: string) {
const resp = await fetch(captionTracks[language], { method: 'GET' });
if (!resp.ok) {
addToast({
data: {
text: 'Unable to fetch captions'
}
});
}
captionsCues = (await parseText(await resp.text(), { strict: false })).cues;
}
</script>
<script lang="ts">
import { draggable, bounds, BoundsFrom, touchAction } from '@neodrag/svelte';
import type { VideoPlay } from '$lib/api/model';
import { findElementForTime, getPublicEnv } from '$lib/misc';
import { invidiousInstanceStore } from '$lib/store';
import { onMount } from 'svelte';
import { addToast } from '../Toast.svelte';
import { parseText, VTTCue } from 'media-captions';
let {
video,
playerContainer,
currentTime = $bindable(),
showControls = $bindable()
}: {
video: VideoPlay;
currentTime: number;
showControls: boolean;
playerContainer: HTMLElement;
} = $props();
onMount(async () => {
if (video.captions) {
for (const caption of video.captions) {
let captionUrl: string;
if (!getPublicEnv('DEFAULT_COMPANION_INSTANCE') && $invidiousInstanceStore) {
captionUrl = caption.url.startsWith('http')
? caption.url
: `${new URL($invidiousInstanceStore).origin}${caption.url}`;
} else {
captionUrl = `${getPublicEnv('DEFAULT_COMPANION_INSTANCE')}${caption.url}`;
}
captionTracks[caption.language_code] = captionUrl;
}
}
});
function removeTags(input: string): string {
return input.replace(/<[^>]+>/g, '').replace(/&[a-zA-Z0-9#]+;/g, '');
}
</script>
{#if trackVisible && captionsCues.length > 0}
{@const currentCaption = findElementForTime(
captionsCues,
currentTime,
(cue: VTTCue) => cue.startTime,
(cue: VTTCue) => cue.endTime
)}
<div
{@attach draggable(() => [
bounds(BoundsFrom.element(playerContainer)),
touchAction('manipulation')
])}
style="z-index: 5;cursor: grab;"
>
{#if currentCaption?.text}
<p
style="
font-size: {captionFontSize};
color: {captionColor};
background-color: {captionBackgroundColor};
opacity: {captionOpacity};
padding: 5px;
width: fit-content;
border-radius: 0.25rem;
user-select: none;
-webkit-text-fill-color: {captionColor};
-webkit-text-stroke: {captionBorderThickness} {captionBorderColor};
"
>
{removeTags(currentCaption.text)}
</p>
{/if}
</div>
{/if}
@@ -65,6 +65,7 @@
import TouchControls from './TouchControls.svelte';
import { Network, type ConnectionStatus } from '@capacitor/network';
import { ScreenOrientation, type ScreenOrientationResult } from '@capacitor/screen-orientation';
import ClosedCaptions from './ClosedCaptions.svelte';
interface Props {
data: { video: VideoPlay; content: ParsedDescription; playlistId: string | null };
@@ -286,28 +287,6 @@
await player.load(dashUrl, await getLastPlayPos());
}
if (data.video.captions) {
for (const caption of data.video.captions) {
let captionUrl: string;
if (!getPublicEnv('DEFAULT_COMPANION_INSTANCE') && $invidiousInstanceStore) {
captionUrl = caption.url.startsWith('http')
? caption.url
: `${new URL($invidiousInstanceStore).origin}${caption.url}`;
} else {
captionUrl = `${getPublicEnv('DEFAULT_COMPANION_INSTANCE')}${caption.url}`;
}
await player.addTextTrackAsync(
captionUrl,
caption.language_code,
'captions',
undefined,
undefined,
caption.label
);
}
}
if (data.content.timestamps) {
try {
player.addChaptersTrack(
@@ -894,6 +873,8 @@
}}
bind:this={playerContainer}
>
<ClosedCaptions video={data.video} {playerContainer} bind:currentTime bind:showControls />
<video
controls={false}
autoplay={$playerAutoPlayStore}
@@ -905,6 +886,7 @@
{data.video.title}
</div>
{/if}
{#if showControls}
<div id="mobile-time" transition:fade>
<p class="chip surface-container-highest s">
@@ -974,7 +956,7 @@
{/if}
</p>
{#if !$isAndroidTvStore}
<CaptionSettings {player} video={data.video} />
<CaptionSettings video={data.video} />
{#if playerElement}
<Settings {player} {playerElement} />
<Airplay {playerElement} />
@@ -1,36 +1,25 @@
<script lang="ts">
import type shaka from 'shaka-player/dist/shaka-player.ui';
import { onMount } from 'svelte';
import { _ } from '$lib/i18n';
import type { VideoPlay } from '$lib/api/model';
import { selectTextTrack, setTextTrackVisibility } from '../ClosedCaptions.svelte';
let { player, video }: { player: shaka.Player; video: VideoPlay } = $props();
let playerTextTracks: shaka.extern.TextTrack[] | undefined = $state(undefined);
onMount(() => {
playerTextTracks = player.getTextTracks();
});
let { video }: { video: VideoPlay } = $props();
</script>
{#if playerTextTracks && playerTextTracks.length > 0 && !video.liveNow}
{#if video.captions.length > 0 && !video.liveNow}
<button class="surface-container-highest">
<i>closed_caption</i>
<menu class="no-wrap mobile player-settings" id="cc-menu" data-ui="#cc-menu">
<li
role="presentation"
data-ui="#cc-menu"
onclick={() => player.setTextTrackVisibility(false)}
>
<li role="presentation" data-ui="#cc-menu" onclick={() => setTextTrackVisibility(false)}>
{$_('player.controls.off')}
</li>
{#each playerTextTracks as track (track)}
{#each video.captions as track (track)}
<li
role="presentation"
data-ui="#cc-menu"
onclick={() => {
player.selectTextTrack(track);
player.setTextTrackVisibility(true);
selectTextTrack(track.language_code);
setTextTrackVisibility(true);
}}
>
{track.label}
@@ -10,7 +10,7 @@
let { player, playerElement }: { player: shaka.Player; playerElement: HTMLMediaElement } =
$props();
let playerSettings: 'quality' | 'speed' | 'language' | 'root' = $state('root');
let playerSettings: 'quality' | 'speed' | 'language' | 'caption' | 'root' = $state('root');
let playerCurrentVideoTrack: shaka.extern.VideoTrack | undefined = $state(undefined);
let playerCurrentAudioTrack: shaka.extern.AudioTrack | undefined = $state(undefined);
let playerLoop = $state($playerAlwaysLoopStore);
@@ -508,7 +508,7 @@
@media screen and (max-width: 640px) {
.color-picker {
--picker-width: 85vw;
--picker-width: 95vw;
}
}
</style>
@@ -10,6 +10,7 @@
"login": "Login",
"username": "Username",
"password": "Password",
"captionStyle": "Caption styles",
"invalidPassword": "Invalid password",
"usernameTaken": "Username taken",
"linkInvidious": "Link Invidious account",