Work on the movie page

This commit is contained in:
Aleksi Lassila
2024-03-30 15:32:50 +02:00
parent 876eed5ff3
commit 26d4ba0f8f
10 changed files with 338 additions and 142 deletions

View File

@@ -16,6 +16,8 @@
"@nestjs/platform-express": "^10.0.0",
"@nestjs/serve-static": "^4.0.1",
"@nestjs/swagger": "^7.3.0",
"@types/express-http-proxy": "^1.6.6",
"express-http-proxy": "^2.0.0",
"reflect-metadata": "^0.2.1",
"rxjs": "^7.8.1",
"sqlite3": "^5.1.7",
@@ -2251,7 +2253,6 @@
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
"dev": true,
"dependencies": {
"@types/connect": "*",
"@types/node": "*"
@@ -2261,7 +2262,6 @@
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
@@ -2302,7 +2302,6 @@
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
"integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
"dev": true,
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^4.17.33",
@@ -2310,11 +2309,18 @@
"@types/serve-static": "*"
}
},
"node_modules/@types/express-http-proxy": {
"version": "1.6.6",
"resolved": "https://registry.npmjs.org/@types/express-http-proxy/-/express-http-proxy-1.6.6.tgz",
"integrity": "sha512-J8ZqHG76rq1UB716IZ3RCmUhg406pbWxsM3oFCFccl5xlWUPzoR4if6Og/cE4juK8emH0H9quZa5ltn6ZdmQJg==",
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/express-serve-static-core": {
"version": "4.17.43",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz",
"integrity": "sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==",
"dev": true,
"dependencies": {
"@types/node": "*",
"@types/qs": "*",
@@ -2334,8 +2340,7 @@
"node_modules/@types/http-errors": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
"dev": true
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA=="
},
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.6",
@@ -2394,8 +2399,7 @@
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"dev": true
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="
},
"node_modules/@types/node": {
"version": "20.11.30",
@@ -2408,14 +2412,12 @@
"node_modules/@types/qs": {
"version": "6.9.14",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.14.tgz",
"integrity": "sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA==",
"dev": true
"integrity": "sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA=="
},
"node_modules/@types/range-parser": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
"dev": true
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="
},
"node_modules/@types/semver": {
"version": "7.5.8",
@@ -2427,7 +2429,6 @@
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
"dev": true,
"dependencies": {
"@types/mime": "^1",
"@types/node": "*"
@@ -2437,7 +2438,6 @@
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz",
"integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==",
"dev": true,
"dependencies": {
"@types/http-errors": "*",
"@types/mime": "*",
@@ -4527,6 +4527,11 @@
"integrity": "sha512-7nOqkomXZEaxUDJw21XZNtRk739QvrPSoZoRtbsEfcii00vdzZUh6zh1CQwHhrib8MdEtJfv5rJiGeb4KuV/vw==",
"dev": true
},
"node_modules/es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
},
"node_modules/escalade": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
@@ -4925,6 +4930,27 @@
"node": ">= 0.10.0"
}
},
"node_modules/express-http-proxy": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/express-http-proxy/-/express-http-proxy-2.0.0.tgz",
"integrity": "sha512-TXxcPFTWVUMSEmyM6iX2sT/JtmqhqngTq29P+eXTVFdtxZrTmM8THUYK59rUXiln0FfPGvxEpGRnVrgvHksXDw==",
"dependencies": {
"debug": "^3.0.1",
"es6-promise": "^4.1.1",
"raw-body": "^2.3.0"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/express-http-proxy/node_modules/debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dependencies": {
"ms": "^2.1.1"
}
},
"node_modules/express/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",

View File

@@ -28,6 +28,8 @@
"@nestjs/platform-express": "^10.0.0",
"@nestjs/serve-static": "^4.0.1",
"@nestjs/swagger": "^7.3.0",
"@types/express-http-proxy": "^1.6.6",
"express-http-proxy": "^2.0.0",
"reflect-metadata": "^0.2.1",
"rxjs": "^7.8.1",
"sqlite3": "^5.1.7",

