diff --git a/src/lib/components/VideoPlayer/JellyfinVideoPlayerModal.svelte b/src/lib/components/VideoPlayer/JellyfinVideoPlayerModal.svelte index 3cd3cc1..d8456c9 100644 --- a/src/lib/components/VideoPlayer/JellyfinVideoPlayerModal.svelte +++ b/src/lib/components/VideoPlayer/JellyfinVideoPlayerModal.svelte @@ -9,6 +9,14 @@ import { appState } from '../../stores/app-state.store'; import { onDestroy } from 'svelte'; import { modalStack, modalStackTop } from '../Modal/modal.store'; + import { createLocalStorageStore } from '../../stores/localstorage.store'; + import { get } from 'svelte/store'; + import { ISO_2_LANGUAGES } from '../../utils/iso-2-languages'; + + type MediaLanguageStore = { + subtitles?: string; + audio?: string; + }; export let id: string; export let modalId: symbol; @@ -41,18 +49,24 @@ id: string, options: { audioStreamIndex?: number; bitrate?: number; playbackPosition?: number } = {} ) { - const itemP = jellyfinApi.getLibraryItem(id); - const jellyfinPlaybackInfoP = itemP.then((item) => - jellyfinApi.getPlaybackInfo( - id, - getDeviceProfile(), - options.playbackPosition || item?.UserData?.PlaybackPositionTicks || 0, - options.bitrate || getQualities(item?.Height || 1080)[0]?.maxBitrate, - options.audioStreamIndex || undefined - ) + const item = await jellyfinApi.getLibraryItem(id); + + const mediaLanguagesStore = createLocalStorageStore( + 'media-tracks-' + (item?.SeriesName || id), + {} + ); + const storedAudioStreamIndex = item?.MediaStreams?.find( + (s) => s.Type === 'Audio' && s.Language === mediaLanguagesStore.get().audio + )?.Index; + const audioStreamIndex = options.audioStreamIndex ?? storedAudioStreamIndex ?? undefined; + + const jellyfinPlaybackInfo = await jellyfinApi.getPlaybackInfo( + id, + getDeviceProfile(), + options.playbackPosition || item?.UserData?.PlaybackPositionTicks || 0, + options.bitrate || getQualities(item?.Height || 1080)[0]?.maxBitrate, + audioStreamIndex ); - const item = await itemP; - const jellyfinPlaybackInfo = await jellyfinPlaybackInfoP; if (!item || !jellyfinPlaybackInfo) { console.error('No item or playback info', item, jellyfinPlaybackInfo); @@ -70,14 +84,30 @@ const mediaSource = jellyfinPlaybackInfo.MediaSources?.[0]; + const storedSubtitlesLang = mediaLanguagesStore.get().subtitles; + + if (options.audioStreamIndex) { + const audioLang = mediaSource?.MediaStreams?.[options.audioStreamIndex]?.Language; + mediaLanguagesStore.update((prev) => ({ + ...prev, + audio: audioLang || undefined + })); + } + let subtitles: Subtitles | undefined; for (const stream of mediaSource?.MediaStreams || []) { - if (stream.Type === 'Subtitle' && stream.IsDefault) { + if ( + stream.Type === 'Subtitle' && + (storedSubtitlesLang !== undefined + ? stream.Language === storedSubtitlesLang + : stream.IsDefault) + ) { subtitles = { kind: 'subtitles', srclang: stream.Language || '', url: `${$appState.user?.settings.jellyfin.baseUrl}/Videos/${id}/${mediaSource?.Id}/Subtitles/${stream.Index}/${stream.Level}/Stream.vtt`, - language: 'English' + // @ts-ignore + language: ISO_2_LANGUAGES[stream?.Language || '']?.name || 'English' }; } } @@ -90,13 +120,34 @@ language: 'English' })) || []; + const selectSubtitles = (subtitles?: Subtitles) => { + mediaLanguagesStore.update((prev) => ({ + ...prev, + subtitles: subtitles?.srclang || '' + })); + + if (subtitleInfo) { + if (subtitles) + subtitleInfo = { + ...subtitleInfo, + subtitles + }; + else + subtitleInfo = { + ...subtitleInfo, + subtitles: undefined + }; + } + }; + subtitleInfo = { subtitles, - availableSubtitles + availableSubtitles, + selectSubtitles }; playbackInfo = { - audioStreamIndex: options.audioStreamIndex || mediaSource?.DefaultAudioStreamIndex || -1, + audioStreamIndex: audioStreamIndex ?? mediaSource?.DefaultAudioStreamIndex ?? -1, audioTracks: mediaSource?.MediaStreams?.filter((s) => s.Type === 'Audio').map((s) => ({ index: s.Index || -1, diff --git a/src/lib/components/VideoPlayer/VideoElement.svelte b/src/lib/components/VideoPlayer/VideoElement.svelte index 328a040..e48d1bb 100644 --- a/src/lib/components/VideoPlayer/VideoElement.svelte +++ b/src/lib/components/VideoPlayer/VideoElement.svelte @@ -81,10 +81,17 @@ } } - $: if (subtitleInfo?.subtitles) { - console.log('Unpausing because subtitles were set'); - video.play(); - } + $: subtitleInfo && updateSubtitlesVisibility(); + const updateSubtitlesVisibility = () => { + const tracks = video?.textTracks; + for (const track of tracks) { + track.mode = track.id === subtitleInfo?.subtitles?.url ? 'showing' : 'disabled'; + } + }; + // $: if (subtitleInfo?.subtitles) { + // console.log('Unpausing because subtitles were set'); + // video.play(); + // } @@ -110,13 +117,18 @@ crossorigin="anonymous" class="w-full h-full" > - {#if subtitleInfo?.subtitles} - + {#if subtitleInfo?.availableSubtitles} + {#each subtitleInfo.availableSubtitles as subtitle} + + {/each} + + {/if} diff --git a/src/lib/components/VideoPlayer/VideoPlayer.svelte b/src/lib/components/VideoPlayer/VideoPlayer.svelte index 006f4d1..7cbd2a5 100644 --- a/src/lib/components/VideoPlayer/VideoPlayer.svelte +++ b/src/lib/components/VideoPlayer/VideoPlayer.svelte @@ -8,7 +8,7 @@ import type { Selectable } from '../../selectable'; import { modalStack } from '../Modal/modal.store'; import SelectSubtitlesModal from './SelectSubtitlesModal.svelte'; - import { ChatBubble, TextAlignLeft, Update } from 'radix-icons-svelte'; + import { ChatBubble, TextAlignLeft } from 'radix-icons-svelte'; import IconButton from './IconButton.svelte'; import SelectAudioModal from './SelectAudioModal.svelte'; import Spinner from '../Utils/Spinner.svelte'; @@ -58,16 +58,7 @@ function selectSubtitles(subtitles?: Subtitles) { if (subtitleInfo) { - if (subtitles) - subtitleInfo = { - ...subtitleInfo, - subtitles - }; - else - subtitleInfo = { - ...subtitleInfo, - subtitles: undefined - }; + subtitleInfo.selectSubtitles(subtitles); } else { console.error('No subtitle info when selecting subtitles'); } diff --git a/src/lib/components/VideoPlayer/VideoPlayer.ts b/src/lib/components/VideoPlayer/VideoPlayer.ts index afd1c35..2b9cb57 100644 --- a/src/lib/components/VideoPlayer/VideoPlayer.ts +++ b/src/lib/components/VideoPlayer/VideoPlayer.ts @@ -6,6 +6,7 @@ import VideoPlayerModal from './JellyfinVideoPlayerModal.svelte'; export type SubtitleInfo = { subtitles?: Subtitles; availableSubtitles: Subtitles[]; + selectSubtitles: (subtitles?: Subtitles) => void; }; export type Subtitles = { diff --git a/src/lib/stores/localstorage.store.ts b/src/lib/stores/localstorage.store.ts index 6e03f8d..8b3a45e 100644 --- a/src/lib/stores/localstorage.store.ts +++ b/src/lib/stores/localstorage.store.ts @@ -5,6 +5,7 @@ export function createLocalStorageStore(key: string, defaultValue: T) { return { subscribe: store.subscribe, + get: () => get(store), set: (value: T) => { localStorage.setItem(key, JSON.stringify(value)); store.set(value);