Skip to content

Commit

Permalink
Add playback subscriber abstraction
Browse files Browse the repository at this point in the history
  • Loading branch information
thornbill committed Oct 1, 2024
1 parent 2442dc6 commit 26f7f28
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -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'
}
23 changes: 23 additions & 0 deletions src/apps/stable/features/playback/constants/playerEvent.ts
Original file line number Diff line number Diff line change
@@ -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'
}
33 changes: 33 additions & 0 deletions src/apps/stable/features/playback/types/callbacks.ts
Original file line number Diff line number Diff line change
@@ -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[]
}
34 changes: 34 additions & 0 deletions src/apps/stable/features/playback/types/streamInfo.ts
Original file line number Diff line number Diff line change
@@ -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
}
101 changes: 101 additions & 0 deletions src/apps/stable/features/playback/utils/playbackSubscriber.ts
Original file line number Diff line number Diff line change
@@ -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);
});
}
}
2 changes: 1 addition & 1 deletion src/components/playback/playbackmanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,7 @@ function sortPlayerTargets(a, b) {
return aVal.localeCompare(bVal);
}

class PlaybackManager {
export class PlaybackManager {
constructor() {
const self = this;

Expand Down
3 changes: 3 additions & 0 deletions src/types/playTarget.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -7,5 +8,7 @@ export interface PlayTarget {
playerName?: string
deviceType?: string
isLocalPlayer?: boolean
playableMediaTypes: MediaType[]
supportedCommands?: string[]
user?: UserDto
}

0 comments on commit 26f7f28

Please sign in to comment.