More subtitle progress

This commit is contained in:
WardPearce
2026-02-18 19:22:32 +13:00
parent 82522c3a65
commit 7241f170fa
4 changed files with 71 additions and 93 deletions
-18
View File
@@ -21,7 +21,6 @@
"@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",
@@ -3524,23 +3523,6 @@
"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",
-1
View File
@@ -57,7 +57,6 @@
"@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",
@@ -4,15 +4,11 @@
const captionTracks: Record<string, string> = {};
// Configurable settings for captions
let captionFontSize: string = $state('1em');
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;
}
@@ -22,23 +18,17 @@
fontSize,
color,
backgroundColor,
opacity,
borderThickness,
borderColor
opacity
}: {
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
@@ -52,18 +42,17 @@
});
}
captionsCues = (await parseText(await resp.text(), { strict: false })).cues;
captionsCues = (await parseText(await resp.text(), { strict: true, type: 'vtt' })).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 { decodeHtmlCharCodes, findElementForTime, getPublicEnv } from '$lib/misc';
import { invidiousInstanceStore } from '$lib/store';
import { onMount } from 'svelte';
import { onDestroy, onMount } from 'svelte';
import { addToast } from '../Toast.svelte';
import { parseText, VTTCue } from 'media-captions';
import { parseText, type VTTCue } from 'media-captions';
let {
video,
@@ -75,7 +64,31 @@
showControls: boolean;
} = $props();
let captionElement: HTMLElement | undefined = $state();
let captionContainerHeight: number = $state(0);
let currentCaption: VTTCue | null = $state(null);
function updateCaptionHeight() {
if (captionElement) {
captionContainerHeight = captionElement.offsetHeight;
}
}
$effect(() => {
updateCaptionHeight();
currentCaption = findElementForTime(
captionsCues,
currentTime,
(cue: VTTCue) => cue.startTime,
(cue: VTTCue) => cue.endTime
);
});
onMount(async () => {
window.addEventListener('resize', updateCaptionHeight);
if (video.captions) {
for (const caption of video.captions) {
let captionUrl: string;
@@ -92,39 +105,49 @@
}
});
function removeTags(input: string): string {
return input.replace(/<[^>]+>/g, '').replace(/&[a-zA-Z0-9#]+;/g, '');
}
onDestroy(() => {
window.removeEventListener('resize', updateCaptionHeight);
});
</script>
{#if trackVisible && captionsCues.length > 0}
{@const currentCaption = findElementForTime(
captionsCues,
currentTime,
(cue: VTTCue) => cue.startTime,
(cue: VTTCue) => cue.endTime
)}
<div
{@attach draggable(() => [bounds(BoundsFrom.viewport()), 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 currentCaption && currentTime <= currentCaption.endTime && currentTime >= currentCaption.startTime}
<div
class="caption-container"
bind:this={captionElement}
style:top={`calc(${showControls ? 'var(--video-player-height) * 0.85' : 'var(--video-player-height) * 0.98'} - ${captionContainerHeight}px)`}
>
{#if currentCaption?.text}
<p
style="
font-size: {captionFontSize};
color: {captionColor};
background-color: {captionBackgroundColor};
opacity: {captionOpacity};
padding: 5px;
border-radius: 0.25rem;
user-select: none;
"
>
{decodeHtmlCharCodes(currentCaption.text)}
</p>
{/if}
</div>
{/if}
{/if}
<style>
.caption-container {
position: absolute;
z-index: 5;
left: 50%;
transform: translateX(-50%);
width: fit-content;
}
@media screen and (max-width: 1000px) {
.caption-container {
width: 100%;
}
}
</style>
@@ -17,7 +17,6 @@
import type { VideoPlay } from '$lib/api/model';
import {
invidiousAuthStore,
invidiousInstanceStore,
isAndroidTvStore,
playerAlwaysLoopStore,
playerAndroidLockOrientation,
@@ -458,31 +457,6 @@
const error = (event as CustomEvent).detail as shaka.util.Error;
console.error('Player error:', error);
});
player.getNetworkingEngine()?.registerResponseFilter((type, response) => {
if (
type !== shaka.net.NetworkingEngine.RequestType.SEGMENT ||
!response.uri.includes('/api/timedtext')
) {
return;
}
const url = new URL(response.uri);
// Fix positioning for auto-generated subtitles
// Credit to Freetube!
if (
url.hostname.endsWith('.youtube.com') &&
url.pathname === '/api/timedtext' &&
url.searchParams.get('caps') === 'asr' &&
url.searchParams.get('kind') === 'asr' &&
url.searchParams.get('fmt') === 'vtt'
) {
const stringBody = new TextDecoder().decode(response.data);
// position:0% for LTR text and position:100% for RTL text
const cleaned = stringBody.replaceAll(/ align:start position:(?:10)?0%$/gm, '');
response.data = new TextEncoder().encode(cleaned).buffer;
}
});
playerElement?.addEventListener('volumechange', saveVolumePreference);