View File

@@ -3,11 +3,15 @@ import { AppModule } from './app.module';
import 'reflect-metadata';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import * as fs from 'fs';
import * as proxy from 'express-http-proxy';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api');
app.enableCors();
// app.use('/api/proxy/jellyfin', proxy('http://192.168.0.129:8096'));
const config = new DocumentBuilder().build();
const document = SwaggerModule.createDocument(app, config, {

View File

@@ -1,11 +1,9 @@
import axios from 'axios';
import createClient from 'openapi-fetch';
import { get } from 'svelte/store';
import type { components, paths } from './jellyfin.generated';
import { settings } from '../../stores/settings.store';
import type { DeviceProfile } from './playback-profiles';
import type { Api } from '../api.interface';
import { appState } from '../../stores/app-state.store';
import type { DeviceProfile } from './playback-profiles';
export type JellyfinItem = components['schemas']['BaseItemDto'];
@@ -29,6 +27,10 @@ export class JellyfinApi implements Api<paths> {
return get(appState).user?.settings.jellyfin.userId || '';
}
getApiKey() {
return get(appState).user?.settings.jellyfin.apiKey || '';
}
async getContinueWatching(): Promise<JellyfinItem[] | undefined> {
return this.getClient()
.GET('/Users/{userId}/Items/Resume', {
@@ -45,24 +47,28 @@ export class JellyfinApi implements Api<paths> {
.then((r) => r.data?.Items || []);
}
async getLibraryItems() {
return (
this.getClient()
.GET('/Users/{userId}/Items', {
params: {
path: {
userId: this.getUserId()
},
query: {
hasTmdbId: true,
recursive: true,
includeItemTypes: ['Movie', 'Series'],
fields: ['ProviderIds', 'Genres', 'DateLastMediaAdded', 'DateCreated']
jellyfinItemsCache: JellyfinItem[] = [];
async getLibraryItems(refreshCache = false) {
if (refreshCache || !this.jellyfinItemsCache.length) {
this.jellyfinItemsCache =
(await this.getClient()
.GET('/Users/{userId}/Items', {
params: {
path: {
userId: this.getUserId()
},
query: {
hasTmdbId: true,
recursive: true,
includeItemTypes: ['Movie', 'Series'],
fields: ['ProviderIds', 'Genres', 'DateLastMediaAdded', 'DateCreated']
}
}
}
})
.then((r) => r.data?.Items || []) || Promise.resolve([])
);
})
.then((r) => r.data?.Items || [])) || Promise.resolve([]);
}
return this.jellyfinItemsCache;
}
getPosterUrl(item: JellyfinItem, quality = 100, original = false) {
@@ -74,6 +80,113 @@ export class JellyfinApi implements Api<paths> {
}`
: '';
}
getLibraryItem(itemId: string, refreshCache = false) {
return this.getLibraryItems(refreshCache).then((items) => items.find((i) => i.Id === itemId));
}
getLibraryItemFromTmdbId(tmdbId: string, refreshCache = false) {
return this.getLibraryItems(refreshCache).then((items) =>
items.find((i) => i.ProviderIds?.Tmdb === tmdbId)
);
}
// PLAYBACK
getPlaybackInfo = async (
itemId: string,
playbackProfile: DeviceProfile,
startTimeTicks = 0,
maxStreamingBitrate = 140000000
) =>
this.getClient()
?.POST('/Items/{itemId}/PlaybackInfo', {
params: {
path: {
itemId: itemId
},
query: {
userId: this.getUserId(),
startTimeTicks,
autoOpenLiveStream: true,
maxStreamingBitrate
}
},
body: {
DeviceProfile: playbackProfile
}
})
.then((r) => ({
playbackUri:
r.data?.MediaSources?.[0]?.TranscodingUrl ||
`/Videos/${r.data?.MediaSources?.[0]?.Id}/stream.mp4?Static=true&mediaSourceId=${
r.data?.MediaSources?.[0]?.Id
}&deviceId=${JELLYFIN_DEVICE_ID}&api_key=${this.getApiKey()}&Tag=${
r.data?.MediaSources?.[0]?.ETag
}`,
mediaSourceId: r.data?.MediaSources?.[0]?.Id,
playSessionId: r.data?.PlaySessionId,
directPlay:
!!r.data?.MediaSources?.[0]?.SupportsDirectPlay ||
!!r.data?.MediaSources?.[0]?.SupportsDirectStream
}));
reportPlaybackStarted(
itemId: string,
sessionId: string,
mediaSourceId: string,
audioStreamIndex?: number,
subtitleStreamIndex?: number
) {
return this.getClient()?.POST('/Sessions/Playing', {
body: {
CanSeek: true,
ItemId: itemId,
PlaySessionId: sessionId,
MediaSourceId: mediaSourceId,
AudioStreamIndex: 1,
SubtitleStreamIndex: -1
}
});
}
reportPlaybackProgress(
itemId: string,
sessionId: string,
isPaused: boolean,
positionTicks: number
) {
return this.getClient()?.POST('/Sessions/Playing/Progress', {
body: {
ItemId: itemId,
PlaySessionId: sessionId,
IsPaused: isPaused,
PositionTicks: Math.round(positionTicks),
CanSeek: true,
MediaSourceId: itemId
}
});
}
reportPlaybackStopped(itemId: string, sessionId: string, positionTicks: number) {
return this.getClient()?.POST('/Sessions/Playing/Stopped', {
body: {
ItemId: itemId,
PlaySessionId: sessionId,
PositionTicks: Math.round(positionTicks),
MediaSourceId: itemId
}
});
}
deleteActiveEncoding(playSessionId: string) {
return this.getClient()?.DELETE('/Videos/ActiveEncodings', {
params: {
query: {
deviceId: JELLYFIN_DEVICE_ID,
playSessionId: playSessionId
}
}
});
}
}
export const jellyfinApi = new JellyfinApi();

View File

@@ -0,0 +1,17 @@
<script lang="ts">
import Container from '../../Container.svelte';
import type { Readable } from 'svelte/store';
import classNames from 'classnames';
let hasFoucus: Readable<boolean>;
</script>
<Container
bind:hasFocus={hasFoucus}
class={classNames('selectable px-6 py-2 rounded-xl font-medium tracking-wide', {
'bg-stone-200 text-stone-900': $hasFoucus,
'bg-stone-800 text-white': !$hasFoucus
})}
on:click
>
<slot />
</Container>

View File

@@ -40,7 +40,7 @@
<Laptop class="w-8 h-8" slot="icon" />
</div>
</Container>
<Container on:click={() => navigate('/movie/76600')}>
<Container on:click={() => navigate('/movie/695721')}>
<div class={itemContainer(1, $focusIndex)}>
<CardStack class="w-8 h-8" slot="icon" />
</div>

View File

@@ -1,19 +1,9 @@
<script lang="ts">
import {
delteActiveEncoding as deleteActiveEncoding,
getJellyfinItem,
getJellyfinPlaybackInfo,
reportJellyfinPlaybackProgress,
reportJellyfinPlaybackStarted,
reportJellyfinPlaybackStopped
} from '../../apis/jellyfin/jellyfin-api';
import getDeviceProfile from '../../apis/jellyfin/playback-profiles';
import { getQualities } from '../../apis/jellyfin/qualities';
import { settings } from '../../stores/settings.store';
import classNames from 'classnames';
import Hls from 'hls.js';
import {
Cross2,
EnterFullScreen,
ExitFullScreen,
Gear,
@@ -33,6 +23,9 @@
import { linear } from 'svelte/easing';
import ContextMenuButton from '../ContextMenu/ContextMenuButton.svelte';
import { isTizen } from '../../utils/browser-detection';
import { jellyfinApi } from '../../apis/jellyfin/jellyfin-api.js';
import { videoPlayerSettings } from '../../stores/localstorage.store';
import { get } from 'svelte/store';
export let jellyfinId: string;
@@ -54,7 +47,8 @@
// Find the correct functions
let elem = document.createElement('div');
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
if (elem.requestFullscreen) {
reqFullscreenFunc = (elem) => {
elem.requestFullscreen();
@@ -62,41 +56,55 @@
fullscreenChangeEvent = 'fullscreenchange';
getFullscreenElement = () => <HTMLElement>document.fullscreenElement;
if (document.exitFullscreen) exitFullscreen = () => document.exitFullscreen();
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
} else if (elem.webkitRequestFullscreen) {
reqFullscreenFunc = (elem) => {
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
elem.webkitRequestFullscreen();
};
fullscreenChangeEvent = 'webkitfullscreenchange';
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
getFullscreenElement = () => <HTMLElement>document.webkitFullscreenElement;
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
if (document.webkitExitFullscreen) exitFullscreen = () => document.webkitExitFullscreen();
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
} else if (elem.msRequestFullscreen) {
reqFullscreenFunc = (elem) => {
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
elem.msRequestFullscreen();
};
fullscreenChangeEvent = 'MSFullscreenChange';
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
getFullscreenElement = () => <HTMLElement>document.msFullscreenElement;
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
if (document.msExitFullscreen) exitFullscreen = () => document.msExitFullscreen();
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
} else if (elem.mozRequestFullScreen) {
reqFullscreenFunc = (elem) => {
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
elem.mozRequestFullScreen();
};
fullscreenChangeEvent = 'mozfullscreenchange';
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
getFullscreenElement = () => <HTMLElement>document.mozFullScreenElement;
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
if (document.mozCancelFullScreen) exitFullscreen = () => document.mozCancelFullScreen();
}
const inintialValues = get(videoPlayerSettings);
let paused: boolean = false;
let duration: number = 0;
let displayedTime: number = 0;
@@ -107,8 +115,8 @@
let playerStateBeforeSeek: boolean;
let fullscreen: boolean = false;
let volume: number = 1;
let mute: boolean = false;
let volume: number = inintialValues.volume;
let mute: boolean = inintialValues.muted;
let resolution: number = 1080;
let currentBitrate: number = 0;
@@ -117,92 +125,96 @@
let uiVisible = true;
$: uiVisible = !shouldCloseUi || seeking || paused || $contextMenu === qualityContextMenuId;
$: videoPlayerSettings.set({ volume, muted: mute });
const fetchPlaybackInfo = (
itemId: string,
maxBitrate: number | undefined = undefined,
starting: boolean = true
) =>
getJellyfinItem(itemId).then((item) =>
getJellyfinPlaybackInfo(
itemId,
getDeviceProfile(),
item?.UserData?.PlaybackPositionTicks || Math.floor(displayedTime * 10_000_000),
maxBitrate || getQualities(item?.Height || 1080)[0].maxBitrate
).then(async (playbackInfo) => {
if (!playbackInfo) return;
const { playbackUri, playSessionId: sessionId, mediaSourceId, directPlay } = playbackInfo;
jellyfinApi.getLibraryItem(itemId).then((item) =>
jellyfinApi
.getPlaybackInfo(
itemId,
getDeviceProfile(),
item?.UserData?.PlaybackPositionTicks || Math.floor(displayedTime * 10_000_000),
maxBitrate || getQualities(item?.Height || 1080)[0]?.maxBitrate
)
.then(async (playbackInfo) => {
if (!playbackInfo) return;
const { playbackUri, playSessionId: sessionId, mediaSourceId, directPlay } = playbackInfo;
if (!playbackUri || !sessionId) {
console.log('No playback URL or session ID', playbackUri, sessionId);
return;
}
video.poster = item?.BackdropImageTags?.length
? `${$settings.jellyfin.baseUrl}/Items/${item?.Id}/Images/Backdrop?quality=100&tag=${item?.BackdropImageTags?.[0]}`
: '';
videoLoaded = false;
if (!directPlay) {
if (Hls.isSupported()) {
const hls = new Hls();
hls.loadSource($settings.jellyfin.baseUrl + playbackUri);
hls.attachMedia(video);
} else if (video.canPlayType('application/vnd.apple.mpegurl') || isTizen()) {
/*
* HLS.js does NOT work on iOS on iPhone because Safari on iPhone does not support MSE.
* This is not a problem, since HLS is natively supported on iOS. But any other browser
* that does not support MSE will not be able to play the video.
*/
video.src = $settings.jellyfin.baseUrl + playbackUri;
} else {
throw new Error('HLS is not supported');
if (!playbackUri || !sessionId) {
console.log('No playback URL or session ID', playbackUri, sessionId);
return;
}
} else {
video.src = $settings.jellyfin.baseUrl + playbackUri;
}
resolution = item?.Height || 1080;
currentBitrate = maxBitrate || getQualities(resolution)[0].maxBitrate;
video.poster = item?.BackdropImageTags?.length
? `${$settings.jellyfin.baseUrl}/Items/${item?.Id}/Images/Backdrop?quality=100&tag=${item?.BackdropImageTags?.[0]}`
: '';
if (item?.UserData?.PlaybackPositionTicks) {
displayedTime = item?.UserData?.PlaybackPositionTicks / 10_000_000;
}
videoLoaded = false;
if (!directPlay) {
if (Hls.isSupported()) {
const hls = new Hls();
// We should not requestFullscreen automatically, as it's not what
// the user expects. Moreover, most browsers will deny the request
// if the video takes a while to load.
// video.play().then(() => videoWrapper.requestFullscreen());
hls.loadSource($settings.jellyfin.baseUrl + playbackUri);
hls.attachMedia(video);
} else if (video.canPlayType('application/vnd.apple.mpegurl') || isTizen()) {
/*
* HLS.js does NOT work on iOS on iPhone because Safari on iPhone does not support MSE.
* This is not a problem, since HLS is natively supported on iOS. But any other browser
* that does not support MSE will not be able to play the video.
*/
video.src = $settings.jellyfin.baseUrl + playbackUri;
} else {
throw new Error('HLS is not supported');
}
} else {
video.src = $settings.jellyfin.baseUrl + playbackUri;
}
// A start report should only be sent when the video starts playing,
// not every time a playback info request is made
if (mediaSourceId && starting)
await reportJellyfinPlaybackStarted(itemId, sessionId, mediaSourceId);
resolution = item?.Height || 1080;
currentBitrate = maxBitrate || getQualities(resolution)[0]?.maxBitrate;
reportProgress = async () => {
await reportJellyfinPlaybackProgress(
itemId,
sessionId,
video?.paused == true,
video?.currentTime * 10_000_000
);
};
if (item?.UserData?.PlaybackPositionTicks) {
displayedTime = item?.UserData?.PlaybackPositionTicks / 10_000_000;
}
if (progressInterval) clearInterval(progressInterval);
progressInterval = setInterval(() => {
video && video.readyState === 4 && video?.currentTime > 0 && sessionId && itemId;
reportProgress();
}, 5000);
// We should not requestFullscreen automatically, as it's not what
// the user expects. Moreover, most browsers will deny the request
// if the video takes a while to load.
// video.play().then(() => videoWrapper.requestFullscreen());
deleteEncoding = () => {
deleteActiveEncoding(sessionId);
};
// A start report should only be sent when the video starts playing,
// not every time a playback info request is made
if (mediaSourceId && starting)
await jellyfinApi.reportPlaybackStarted(itemId, sessionId, mediaSourceId);
stopCallback = () => {
reportJellyfinPlaybackStopped(itemId, sessionId, video?.currentTime * 10_000_000);
deleteEncoding();
};
})
reportProgress = async () => {
await jellyfinApi.reportPlaybackProgress(
itemId,
sessionId,
video?.paused == true,
video?.currentTime * 10_000_000
);
};
if (progressInterval) clearInterval(progressInterval);
progressInterval = setInterval(() => {
video && video.readyState === 4 && video?.currentTime > 0 && sessionId && itemId;
reportProgress();
}, 5000);
deleteEncoding = () => {
jellyfinApi.deleteActiveEncoding(sessionId);
};
stopCallback = () => {
jellyfinApi.reportPlaybackStopped(itemId, sessionId, video?.currentTime * 10_000_000);
deleteEncoding();
};
})
);
function onSeekStart() {
@@ -294,17 +306,14 @@
$: {
if (video && jellyfinId) {
if (video.src === '') fetchPlaybackInfo(jellyfinId);
paused = false;
console.log('Paused', paused);
video.play();
// video.play();
}
}
onMount(() => {
// Workaround because the paused state does not sync
// with the video element until a change is made
paused = false;
// paused = false;
// if (video && $playerState.jellyfinId) {
// if (video.src === '') fetchPlaybackInfo($playerState.jellyfinId);
// }
@@ -345,7 +354,7 @@
function handleShortcuts(event: KeyboardEvent) {
if (event.key === 'f') {
handleRequestFullscreen();
} else if (event.key === ' ') {
} else if (event.key === ' ' || event.key === 'k') {
paused = !paused;
} else if (event.key === 'ArrowLeft') {
video.currentTime -= 10;
@@ -355,6 +364,8 @@
volume = Math.min(volume + 0.1, 1);
} else if (event.key === 'ArrowDown') {
volume = Math.max(volume - 0.1, 0);
} else if (event.key === 'm') {
mute = !mute;
}
}
</script>
@@ -374,7 +385,7 @@
<!--&gt;-->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="w-screen h-screen flex items-center justify-center"
class="w-full h-full flex items-center justify-center relative"
bind:this={videoWrapper}
on:mousemove={() => handleUserInteraction(false)}
on:touchend|preventDefault={() => handleUserInteraction(true)}
@@ -407,7 +418,7 @@
{#if uiVisible}
<!-- Video controls -->
<div
class="absolute bottom-0 w-screen bg-gradient-to-t from-black/[.8] via-60% via-black-opacity-80 to-transparent"
class="absolute bottom-0 inset-x-0 bg-gradient-to-t from-black/[.8] via-60% via-black-opacity-80 to-transparent"
on:touchend|stopPropagation
transition:fade={{ duration: 100 }}
>

View File

@@ -5,11 +5,16 @@
import { TMDB_IMAGES_ORIGINAL } from '../constants';
import classNames from 'classnames';
import { DotFilled } from 'radix-icons-svelte';
import SidebarMargin from '../components/SidebarMargin.svelte';
import Button from '../components/Button.svelte';
import { jellyfinApi } from '../apis/jellyfin/jellyfin-api';
import VideoPlayer from '../components/VideoPlayer/VideoPlayer.svelte';
export let id: string;
const movieDataP = tmdbApi.getTmdbMovie(Number(id));
const jellyfinItem = jellyfinApi.getLibraryItemFromTmdbId(id);
let playbackId: string = '';
let heroIndex: number;
</script>
@@ -60,8 +65,19 @@
</div>
{/if}
{/await}
{#await jellyfinItem then item}
{#if item}
<div class="flex mt-4">
<Button on:click={() => (playbackId = item.Id || '')}>Play</Button>
</div>
{/if}
{/await}
</div>
</HeroCarousel>
</div>
<Container on:click={() => history.back()}>Go back</Container>
<div>
{#if playbackId}
<VideoPlayer jellyfinId={playbackId} />
{/if}
</div>
</Container>

View File

@@ -25,7 +25,7 @@ type AwaitableStoreValue<R, T = { data?: R }> = {
loading: boolean;
} & T;
function _createDataFetchStore<T>(fn: () => Promise<T>) {
export function _createDataFetchStore<T>(fn: () => Promise<T>) {
const store = writable<AwaitableStoreValue<T>>({
loading: true,
data: undefined

View File

@@ -22,3 +22,10 @@ export function createLocalStorageStore<T>(key: string, defaultValue: T) {
}
export const skippedVersion = createLocalStorageStore<string | null>('skipped-version', null);
export const videoPlayerSettings = createLocalStorageStore<{
muted: boolean;
volume: number;
}>('video-player-settings', {
muted: false,
volume: 1
});