144 lines
6.8 KiB
TypeScript
144 lines
6.8 KiB
TypeScript
import type { Api } from '@jellyfin/sdk/lib/api';
|
|
import type { MediaSegmentDto } from '@jellyfin/sdk/lib/generated-client/models/media-segment-dto';
|
|
import { MediaSegmentType } from '@jellyfin/sdk/lib/generated-client/models/media-segment-type';
|
|
import { getMediaSegmentsApi } from '@jellyfin/sdk/lib/utils/api/media-segments-api';
|
|
|
|
import type { PlaybackManager } from 'components/playback/playbackmanager';
|
|
import { TICKS_PER_MILLISECOND, TICKS_PER_SECOND } from 'constants/time';
|
|
import { ServerConnections } from 'lib/jellyfin-apiclient';
|
|
import { currentSettings as userSettings } from 'scripts/settings/userSettings';
|
|
import type { PlayerState } from 'types/playbackStopInfo';
|
|
import type { Event } from 'utils/events';
|
|
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
|
|
|
import { getMediaSegmentAction } from './mediaSegmentSettings';
|
|
import { findCurrentSegment } from './mediaSegments';
|
|
import { PlaybackSubscriber } from './playbackSubscriber';
|
|
import { MediaSegmentAction } from '../constants/mediaSegmentAction';
|
|
|
|
class MediaSegmentManager extends PlaybackSubscriber {
|
|
private hasSegments = false;
|
|
private isLastSegmentIgnored = false;
|
|
private lastSegmentIndex = 0;
|
|
private lastTime = -1;
|
|
private mediaSegmentTypeActions: Record<Partial<MediaSegmentType>, MediaSegmentAction> | undefined;
|
|
private mediaSegments: MediaSegmentDto[] = [];
|
|
|
|
private async fetchMediaSegments(api: Api, itemId: string, includeSegmentTypes: MediaSegmentType[]) {
|
|
try {
|
|
const { data: mediaSegments } = await getMediaSegmentsApi(api)
|
|
.getItemSegments({ itemId, includeSegmentTypes });
|
|
this.mediaSegments = mediaSegments.Items || [];
|
|
} catch (err) {
|
|
console.error('[MediaSegmentManager] failed to fetch segments', err);
|
|
this.mediaSegments = [];
|
|
}
|
|
}
|
|
|
|
skipSegment(mediaSegment: MediaSegmentDto) {
|
|
// Ignore segment if playback progress has passed the segment's start time
|
|
if (mediaSegment.StartTicks !== undefined && this.lastTime > mediaSegment.StartTicks) {
|
|
console.info('[MediaSegmentManager] ignoring skipping segment that has been seeked back into', mediaSegment);
|
|
this.isLastSegmentIgnored = true;
|
|
} else if (mediaSegment.EndTicks) {
|
|
// If there is an end time, seek to it
|
|
// Do not skip if duration < 1s to avoid slow stream changes
|
|
if (mediaSegment.StartTicks && mediaSegment.EndTicks - mediaSegment.StartTicks < TICKS_PER_SECOND) {
|
|
console.info('[MediaSegmentManager] ignoring skipping segment with duration <1s', mediaSegment);
|
|
this.isLastSegmentIgnored = true;
|
|
return;
|
|
}
|
|
console.debug('[MediaSegmentManager] skipping to %s ms', mediaSegment.EndTicks / TICKS_PER_MILLISECOND);
|
|
this.playbackManager.seek(mediaSegment.EndTicks, this.player);
|
|
} else {
|
|
// If there is no end time, skip to the next track
|
|
console.debug('[MediaSegmentManager] skipping to next item in queue');
|
|
this.playbackManager.nextTrack(this.player);
|
|
}
|
|
}
|
|
|
|
promptToSkip(mediaSegment: MediaSegmentDto) {
|
|
if (mediaSegment.StartTicks && mediaSegment.EndTicks
|
|
&& mediaSegment.EndTicks - mediaSegment.StartTicks < TICKS_PER_SECOND * 3) {
|
|
console.info('[MediaSegmentManager] ignoring segment prompt with duration <3s', mediaSegment);
|
|
this.isLastSegmentIgnored = true;
|
|
return;
|
|
}
|
|
this.playbackManager.promptToSkip(mediaSegment);
|
|
}
|
|
|
|
private performAction(mediaSegment: MediaSegmentDto) {
|
|
if (!this.mediaSegmentTypeActions || !mediaSegment.Type || !this.mediaSegmentTypeActions[mediaSegment.Type]) {
|
|
console.error('[MediaSegmentManager] segment type missing from action map', mediaSegment, this.mediaSegmentTypeActions);
|
|
return;
|
|
}
|
|
|
|
const action = this.mediaSegmentTypeActions[mediaSegment.Type];
|
|
if (action === MediaSegmentAction.Skip) {
|
|
this.skipSegment(mediaSegment);
|
|
} else if (action === MediaSegmentAction.AskToSkip) {
|
|
this.promptToSkip(mediaSegment);
|
|
}
|
|
}
|
|
|
|
onPlayerPlaybackStart(_e: Event, state: PlayerState) {
|
|
this.isLastSegmentIgnored = false;
|
|
this.lastSegmentIndex = 0;
|
|
this.lastTime = -1;
|
|
this.hasSegments = !!state.MediaSource?.HasSegments;
|
|
|
|
const itemId = state.MediaSource?.Id;
|
|
const serverId = state.NowPlayingItem?.ServerId || ServerConnections.currentApiClient()?.serverId();
|
|
|
|
if (!this.hasSegments || !serverId || !itemId) return;
|
|
|
|
// Get the user settings for media segment actions
|
|
this.mediaSegmentTypeActions = Object.values(MediaSegmentType)
|
|
.map(type => ({
|
|
type,
|
|
action: getMediaSegmentAction(userSettings, type)
|
|
}))
|
|
.filter(({ action }) => !!action && action !== MediaSegmentAction.None)
|
|
.reduce((acc, { type, action }) => {
|
|
if (action) acc[type] = action;
|
|
return acc;
|
|
}, {} as Record<Partial<MediaSegmentType>, MediaSegmentAction>);
|
|
|
|
if (!Object.keys(this.mediaSegmentTypeActions).length) {
|
|
console.info('[MediaSegmentManager] user has no media segment actions enabled');
|
|
return;
|
|
}
|
|
|
|
const api = toApi(ServerConnections.getApiClient(serverId));
|
|
void this.fetchMediaSegments(
|
|
api,
|
|
itemId,
|
|
Object.keys(this.mediaSegmentTypeActions).map(t => t as keyof typeof MediaSegmentType));
|
|
}
|
|
|
|
onPlayerTimeUpdate() {
|
|
if (this.hasSegments && this.mediaSegments.length) {
|
|
const time = this.playbackManager.currentTime(this.player) * TICKS_PER_MILLISECOND;
|
|
const currentSegmentDetails = findCurrentSegment(this.mediaSegments, time, this.lastSegmentIndex);
|
|
if (
|
|
// The current time falls within a segment
|
|
currentSegmentDetails
|
|
// and the last segment is not ignored or the segment index has changed
|
|
&& (!this.isLastSegmentIgnored || this.lastSegmentIndex !== currentSegmentDetails.index)
|
|
) {
|
|
console.debug(
|
|
'[MediaSegmentManager] found %s segment at %s ms',
|
|
currentSegmentDetails.segment.Type,
|
|
time / TICKS_PER_MILLISECOND,
|
|
currentSegmentDetails);
|
|
this.isLastSegmentIgnored = false;
|
|
this.performAction(currentSegmentDetails.segment);
|
|
this.lastSegmentIndex = currentSegmentDetails.index;
|
|
}
|
|
this.lastTime = time;
|
|
}
|
|
}
|
|
}
|
|
|
|
export const bindMediaSegmentManager = (playbackManager: PlaybackManager) => new MediaSegmentManager(playbackManager);
|