feat: Improved movie playback and listing stream options

This commit is contained in:
Aleksi Lassila
2024-12-10 03:32:51 +02:00
parent 96d52299b0
commit 1f7f74a8a7
13 changed files with 558 additions and 188 deletions

View File

@@ -13,7 +13,14 @@ import {
Subtitles,
UserContext,
VideoStream,
VideoStreamCandidate,
} from 'plugins/plugin-types';
import {
bitrateQualities,
formatSize,
formatTicksToTime,
getClosestBitrate,
} from './utils';
interface JellyfinSettings extends PluginSettings {
apiKey: string;
@@ -27,68 +34,14 @@ interface JellyfinUserContext extends UserContext {
const JELLYFIN_DEVICE_ID = 'Reiverr Client';
const bitrateQualities = [
{
label: '4K - 120 Mbps',
bitrate: 120000000,
codec: undefined,
},
{
label: '4K - 80 Mbps',
bitrate: 80000000,
codec: undefined,
},
{
label: '1080p - 40 Mbps',
bitrate: 40000000,
codec: undefined,
},
{
label: '1080p - 10 Mbps',
bitrate: 10000000,
codec: undefined,
},
{
label: '720p - 8 Mbps',
bitrate: 8000000,
codec: undefined,
},
{
label: '720p - 4 Mbps',
bitrate: 4000000,
codec: undefined,
},
{
label: '480p - 3 Mbps',
bitrate: 3000000,
codec: undefined,
},
{
label: '480p - 720 Kbps',
bitrate: 720000,
codec: undefined,
},
{
label: '360p - 420 Kbps',
bitrate: 420000,
codec: undefined,
},
];
function getClosestBitrate(qualities, bitrate) {
return qualities.reduce(
(prev, curr) =>
Math.abs(curr.bitrate - bitrate) < Math.abs(prev.bitrate - bitrate)
? curr
: prev,
qualities[0],
);
}
@Injectable()
export default class JellyfinPlugin implements SourcePlugin {
name: string = 'jellyfin';
private getProxyUrl(tmdbId: string) {
return `/api/movies/${tmdbId}/sources/${this.name}/stream/proxy`;
}
validateSettings: (settings: JellyfinSettings) => Promise<{
isValid: boolean;
errors: Record<string, string>;
@@ -213,8 +166,25 @@ export default class JellyfinPlugin implements SourcePlugin {
.then((res) => res.data.Items ?? []);
}
async getMovieStreams(
tmdbId: string,
userContext: JellyfinUserContext,
config: PlaybackConfig = {
audioStreamIndex: undefined,
bitrate: undefined,
progress: undefined,
defaultLanguage: undefined,
deviceProfile: undefined,
},
): Promise<VideoStreamCandidate[]> {
return this.getMovieStream(tmdbId, '', userContext, config).then(
(stream) => [stream],
);
}
async getMovieStream(
tmdbId: string,
key: string,
userContext: JellyfinUserContext,
config: PlaybackConfig = {
audioStreamIndex: undefined,
@@ -226,7 +196,7 @@ export default class JellyfinPlugin implements SourcePlugin {
): Promise<VideoStream> {
const context = new PluginContext(userContext.settings, userContext.token);
const items = await this.getLibraryItems(context);
const proxyUrl = `/api/movies/${tmdbId}/sources/${this.name}/stream`;
const proxyUrl = this.getProxyUrl(tmdbId);
const movie = items.find((item) => item.ProviderIds?.Tmdb === tmdbId);
@@ -288,22 +258,20 @@ export default class JellyfinPlugin implements SourcePlugin {
index: s.Index,
})) ?? [];
const qualities = [
const qualities: VideoStream['qualities'] = [
...bitrateQualities,
{
bitrate: mediasSource.Bitrate,
label: 'Original',
codec: undefined,
original: true,
},
].map((q, i) => ({
...q,
index: i,
}));
const bitrate = Math.min(
maxStreamingBitrate,
movie.MediaSources[0].Bitrate,
);
const bitrate = Math.min(maxStreamingBitrate, mediasSource.Bitrate);
const subtitles: Subtitles[] = mediasSource.MediaStreams.filter(
(s) => s.Type === 'Subtitle' && s.DeliveryUrl,
@@ -315,6 +283,32 @@ export default class JellyfinPlugin implements SourcePlugin {
}));
return {
key: '',
title: movie.Name,
properties: [
{
label: 'Video',
value: mediasSource.Bitrate || 0,
formatted:
mediasSource.MediaStreams.find((s) => s.Type === 'Video')
?.DisplayTitle || 'Unknown',
},
{
label: 'Size',
value: mediasSource.Size,
formatted: formatSize(mediasSource.Size),
},
{
label: 'Filename',
value: mediasSource.Name,
formatted: undefined,
},
{
label: 'Runtime',
value: mediasSource.RunTimeTicks,
formatted: formatTicksToTime(mediasSource.RunTimeTicks),
},
],
audioStreamIndex:
config.audioStreamIndex ??
mediasSource?.DefaultAudioStreamIndex ??
@@ -322,7 +316,7 @@ export default class JellyfinPlugin implements SourcePlugin {
audioStreams,
progress: config.progress ?? 0,
qualities,
quality: getClosestBitrate(qualities, bitrate).index,
qualityIndex: getClosestBitrate(qualities, bitrate).index,
subtitles,
uri: playbackUri,
directPlay:

View File

@@ -0,0 +1,91 @@
export function formatSize(size: number) {
const gbs = size / 1024 / 1024 / 1024;
const mbs = size / 1024 / 1024;
if (gbs >= 1) {
return `${gbs.toFixed(2)} GB`;
} else {
return `${mbs.toFixed(2)} MB`;
}
}
export const bitrateQualities = [
{
label: '4K - 120 Mbps',
bitrate: 120000000,
codec: undefined,
original: false,
},
{
label: '4K - 80 Mbps',
bitrate: 80000000,
codec: undefined,
original: false,
},
{
label: '1080p - 40 Mbps',
bitrate: 40000000,
codec: undefined,
original: false,
},
{
label: '1080p - 10 Mbps',
bitrate: 10000000,
codec: undefined,
original: false,
},
{
label: '720p - 8 Mbps',
bitrate: 8000000,
codec: undefined,
original: false,
},
{
label: '720p - 4 Mbps',
bitrate: 4000000,
codec: undefined,
original: false,
},
{
label: '480p - 3 Mbps',
bitrate: 3000000,
codec: undefined,
original: false,
},
{
label: '480p - 720 Kbps',
bitrate: 720000,
codec: undefined,
original: false,
},
{
label: '360p - 420 Kbps',
bitrate: 420000,
codec: undefined,
original: false,
},
];
export function getClosestBitrate(qualities, bitrate) {
return qualities.reduce(
(prev, curr) =>
Math.abs(curr.bitrate - bitrate) < Math.abs(prev.bitrate - bitrate)
? curr
: prev,
qualities[0],
);
}
export function formatTicksToTime(ticks: number) {
return formatMinutesToTime(ticks / 10_000_000 / 60);
}
export function formatMinutesToTime(minutes: number) {
const days = Math.floor(minutes / 60 / 24);
const hours = Math.floor((minutes / 60) % 24);
const minutesLeft = Math.floor(minutes % 60);
return `${days > 0 ? days + 'd ' : ''}${hours > 0 ? hours + 'h ' : ''}${
days > 0 ? '' : minutesLeft + 'min'
}`;
}

View File

@@ -40,6 +40,7 @@ export type Quality = {
bitrate: number;
label: string;
codec: string | undefined;
original: boolean;
};
export type Subtitles = {
@@ -49,14 +50,26 @@ export type Subtitles = {
codec: string | undefined;
};
export type VideoStream = {
export type VideoStreamProperty = {
label: string;
value: string | number;
formatted: string | undefined;
};
export type VideoStreamCandidate = {
key: string;
title: string;
properties: VideoStreamProperty[];
};
export type VideoStream = VideoStreamCandidate & {
uri: string;
directPlay: boolean;
progress: number;
audioStreams: AudioStream[];
audioStreamIndex: number;
qualities: Quality[];
quality: number;
qualityIndex: number;
subtitles: Subtitles[];
};
@@ -93,10 +106,24 @@ export interface SourcePlugin {
getMovieStream: (
tmdbId: string,
key: string,
context: UserContext,
config?: PlaybackConfig,
) => Promise<VideoStream>;
getMovieStreams: (
tmdbId: string,
context: UserContext,
config?: PlaybackConfig,
) => Promise<VideoStreamCandidate[]>;
getMovieStream: (
tmdbId: string,
context: UserContext,
key: string,
config?: PlaybackConfig,
) => Promise<VideoStream>;
getEpisodeStream: (
tmdbId: string,
season: number,