Add playback subscriber abstraction

This commit is contained in:
Bill Thornton 2024-10-01 13:41:40 -04:00
parent 2442dc6b52
commit 26f7f281cd
7 changed files with 209 additions and 1 deletions

View File

@ -0,0 +1,14 @@
/**
* Events triggered by PlaybackManager.
*/
export enum PlaybackManagerEvent {
Pairing = 'pairing',
Paired = 'paired',
PairError = 'pairerror',
PlaybackCancelled = 'playbackcancelled',
PlaybackError = 'playbackerror',
PlaybackStart = 'playbackstart',
PlaybackStop = 'playbackstop',
PlayerChange = 'playerchange',
ReportPlayback = 'reportplayback'
}

View File

@ -0,0 +1,23 @@
/**
* Events triggered by media player plugins.
* NOTE: This list is incomplete
*/
export enum PlayerEvent {
Error = 'error',
FullscreenChange = 'fullscreenchange',
ItemStarted = 'itemstarted',
ItemStopped = 'itemstopped',
MediaStreamsChange = 'mediastreamschange',
Pause = 'pause',
PlaybackStart = 'playbackstart',
PlaybackStop = 'playbackstop',
PlaylistItemAdd = 'playlistitemadd',
PlaylistItemMove = 'playlistitemmove',
PlaylistItemRemove = 'playlistitemremove',
RepeatModeChange = 'repeatmodechange',
ShuffleModeChange = 'shufflequeuemodechange',
Stopped = 'stopped',
TimeUpdate = 'timeupdate',
Unpause = 'unpause',
VolumeChange = 'volumechange'
}

View File

@ -0,0 +1,33 @@
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
import type { MediaSourceInfo } from '@jellyfin/sdk/lib/generated-client/models/media-source-info';
import type { MediaType } from '@jellyfin/sdk/lib/generated-client/models/media-type';
import type { StreamInfo } from './streamInfo';
export interface ManagedPlayerStopInfo {
item: BaseItemDto
mediaSource: MediaSourceInfo
nextItem?: BaseItemDto | null
nextMediaType?: MediaType | null
positionMs?: number
}
export interface MovedItem {
newIndex: number
playlistItemId: string
}
export type PlayerErrorCode = string;
export interface PlayerStopInfo {
src?: URL | BaseItemDto
}
export interface PlayerError {
streamInfo?: StreamInfo
type: MediaError | string
}
export interface RemovedItems {
playlistItemIds: string[]
}

View File

@ -0,0 +1,34 @@
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
import type { MediaSourceInfo } from '@jellyfin/sdk/lib/generated-client/models/media-source-info';
import type { MediaType } from '@jellyfin/sdk/lib/generated-client/models/media-type';
import type { PlayMethod } from '@jellyfin/sdk/lib/generated-client/models/play-method';
export interface StreamInfo {
ended?: boolean
fullscreen?: boolean
item?: BaseItemDto
lastMediaInfoQuery?: number
liveStreamId?: string
mediaSource?: MediaSourceInfo
mediaType?: MediaType
mimeType?: string
playMethod?: PlayMethod
playSessionId?: string
playbackStartTimeTicks?: number
playerStartPositionTicks?: number
resetSubtitleOffset?: boolean
started?: boolean
textTracks?: TrackInfo[]
title?: string
tracks?: TrackInfo[]
transcodingOffsetTicks?: number
url?: string
}
interface TrackInfo {
url: string
language: string
isDefault: boolean
index: number
format: string
}

View File

