diff --git a/src/app.css b/src/app.css index 0ef35a3..18c8c1b 100644 --- a/src/app.css +++ b/src/app.css @@ -84,6 +84,12 @@ html[data-useragent*="Tizen"] .selectable-secondary { @apply focus-within:outline outline-2 outline-[#f0cd6dc2] outline-offset-2; } + + +.header2 { + @apply font-semibold text-2xl text-secondary-100; +} + @media tv { html { font-size: 24px; diff --git a/src/lib/apis/jellyfin/jellyfin-api.ts b/src/lib/apis/jellyfin/jellyfin-api.ts index 2e5d75b..61b9892 100644 --- a/src/lib/apis/jellyfin/jellyfin-api.ts +++ b/src/lib/apis/jellyfin/jellyfin-api.ts @@ -140,7 +140,8 @@ export class JellyfinApi implements Api { itemId: string, playbackProfile: DeviceProfile, startTimeTicks = 0, - maxStreamingBitrate = 140000000 + maxStreamingBitrate = 140000000, + audioStreamIndex?: number ) => this.getClient() ?.POST('/Items/{itemId}/PlaybackInfo', { @@ -152,7 +153,8 @@ export class JellyfinApi implements Api { userId: this.getUserId(), startTimeTicks, autoOpenLiveStream: true, - maxStreamingBitrate + maxStreamingBitrate, + ...(audioStreamIndex ? { audioStreamIndex } : {}) } }, body: { @@ -160,6 +162,7 @@ export class JellyfinApi implements Api { } }) .then((r) => ({ + ...r.data, playbackUri: r.data?.MediaSources?.[0]?.TranscodingUrl || `/Videos/${r.data?.MediaSources?.[0]?.Id}/stream.mp4?Static=true&mediaSourceId=${ diff --git a/src/lib/components/Dialog/ConfirmDialog.svelte b/src/lib/components/Dialog/ConfirmDialog.svelte index 34b407d..894287a 100644 --- a/src/lib/components/Dialog/ConfirmDialog.svelte +++ b/src/lib/components/Dialog/ConfirmDialog.svelte @@ -3,6 +3,7 @@ import Button from '../Button.svelte'; import Modal from '../Modal/Modal.svelte'; import { modalStack } from '../Modal/modal.store'; + import Dialog from './Dialog.svelte'; export let modalId: symbol; @@ -26,28 +27,24 @@ } - -
-
-
- -
-
- -
- - - - -
+ +
+
- +
+ +
+ + + + +
diff --git a/src/lib/components/Dialog/Dialog.svelte b/src/lib/components/Dialog/Dialog.svelte new file mode 100644 index 0000000..33214f1 --- /dev/null +++ b/src/lib/components/Dialog/Dialog.svelte @@ -0,0 +1,13 @@ + + + +
+
+ +
+
+
diff --git a/src/lib/components/Modal/Modal.svelte b/src/lib/components/Modal/Modal.svelte index cf6a7fc..8c7c148 100644 --- a/src/lib/components/Modal/Modal.svelte +++ b/src/lib/components/Modal/Modal.svelte @@ -1,18 +1,7 @@ - - diff --git a/src/lib/components/SeriesPage/EpisodeGrid.svelte b/src/lib/components/SeriesPage/EpisodeGrid.svelte index 76381f7..b3c2fc6 100644 --- a/src/lib/components/SeriesPage/EpisodeGrid.svelte +++ b/src/lib/components/SeriesPage/EpisodeGrid.svelte @@ -61,12 +61,10 @@ function handleMountCard(s: Selectable, episode: TmdbEpisode) { currentJellyfinEpisode.then((currentEpisode) => { - console.log('currentEpisode', currentEpisode, episode); if ( currentEpisode?.IndexNumber === episode.episode_number && currentEpisode?.ParentIndexNumber === episode.season_number ) { - console.log('MATCHED', currentEpisode, episode); s.focus({ setFocusedElement: false, propagate: false }); } }); diff --git a/src/lib/components/VideoPlayer/IconButton.svelte b/src/lib/components/VideoPlayer/IconButton.svelte new file mode 100644 index 0000000..55e41db --- /dev/null +++ b/src/lib/components/VideoPlayer/IconButton.svelte @@ -0,0 +1,17 @@ + + + +
+ +
+
diff --git a/src/lib/components/VideoPlayer/JellyfinVideoPlayerModal.svelte b/src/lib/components/VideoPlayer/JellyfinVideoPlayerModal.svelte index d0db93b..3cd3cc1 100644 --- a/src/lib/components/VideoPlayer/JellyfinVideoPlayerModal.svelte +++ b/src/lib/components/VideoPlayer/JellyfinVideoPlayerModal.svelte @@ -2,13 +2,13 @@ import classNames from 'classnames'; import Container from '../../../Container.svelte'; import VideoPlayer from './VideoPlayer.svelte'; - import type { PlaybackInfo } from './VideoPlayer'; + import type { PlaybackInfo, Subtitles, SubtitleInfo, AudioTrack } from './VideoPlayer'; import { jellyfinApi } from '../../apis/jellyfin/jellyfin-api'; import getDeviceProfile from '../../apis/jellyfin/playback-profiles'; import { getQualities } from '../../apis/jellyfin/qualities'; import { appState } from '../../stores/app-state.store'; import { onDestroy } from 'svelte'; - import { modalStack } from '../Modal/modal.store'; + import { modalStack, modalStackTop } from '../Modal/modal.store'; export let id: string; export let modalId: symbol; @@ -19,6 +19,7 @@ let progressTime: number; let playbackInfo: PlaybackInfo | undefined; + let subtitleInfo: SubtitleInfo | undefined; let sessionId: string | undefined; let reportProgressInterval: ReturnType; @@ -36,14 +37,18 @@ deleteEncoding(sessionId); }; - async function loadPlaybackInfo(id: string, bitrate?: number) { + async function loadPlaybackInfo( + id: string, + options: { audioStreamIndex?: number; bitrate?: number; playbackPosition?: number } = {} + ) { const itemP = jellyfinApi.getLibraryItem(id); const jellyfinPlaybackInfoP = itemP.then((item) => jellyfinApi.getPlaybackInfo( id, getDeviceProfile(), - item?.UserData?.PlaybackPositionTicks || 0, - bitrate || getQualities(item?.Height || 1080)[0]?.maxBitrate + options.playbackPosition || item?.UserData?.PlaybackPositionTicks || 0, + options.bitrate || getQualities(item?.Height || 1080)[0]?.maxBitrate, + options.audioStreamIndex || undefined ) ); const item = await itemP; @@ -63,15 +68,55 @@ sessionId = playSessionId; + const mediaSource = jellyfinPlaybackInfo.MediaSources?.[0]; + + let subtitles: Subtitles | undefined; + for (const stream of mediaSource?.MediaStreams || []) { + if (stream.Type === 'Subtitle' && 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' + }; + } + } + + const availableSubtitles = + mediaSource?.MediaStreams?.filter((s) => s.Type === 'Subtitle').map((s) => ({ + kind: 'subtitles' as const, + srclang: s.Language || '', + url: `${$appState.user?.settings.jellyfin.baseUrl}/Videos/${id}/${mediaSource?.Id}/Subtitles/${s.Index}/${s.Level}/Stream.vtt`, + language: 'English' + })) || []; + + subtitleInfo = { + subtitles, + availableSubtitles + }; + playbackInfo = { + audioStreamIndex: options.audioStreamIndex || mediaSource?.DefaultAudioStreamIndex || -1, + audioTracks: + mediaSource?.MediaStreams?.filter((s) => s.Type === 'Audio').map((s) => ({ + index: s.Index || -1, + language: s.Language || '' + })) || [], + selectAudioTrack: (index: number) => + loadPlaybackInfo(id, { + ...options, + audioStreamIndex: index, + playbackPosition: progressTime * 10_000_000 + }), directPlay, playbackUrl: $appState.user?.settings.jellyfin.baseUrl + playbackUri, backdrop: item?.BackdropImageTags?.length ? `${$appState.user?.settings.jellyfin.baseUrl}/Items/${item?.Id}/Images/Backdrop?quality=100&tag=${item?.BackdropImageTags?.[0]}` : '', - startTime: item?.UserData?.PlaybackPositionTicks - ? item?.UserData?.PlaybackPositionTicks / 10_000_000 - : undefined + startTime: + (options.playbackPosition || 0) / 10_000_000 || + (item?.UserData?.PlaybackPositionTicks || 0) / 10_000_000 || + undefined }; if (mediaSourceId) reportPlaybackStarted(id, sessionId, mediaSourceId); @@ -98,5 +143,12 @@ 'opacity-0': hidden })} > - + diff --git a/src/lib/components/VideoPlayer/ProgressBar.svelte b/src/lib/components/VideoPlayer/ProgressBar.svelte index 31e7a97..0d67e23 100644 --- a/src/lib/components/VideoPlayer/ProgressBar.svelte +++ b/src/lib/components/VideoPlayer/ProgressBar.svelte @@ -46,7 +46,7 @@
+ import type { AudioTrack, SubtitleInfo, Subtitles } from './VideoPlayer'; + import Button from '../Button.svelte'; + import { modalStack } from '../Modal/modal.store.js'; + import { scrollIntoView } from '../../selectable'; + import { ChatBubble, Check, TextAlignLeft } from 'radix-icons-svelte'; + import Dialog from '../Dialog/Dialog.svelte'; + import { ISO_2_LANGUAGES } from '../../utils/iso-2-languages'; + + export let modalId: symbol; + + export let selectedAudioStreamIndex: number; + export let audioTracks: AudioTrack[]; + export let selectAudioStream: (index: number) => void; + export let onClose = () => {}; + + + +
+

+ Audio + +

+
+ {#each audioTracks || [] as track} + + {/each} +
+
+
diff --git a/src/lib/components/VideoPlayer/SelectSubtitlesModal.svelte b/src/lib/components/VideoPlayer/SelectSubtitlesModal.svelte new file mode 100644 index 0000000..1b7f10f --- /dev/null +++ b/src/lib/components/VideoPlayer/SelectSubtitlesModal.svelte @@ -0,0 +1,56 @@ + + + +

+ Subtitles + +

+
+ + {#each subtitleInfo?.availableSubtitles || [] as subtitles} + + {/each} +
+
diff --git a/src/lib/components/VideoPlayer/Track.svelte b/src/lib/components/VideoPlayer/Track.svelte new file mode 100644 index 0000000..49d1d00 --- /dev/null +++ b/src/lib/components/VideoPlayer/Track.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/VideoPlayer/VideoElement.svelte b/src/lib/components/VideoPlayer/VideoElement.svelte index deb289b..e16e623 100644 --- a/src/lib/components/VideoPlayer/VideoElement.svelte +++ b/src/lib/components/VideoPlayer/VideoElement.svelte @@ -1,9 +1,10 @@ @@ -84,10 +92,22 @@ on:loadeddata={() => { video.currentTime = progressTime; videoDidLoad = true; + console.log('Video loaded'); }} on:dblclick on:click={togglePlay} autoplay playsinline + crossorigin="anonymous" class="w-full h-full" -/> +> + {#if subtitleInfo?.subtitles} + + {/if} + diff --git a/src/lib/components/VideoPlayer/VideoPlayer.svelte b/src/lib/components/VideoPlayer/VideoPlayer.svelte index 48ca682..926751d 100644 --- a/src/lib/components/VideoPlayer/VideoPlayer.svelte +++ b/src/lib/components/VideoPlayer/VideoPlayer.svelte @@ -1,14 +1,23 @@