From 26f7f281cd1c6e3b0ee596f8c0cda9c098d06267 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Tue, 1 Oct 2024 13:41:40 -0400 Subject: [PATCH] Add playback subscriber abstraction --- .../constants/playbackManagerEvent.ts | 14 +++ .../playback/constants/playerEvent.ts | 23 ++++ .../features/playback/types/callbacks.ts | 33 ++++++ .../features/playback/types/streamInfo.ts | 34 ++++++ .../playback/utils/playbackSubscriber.ts | 101 ++++++++++++++++++ src/components/playback/playbackmanager.js | 2 +- src/types/playTarget.ts | 3 + 7 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 src/apps/stable/features/playback/constants/playbackManagerEvent.ts create mode 100644 src/apps/stable/features/playback/constants/playerEvent.ts create mode 100644 src/apps/stable/features/playback/types/callbacks.ts create mode 100644 src/apps/stable/features/playback/types/streamInfo.ts create mode 100644 src/apps/stable/features/playback/utils/playbackSubscriber.ts diff --git a/src/apps/stable/features/playback/constants/playbackManagerEvent.ts b/src/apps/stable/features/playback/constants/playbackManagerEvent.ts new file mode 100644 index 00000000000..2200c833b5b --- /dev/null +++ b/src/apps/stable/features/playback/constants/playbackManagerEvent.ts @@ -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' +} diff --git a/src/apps/stable/features/playback/constants/playerEvent.ts b/src/apps/stable/features/playback/constants/playerEvent.ts new file mode 100644 index 00000000000..a02a7caaf1c --- /dev/null +++ b/src/apps/stable/features/playback/constants/playerEvent.ts @@ -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' +} diff --git a/src/apps/stable/features/playback/types/callbacks.ts b/src/apps/stable/features/playback/types/callbacks.ts new file mode 100644 index 00000000000..1bede1b2c03 --- /dev/null +++ b/src/apps/stable/features/playback/types/callbacks.ts @@ -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[] +} diff --git a/src/apps/stable/features/playback/types/streamInfo.ts b/src/apps/stable/features/playback/types/streamInfo.ts new file mode 100644 index 00000000000..0fe54210f36 --- /dev/null +++ b/src/apps/stable/features/playback/types/streamInfo.ts @@ -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 +} diff --git a/src/apps/stable/features/playback/utils/playbackSubscriber.ts b/src/apps/stable/features/playback/utils/playbackSubscriber.ts new file mode 100644 index 00000000000..54ff9d94fc7 --- /dev/null +++ b/src/apps/stable/features/playback/utils/playbackSubscriber.ts @@ -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); + }); + } +} diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index b51046d3404..e90aff609fe 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -687,7 +687,7 @@ function sortPlayerTargets(a, b) { return aVal.localeCompare(bVal); } -class PlaybackManager { +export class PlaybackManager { constructor() { const self = this; diff --git a/src/types/playTarget.ts b/src/types/playTarget.ts index 33353137d59..b2f0f6a2846 100644 --- a/src/types/playTarget.ts +++ b/src/types/playTarget.ts @@ -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 }