@ -0,0 +1,101 @@
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
import type { MediaSourceInfo } from '@jellyfin/sdk/lib/generated-client/models/media-source-info';
import type { PlaybackManager } from 'components/playback/playbackmanager';
import type { MediaError } from 'types/mediaError';
import type { PlayTarget } from 'types/playTarget';
import type { PlaybackStopInfo, PlayerState } from 'types/playbackStopInfo';
import type { Plugin } from 'types/plugin';
import Events, { type Event } from 'utils/events';
import { PlaybackManagerEvent } from '../constants/playbackManagerEvent';
import { PlayerEvent } from '../constants/playerEvent';
import type { ManagedPlayerStopInfo, MovedItem, PlayerError, PlayerErrorCode, PlayerStopInfo, RemovedItems } from '../types/callbacks';
export interface PlaybackSubscriber {
onPlaybackCancelled?(e: Event): void
onPlaybackError?(e: Event, errorType: MediaError): void
onPlaybackStart?(e: Event, player: Plugin, state: PlayerState): void
onPlaybackStop?(e: Event, info: PlaybackStopInfo): void
onPlayerChange?(e: Event, player: Plugin, target: PlayTarget, previousPlayer: Plugin): void
onPlayerError?(e: Event, error: PlayerError): void
onPlayerFullscreenChange?(e: Event): void
onPlayerItemStarted?(e: Event, item?: BaseItemDto, mediaSource?: MediaSourceInfo): void
onPlayerItemStopped?(e: Event, info: ManagedPlayerStopInfo): void
onPlayerMediaStreamsChange?(e: Event): void
onPlayerPause?(e: Event): void
onPlayerPlaybackStart?(e: Event, state: PlayerState): void
onPlayerPlaybackStop?(e: Event, state: PlayerState): void
onPlayerPlaylistItemAdd?(e: Event): void
onPlayerPlaylistItemMove?(e: Event, item: MovedItem): void
onPlayerPlaylistItemRemove?(e: Event, items?: RemovedItems): void
onPlayerRepeatModeChange?(e: Event): void
onPlayerShuffleModeChange?(e: Event): void
onPlayerStopped?(e: Event, info?: PlayerStopInfo | PlayerErrorCode): void
onPlayerTimeUpdate?(e: Event): void
onPlayerUnpause?(e: Event): void
onPlayerVolumeChange?(e: Event): void
onReportPlayback?(e: Event, isServerItem: boolean): void
}
export abstract class PlaybackSubscriber {
private player: Plugin | undefined;
private playbackManagerEvents = {
[PlaybackManagerEvent.PlaybackCancelled]: this.onPlaybackCancelled,
[PlaybackManagerEvent.PlaybackError]: this.onPlaybackError,
[PlaybackManagerEvent.PlaybackStart]: this.onPlaybackStart,
[PlaybackManagerEvent.PlaybackStop]: this.onPlaybackStop,
[PlaybackManagerEvent.PlayerChange]: this.onPlayerChange,
[PlaybackManagerEvent.ReportPlayback]: this.onReportPlayback
};
private playerEvents = {
[PlayerEvent.Error]: this.onPlayerError,
[PlayerEvent.FullscreenChange]: this.onPlayerFullscreenChange,
[PlayerEvent.ItemStarted]: this.onPlayerItemStarted,
[PlayerEvent.ItemStopped]: this.onPlayerItemStopped,
[PlayerEvent.MediaStreamsChange]: this.onPlayerMediaStreamsChange,
[PlayerEvent.Pause]: this.onPlayerPause,
[PlayerEvent.PlaybackStart]: this.onPlayerPlaybackStart,
[PlayerEvent.PlaybackStop]: this.onPlayerPlaybackStop,
[PlayerEvent.PlaylistItemAdd]: this.onPlayerPlaylistItemAdd,
[PlayerEvent.PlaylistItemMove]: this.onPlayerPlaylistItemMove,
[PlayerEvent.PlaylistItemRemove]: this.onPlayerPlaylistItemRemove,
[PlayerEvent.RepeatModeChange]: this.onPlayerRepeatModeChange,
[PlayerEvent.ShuffleModeChange]: this.onPlayerShuffleModeChange,
[PlayerEvent.Stopped]: this.onPlayerStopped,
[PlayerEvent.TimeUpdate]: this.onPlayerTimeUpdate,
[PlayerEvent.Unpause]: this.onPlayerUnpause,
[PlayerEvent.VolumeChange]: this.onPlayerVolumeChange
};
constructor(
protected readonly playbackManager: PlaybackManager
) {
Object.entries(this.playbackManagerEvents).forEach(([event, handler]) => {
if (handler) Events.on(playbackManager, event, handler);
});
this.bindPlayerEvents();
Events.on(playbackManager, PlaybackManagerEvent.PlayerChange, this.bindPlayerEvents.bind(this));
}
private bindPlayerEvents() {
const newPlayer = this.playbackManager.getCurrentPlayer();
if (this.player === newPlayer) return;
if (this.player) {
Object.entries(this.playerEvents).forEach(([event, handler]) => {
if (handler) Events.off(this.player, event, handler);
});
}
this.player = newPlayer;
if (!this.player) return;
Object.entries(this.playerEvents).forEach(([event, handler]) => {
if (handler) Events.on(this.player, event, handler);
});
}
}

View File

@ -687,7 +687,7 @@ function sortPlayerTargets(a, b) {
return aVal.localeCompare(bVal);
}
class PlaybackManager {
export class PlaybackManager {
constructor() {
const self = this;

View File

@ -1,3 +1,4 @@
import type { MediaType } from '@jellyfin/sdk/lib/generated-client/models/media-type';
import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto';
export interface PlayTarget {
@ -7,5 +8,7 @@ export interface PlayTarget {
playerName?: string
deviceType?: string
isLocalPlayer?: boolean
playableMediaTypes: MediaType[]
supportedCommands?: string[]
user?: UserDto
}