Started own captions
This commit is contained in:
Generated
+20
-2
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user