feat: add clock, source and titles to videoplayer

This commit is contained in:
Aleksi Lassila
2025-02-08 05:02:58 +02:00
parent f365c96df5
commit 4a20351911
4 changed files with 180 additions and 139 deletions

View File

@@ -5,17 +5,26 @@
import type { NavigateEvent } from '../../selectable';
import { formatMinutesToTime, formatSecondsToTime } from '../../utils';
export let totalTime: number;
export let progressTime: number;
export let duration: number;
export let currentTime: number;
export let bufferedTime: number;
export let step = 0.1;
export let paused = false;
export let seeking = false;
let displayTime = currentTime;
const updateDisplayTime = (time: number) => {
if (!seeking) {
displayTime = time;
}
};
$: updateDisplayTime(currentTime);
const dispatch = createEventDispatcher<{
jumpTo: number;
playPause: void;
play: void;
pause: void;
}>();
let pausedBeforeSeeking = paused;
@@ -23,25 +32,28 @@
seeking = true;
pausedBeforeSeeking = paused;
paused = true;
dispatch('pause');
}
function handleStopSeeking() {
if (!seeking) return;
paused = pausedBeforeSeeking;
seeking = false;
dispatch('jumpTo', displayTime);
dispatch('jumpTo', progressTime);
if (!pausedBeforeSeeking) {
dispatch('play');
} else {
dispatch('pause');
}
seeking = false;
}
let pauseTime = 0;
let pauseTime: number | undefined = undefined;
const handlePause = (paused: boolean) => {
if (paused) {
pauseTime = progressTime;
} else if (pauseTime > 0) {
if (pauseTime !== undefined) {
dispatch('jumpTo', pauseTime);
pauseTime = 0;
displayTime = pauseTime;
pauseTime = undefined;
}
};
$: handlePause(paused);
@@ -49,16 +61,16 @@
function handleNavigateEvent({ detail }: CustomEvent<NavigateEvent>) {
if (detail.direction === 'left') {
if (paused) {
pauseTime = pauseTime - 10;
pauseTime = Math.max(0, (pauseTime ?? currentTime) - 10);
} else {
dispatch('jumpTo', progressTime - 10);
dispatch('jumpTo', displayTime - 10);
detail.preventNavigation();
}
} else if (detail.direction === 'right') {
if (paused) {
pauseTime = pauseTime + 30;
pauseTime = Math.min(duration, (pauseTime ?? currentTime) + 30);
} else {
dispatch('jumpTo', progressTime + 30);
dispatch('jumpTo', displayTime + 30);
detail.preventNavigation();
}
}
@@ -71,15 +83,15 @@
let:hasFocus
focusOnMount
on:navigate={handleNavigateEvent}
on:select={() => dispatch('playPause')}
on:select={() => dispatch(paused ? 'play' : 'pause')}
>
<div class="absolute inset-y-1 inset-x-2 rounded-full bg-zinc-300/50" />
<!-- Secondary progress -->
<div
class="absolute inset-y-1 inset-x-2 rounded-full bg-zinc-300/50 transition-transform"
style={`left: 0.5rem; right: calc(${(1 - bufferedTime / totalTime) * 100}% - 0.5rem + ${
bufferedTime / totalTime
style={`left: 0.5rem; right: calc(${(1 - bufferedTime / duration) * 100}% - 0.5rem + ${
bufferedTime / duration
}rem);`}
/>
@@ -87,8 +99,8 @@
<div
class="absolute inset-y-1 inset-x-2 rounded-full bg-secondary-100 transition-transform"
style={`left: 0.5rem; right: calc(${
(1 - (pauseTime > 0 ? pauseTime : progressTime) / totalTime) * 100
}% - 0.5rem + ${(pauseTime > 0 ? pauseTime : progressTime) / totalTime}rem);`}
(1 - (pauseTime ?? displayTime) / duration) * 100
}% - 0.5rem + ${(pauseTime ?? displayTime) / duration}rem);`}
/>
<div
@@ -98,8 +110,8 @@
'opacity-0 group-hover:opacity-100': !hasFocus
}
)}
style={`left: calc(${((pauseTime > 0 ? pauseTime : progressTime) / totalTime) * 100}% - ${
(pauseTime > 0 ? pauseTime : progressTime) / totalTime
style={`left: calc(${((pauseTime ?? displayTime) / duration) * 100}% - ${
(pauseTime ?? displayTime) / duration
}rem);
box-shadow: 0 0 0.25rem 2px #00000033;
`}
@@ -109,9 +121,9 @@
type="range"
class="w-full absolute cursor-pointer h-4 inset-y-0 opacity-0"
min={0}
max={totalTime}
max={duration}
{step}
bind:value={progressTime}
bind:value={displayTime}
on:mousedown={handleStartSeeking}
on:mouseup={handleStopSeeking}
on:touchstart={handleStartSeeking}
@@ -119,7 +131,7 @@
/>
</Container>
<div class="flex justify-between px-2 pt-4 text-lg">
<span>{formatSecondsToTime(pauseTime || progressTime)}</span>
<span>-{formatSecondsToTime(totalTime - (pauseTime || progressTime))}</span>
<span>{formatSecondsToTime(pauseTime || displayTime)}</span>
<span>-{formatSecondsToTime(duration - (pauseTime || displayTime))}</span>
</div>
</div>

View File

@@ -1,23 +1,24 @@
<script lang="ts">
import { get } from 'svelte/store';
import { sessions } from '../../stores/session.store';
import { reiverrApiNew, user } from '../../stores/user.store';
import type { PlaybackInfo, SubtitleInfo, Subtitles, VideoPlayerContext } from './VideoPlayer';
import VideoPlayerModal from './VideoPlayerModal.svelte';
import { getQualities } from '../../apis/jellyfin/qualities';
import getDeviceProfile from '../../apis/jellyfin/playback-profiles';
import { tmdbApi } from '../../apis/tmdb/tmdb-api';
import { onDestroy, onMount } from 'svelte';
import { createLocalStorageStore } from '../../stores/localstorage.store';
import Modal from '../Modal/Modal.svelte';
import VideoPlayer from './VideoPlayer.svelte';
import { modalStackTop } from '../Modal/modal.store';
import type { StreamDto } from '$lib/apis/reiverr/reiverr.openapi';
import {
episodeUserDataStore,
movieUserDataStore,
seriesUserDataStore
seriesUserDataStore,
tmdbMovieDataStore,
tmdbSeriesDataStore
} from '$lib/stores/data.store';
import { onDestroy, onMount } from 'svelte';
import { get } from 'svelte/store';
import getDeviceProfile from '../../apis/jellyfin/playback-profiles';
import { createLocalStorageStore } from '../../stores/localstorage.store';
import { sessions } from '../../stores/session.store';
import { reiverrApiNew, user } from '../../stores/user.store';
import { modalStackTop } from '../Modal/modal.store';
import Modal from '../Modal/Modal.svelte';
import type { PlaybackInfo, SubtitleInfo, Subtitles, VideoPlayerContext } from './VideoPlayer';
import VideoPlayer from './VideoPlayer.svelte';
export let modalId: symbol;
export let tmdbId: string;
export let season: number | undefined = undefined;
@@ -26,15 +27,22 @@
export let key: string = '';
export let progress: number = 0;
export let modalId: symbol;
export let hidden: boolean = false;
let title: string = '';
let subtitle: string = '';
let sourceUri = '';
const { unsubscribe, ...request } = (
season !== undefined && episode !== undefined ? tmdbSeriesDataStore : tmdbMovieDataStore
).getRequest(Number(tmdbId));
let playerContext: VideoPlayerContext | undefined;
request.subscribe((item) => {
if (!item) return;
if ('title' in item) {
title = item.title ?? title;
} else if ('name' in item) {
title = `Episode ${episode}`;
subtitle = item.name ?? '';
}
});
type MediaLanguageStore = {
subtitles?: string;
@@ -43,8 +51,6 @@
let video: HTMLVideoElement;
let paused: boolean;
let progressTime: number;
let videoDuration: number;
let playbackInfo: PlaybackInfo | undefined;
let subtitleInfo: SubtitleInfo | undefined;
@@ -170,7 +176,9 @@
// (item?.UserData?.PlaybackPositionTicks || 0) / 10_000_000 ||
// undefined
};
videoDuration = stream.duration;
// title = stream.title;
// subtitle = stream.subtitle;
// if (mediaSourceId) reportPlaybackStarted(id, sessionId, mediaSourceId);
@@ -183,7 +191,9 @@
);
};
onMount(() => refreshVideoStream());
onMount(() => {
refreshVideoStream();
});
/*
title
subtitle
@@ -232,8 +242,8 @@
modalHidden={$modalStackTop?.id !== modalId}
{title}
{subtitle}
source={`${sourceId}`}
bind:paused
bind:progressTime
bind:video
bind:subtitleInfo
/>

View File

@@ -10,17 +10,16 @@
export let playbackInfo: PlaybackInfo | undefined;
export let subtitleInfo: SubtitleInfo | undefined;
export let video: HTMLVideoElement;
export let videoDidLoad = false;
export let paused = false;
export let seeking = false;
export let totalTime = 0;
export let progressTime = 0;
export let duration = 0;
export let currentTime = 0;
export let bufferedTime = 0;
export let buffering = false;
export let muted = false;
export let volume = 1;
export let videoDidLoad = false;
export let buffering = false;
export let video: HTMLVideoElement;
// let hls: Hls | undefined;
@@ -102,12 +101,10 @@
<video
bind:this={video}
bind:paused
bind:duration={totalTime}
bind:duration
bind:volume
bind:muted
on:timeupdate={() => {
progressTime = !seeking && videoDidLoad ? video.currentTime : progressTime;
}}
bind:currentTime
on:progress={handleProgress}
on:loadeddata={() => {
// console.log('video loaded', video.currentTime, video.duration, playbackInfo?.progress);

View File

@@ -1,36 +1,36 @@
<script lang="ts">
import Container from '../Container.svelte';
import VideoElement from './VideoElement.svelte';
import type { PlaybackInfo, SubtitleInfo, Subtitles } from './VideoPlayer';
import classNames from 'classnames';
import ProgressBar from './ProgressBar.svelte';
import { ChatBubble, Pause, TextAlignLeft } from 'radix-icons-svelte';
import { onDestroy } from 'svelte';
import type { Selectable } from '../../selectable';
import Container from '../Container.svelte';
import { modalStack } from '../Modal/modal.store';
import SelectSubtitlesModal from './SelectSubtitlesModal.svelte';
import { ChatBubble, Pause, TextAlignLeft } from 'radix-icons-svelte';
import IconButton from './IconButton.svelte';
import SelectAudioModal from './SelectAudioModal.svelte';
import Spinner from '../Utils/Spinner.svelte';
import { createInfoNotification } from '../Notifications/notification.store';
import IconButton from './IconButton.svelte';
import ProgressBar from './ProgressBar.svelte';
import SelectAudioModal from './SelectAudioModal.svelte';
import SelectSubtitlesModal from './SelectSubtitlesModal.svelte';
import VideoElement from './VideoElement.svelte';
import type { PlaybackInfo, SubtitleInfo, Subtitles } from './VideoPlayer';
export let playbackInfo: PlaybackInfo | undefined;
export let subtitleInfo: SubtitleInfo | undefined;
export let title: string;
export let subtitle: string;
export let subtitle: string = ''
export let source: string = '';
export let modalHidden = false;
// Bindings
export let videoDidLoad = false;
export let paused = false;
export let seeking = false;
export let totalTime = 0;
export let progressTime = 0;
export let duration = 0;
export let currentTime = 0;
export let bufferedTime = 0;
let buffering = false;
export let muted = false;
export let volume = 1;
export let videoDidLoad = false;
let buffering = false;
let seeking = false;
export let video: HTMLVideoElement;
@@ -39,6 +39,11 @@
let hideInterfaceTimeout: ReturnType<typeof setTimeout>;
let container: Selectable;
let clockTime = 0;
let clockInterval = setInterval(() => {
clockTime = Date.now();
}, 1000);
$: if (modalHidden) video?.pause();
else video?.play();
$: if (!seeking && !modalHidden) handleShowInterface();
@@ -97,6 +102,7 @@
onDestroy(() => {
clearTimeout(showInterfaceTimeout);
clearTimeout(hideInterfaceTimeout);
clearInterval(clockInterval);
});
</script>
@@ -110,14 +116,14 @@
}
handleShowInterface();
}}
on:click={() => (paused ? video?.play() : video?.pause())}
>
<VideoElement
bind:playbackInfo
bind:subtitleInfo
bind:paused
bind:seeking
bind:totalTime
bind:progressTime
bind:duration
bind:currentTime
bind:bufferedTime
bind:muted
bind:volume
@@ -156,69 +162,85 @@
</div>
{/if}
<Container
class={classNames('absolute inset-x-12 bottom-8 transition-opacity flex flex-col', {
'opacity-0': !showInterface
})}
class={classNames(
'absolute inset-x-12 inset-y-8 transition-opacity flex flex-col justify-between',
{
'opacity-0': !showInterface
}
)}
bind:selectable={container}
>
<Container
direction="horizontal"
on:navigate={({ detail }) => {
if (detail.direction === 'up') {
detail.stopPropagation();
detail.preventNavigation();
handleHideInterface();
}
}}
class="flex justify-between px-2 py-4 items-end"
<div
class="flex justify-between items-center text-secondary-300 font-medium text-wider text-xl tracking-wide"
>
<div>@{source}</div>
<div>
<div class="text-secondary-300 font-medium text-wider text-xl mb-1 tracking-wide">
{subtitle}
Ends at {new Date(
clockTime + ((duration ?? 0) - (currentTime ?? 0)) * 1000
).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}
</div>
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div on:click={(e) => e.stopPropagation()}>
<Container
direction="horizontal"
on:navigate={({ detail }) => {
if (detail.direction === 'up') {
detail.stopPropagation();
detail.preventNavigation();
handleHideInterface();
}
}}
class="flex justify-between px-2 py-4 items-end"
>
<div>
<div class="text-secondary-300 font-medium text-wider text-xl mb-1 tracking-wide">
{subtitle}
</div>
<h1 class="header4">{title}</h1>
</div>
<h1 class="header4">{title}</h1>
</div>
<div class="flex space-x-2">
<IconButton
on:clickOrSelect={() => {
// video.pause();
modalStack.create(SelectSubtitlesModal, {
subtitleInfo,
selectSubtitles
});
}}
>
<TextAlignLeft size={24} />
</IconButton>
<IconButton
on:clickOrSelect={() => {
// video.pause();
modalStack.create(SelectAudioModal, {
selectedAudioStreamIndex: playbackInfo?.audioStreamIndex || -1,
audioTracks: playbackInfo?.audioTracks || [],
selectAudioStream
// onClose: () => video.play()
});
}}
>
<ChatBubble size={24} />
</IconButton>
</div>
</Container>
<ProgressBar
bind:seeking
on:jumpTo={(e) => {
video.currentTime = e.detail;
}}
on:playPause={() => {
if (paused) video.play();
else video.pause();
}}
bind:totalTime
bind:progressTime
bind:bufferedTime
bind:paused
/>
<div class="flex space-x-2">
<IconButton
on:clickOrSelect={() => {
// video.pause();
modalStack.create(SelectSubtitlesModal, {
subtitleInfo,
selectSubtitles
});
}}
>
<TextAlignLeft size={24} />
</IconButton>
<IconButton
on:clickOrSelect={() => {
// video.pause();
modalStack.create(SelectAudioModal, {
selectedAudioStreamIndex: playbackInfo?.audioStreamIndex || -1,
audioTracks: playbackInfo?.audioTracks || [],
selectAudioStream
// onClose: () => video.play()
});
}}
>
<ChatBubble size={24} />
</IconButton>
</div>
</Container>
<ProgressBar
bind:seeking
on:jumpTo={(e) => {
video.currentTime = e.detail;
video.play();
}}
on:play={() => video.play()}
on:pause={() => video.pause()}
{duration}
{currentTime}
{bufferedTime}
bind:paused
/>
</div>
</Container>
</Container>