diff --git a/src/apps/stable/features/playback/constants/playerEvent.ts b/src/apps/stable/features/playback/constants/playerEvent.ts index ad9d558a13..b3478ed9d7 100644 --- a/src/apps/stable/features/playback/constants/playerEvent.ts +++ b/src/apps/stable/features/playback/constants/playerEvent.ts @@ -17,6 +17,7 @@ export enum PlayerEvent { PromptSkip = 'promptskip', RepeatModeChange = 'repeatmodechange', ShuffleModeChange = 'shufflequeuemodechange', + StateChange = 'statechange', Stopped = 'stopped', TimeUpdate = 'timeupdate', Unpause = 'unpause', diff --git a/src/apps/stable/features/playback/utils/image.ts b/src/apps/stable/features/playback/utils/image.ts new file mode 100644 index 0000000000..a892284d8e --- /dev/null +++ b/src/apps/stable/features/playback/utils/image.ts @@ -0,0 +1,59 @@ +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type'; + +import ServerConnections from 'components/ServerConnections'; +import type { ItemDto } from 'types/base/models/item-dto'; + +interface ImageOptions { + height?: number + maxHeight?: number + tag?: string + type?: ImageType +} + +function getSeriesImageUrl(item: ItemDto, options: ImageOptions = {}) { + if (!item.ServerId) return null; + + if (item.SeriesId && options.type === ImageType.Primary && item.SeriesPrimaryImageTag) { + options.tag = item.SeriesPrimaryImageTag; + + return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.SeriesId, options); + } + + if (options.type === ImageType.Thumb) { + if (item.SeriesId && item.SeriesThumbImageTag) { + options.tag = item.SeriesThumbImageTag; + + return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.SeriesId, options); + } + if (item.ParentThumbItemId && item.ParentThumbImageTag) { + options.tag = item.ParentThumbImageTag; + + return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.ParentThumbItemId, options); + } + } + + return null; +} + +export function getImageUrl(item: ItemDto, options: ImageOptions = {}) { + if (!item.ServerId) return null; + + options.type = options.type || ImageType.Primary; + + if (item.Type === BaseItemKind.Episode) return getSeriesImageUrl(item, options); + + const itemId = item.PrimaryImageItemId || item.Id; + + if (itemId && item.ImageTags?.[options.type]) { + options.tag = item.ImageTags[options.type]; + return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(itemId, options); + } + + if (item.AlbumId && item.AlbumPrimaryImageTag) { + options.tag = item.AlbumPrimaryImageTag; + return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.AlbumId, options); + } + + return null; +} diff --git a/src/apps/stable/features/playback/utils/mediaSessionSubscriber.ts b/src/apps/stable/features/playback/utils/mediaSessionSubscriber.ts new file mode 100644 index 0000000000..e607f6cc8a --- /dev/null +++ b/src/apps/stable/features/playback/utils/mediaSessionSubscriber.ts @@ -0,0 +1,178 @@ +import { MediaType } from '@jellyfin/sdk/lib/generated-client/models/media-type'; + +import { getImageUrl } from 'apps/stable/features/playback/utils/image'; +import { PlaybackSubscriber } from 'apps/stable/features/playback/utils/playbackSubscriber'; +import nowplayinghelper from 'components/playback/nowplayinghelper'; +import type { PlaybackManager } from 'components/playback/playbackmanager'; +import { MILLISECONDS_PER_SECOND, TICKS_PER_MILLISECOND } from 'constants/time'; +import browser from 'scripts/browser'; +import shell from 'scripts/shell'; +import type { ItemDto } from 'types/base/models/item-dto'; +import type { PlayerState } from 'types/playbackStopInfo'; +import type { Event } from 'utils/events'; + +/** The default image resolutions to provide to the media session */ +const DEFAULT_IMAGE_SIZES = [96, 128, 192, 256, 384, 512]; + +const hasNavigatorSession = 'mediaSession' in navigator; +const hasNativeShell = !!window.NativeShell; + +const getArtwork = (item: ItemDto): MediaImage[] => { + const artwork: MediaImage[] = []; + + DEFAULT_IMAGE_SIZES.forEach(height => { + const src = getImageUrl(item, { height }); + if (src) { + artwork.push({ + src, + sizes: `${height}x${height}` + }); + } + }); + + return artwork; +}; + +const resetMediaSession = () => { + if (hasNavigatorSession) { + navigator.mediaSession.metadata = null; + } else if (hasNativeShell) { + shell.hideMediaSession(); + } +}; + +/** A PlaybackSubscriber that manages MediaSession state and events. */ +class MediaSessionSubscriber extends PlaybackSubscriber { + constructor(playbackManager: PlaybackManager) { + super(playbackManager); + + resetMediaSession(); + if (hasNavigatorSession) this.bindNavigatorSession(); + } + + private bindNavigatorSession() { + /* eslint-disable compat/compat */ + navigator.mediaSession.setActionHandler('pause', this.onMediaSessionAction.bind(this)); + navigator.mediaSession.setActionHandler('play', this.onMediaSessionAction.bind(this)); + navigator.mediaSession.setActionHandler('stop', this.onMediaSessionAction.bind(this)); + navigator.mediaSession.setActionHandler('previoustrack', this.onMediaSessionAction.bind(this)); + navigator.mediaSession.setActionHandler('nexttrack', this.onMediaSessionAction.bind(this)); + navigator.mediaSession.setActionHandler('seekto', this.onMediaSessionAction.bind(this)); + // iOS will only show next/prev track controls or seek controls + if (!browser.iOS) { + navigator.mediaSession.setActionHandler('seekbackward', this.onMediaSessionAction.bind(this)); + navigator.mediaSession.setActionHandler('seekforward', this.onMediaSessionAction.bind(this)); + } + /* eslint-enable compat/compat */ + } + + private onMediaSessionAction(details: MediaSessionActionDetails) { + switch (details.action) { + case 'pause': + return this.playbackManager.pause(this.player); + case 'play': + return this.playbackManager.unpause(this.player); + case 'stop': + return this.playbackManager.stop(this.player); + case 'seekbackward': + return this.playbackManager.rewind(this.player); + case 'seekforward': + return this.playbackManager.fastForward(this.player); + case 'seekto': + return this.playbackManager.seekMs((details.seekTime || 0) * MILLISECONDS_PER_SECOND, this.player); + case 'previoustrack': + return this.playbackManager.previousTrack(this.player); + case 'nexttrack': + return this.playbackManager.nextTrack(this.player); + default: + console.info('[MediaSessionSubscriber] Unhandled media session action', details); + } + } + + private onMediaSessionUpdate( + { type: action }: Event, + state: PlayerState = this.playbackManager.getPlayerState(this.player) + ) { + const item = state.NowPlayingItem; + + if (!item) { + console.debug('[MediaSessionSubscriber] no now playing item; resetting media session', state); + return resetMediaSession(); + } + + const isVideo = item.MediaType === MediaType.Video; + const isLocalPlayer = !!this.player?.isLocalPlayer; + + // Local players do their own notifications + if (isLocalPlayer && isVideo) { + console.debug('[MediaSessionSubscriber] ignoring local player update'); + return; + } + + const album = item.Album || undefined; + const names = nowplayinghelper.getNowPlayingNames(item); + const artist = names[names.length - 1].text; + const title = names.length === 1 ? undefined : names[0].text; + + if (hasNavigatorSession) { + if ( + !navigator.mediaSession.metadata + || navigator.mediaSession.metadata.album !== album + || navigator.mediaSession.metadata.artist !== artist + || navigator.mediaSession.metadata.title !== title + ) { + navigator.mediaSession.metadata = new MediaMetadata({ + title, + artist, + album, + artwork: getArtwork(item) + }); + } + } else { + shell.updateMediaSession({ + action, + isLocalPlayer, + itemId: item.Id, + title, + artist, + album, + duration: item.RunTimeTicks ? Math.round(item.RunTimeTicks / TICKS_PER_MILLISECOND) : 0, + position: state.PlayState.PositionTicks ? Math.round(state.PlayState.PositionTicks / TICKS_PER_MILLISECOND) : 0, + imageUrl: getImageUrl(item, { maxHeight: 3_000 }), + canSeek: !!state.PlayState.CanSeek, + isPaused: !!state.PlayState.IsPaused + }); + } + } + + onPlayerChange() { + this.onMediaSessionUpdate({ type: 'timeupdate' }); + } + + onPlayerPause(e: Event) { + this.onMediaSessionUpdate(e); + } + + onPlayerPlaybackStart(e: Event, state: PlayerState) { + this.onMediaSessionUpdate(e, state); + } + + onPlayerPlaybackStop() { + resetMediaSession(); + } + + onPlayerStateChange(e: Event, state: PlayerState) { + this.onMediaSessionUpdate(e, state); + } + + onPlayerUnpause(e: Event) { + this.onMediaSessionUpdate(e); + } +} + +/** Bind a new MediaSessionSubscriber to the specified PlaybackManager */ +export const bindMediaSessionSubscriber = (playbackManager: PlaybackManager) => { + if (hasNativeShell || hasNavigatorSession) { + return new MediaSessionSubscriber(playbackManager); + } +}; diff --git a/src/apps/stable/features/playback/utils/playbackSubscriber.ts b/src/apps/stable/features/playback/utils/playbackSubscriber.ts index 830d3bc8ee..9a800e7648 100644 --- a/src/apps/stable/features/playback/utils/playbackSubscriber.ts +++ b/src/apps/stable/features/playback/utils/playbackSubscriber.ts @@ -1,24 +1,23 @@ import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'; +import type { MediaSegmentDto } from '@jellyfin/sdk/lib/generated-client/models/media-segment-dto'; import type { MediaSourceInfo } from '@jellyfin/sdk/lib/generated-client/models/media-source-info'; +import { PlaybackManagerEvent } from 'apps/stable/features/playback/constants/playbackManagerEvent'; +import { PlayerEvent } from 'apps/stable/features/playback/constants/playerEvent'; +import type { ManagedPlayerStopInfo, MovedItem, PlayerError, PlayerErrorCode, PlayerStopInfo, RemovedItems } from 'apps/stable/features/playback/types/callbacks'; 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 type { PlayerPlugin } 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'; -import type { MediaSegmentDto } from '@jellyfin/sdk/lib/generated-client/models/media-segment-dto'; - export interface PlaybackSubscriber { onPlaybackCancelled?(e: Event): void onPlaybackError?(e: Event, errorType: MediaError): void - onPlaybackStart?(e: Event, player: Plugin, state: PlayerState): void + onPlaybackStart?(e: Event, player: PlayerPlugin, state: PlayerState): void onPlaybackStop?(e: Event, info: PlaybackStopInfo): void - onPlayerChange?(e: Event, player: Plugin, target: PlayTarget, previousPlayer: Plugin): void + onPlayerChange?(e: Event, player: PlayerPlugin, target: PlayTarget, previousPlayer: PlayerPlugin): void onPromptSkip?(e: Event, mediaSegment: MediaSegmentDto): void onPlayerError?(e: Event, error: PlayerError): void onPlayerFullscreenChange?(e: Event): void @@ -34,6 +33,7 @@ export interface PlaybackSubscriber { onPlayerRepeatModeChange?(e: Event): void onPlayerShuffleModeChange?(e: Event): void onPlayerStopped?(e: Event, info?: PlayerStopInfo | PlayerErrorCode): void + onPlayerStateChange?(e: Event, state: PlayerState): void onPlayerTimeUpdate?(e: Event): void onPlayerUnpause?(e: Event): void onPlayerVolumeChange?(e: Event): void @@ -41,7 +41,7 @@ export interface PlaybackSubscriber { } export abstract class PlaybackSubscriber { - protected player: Plugin | undefined; + protected player: PlayerPlugin | undefined; private readonly playbackManagerEvents = { [PlaybackManagerEvent.PlaybackCancelled]: this.onPlaybackCancelled?.bind(this), @@ -67,6 +67,7 @@ export abstract class PlaybackSubscriber { [PlayerEvent.PromptSkip]: this.onPromptSkip?.bind(this), [PlayerEvent.RepeatModeChange]: this.onPlayerRepeatModeChange?.bind(this), [PlayerEvent.ShuffleModeChange]: this.onPlayerShuffleModeChange?.bind(this), + [PlayerEvent.StateChange]: this.onPlayerStateChange?.bind(this), [PlayerEvent.Stopped]: this.onPlayerStopped?.bind(this), [PlayerEvent.TimeUpdate]: this.onPlayerTimeUpdate?.bind(this), [PlayerEvent.Unpause]: this.onPlayerUnpause?.bind(this), diff --git a/src/components/nowPlayingBar/nowPlayingBar.js b/src/components/nowPlayingBar/nowPlayingBar.js index 9c3bdaca40..3e205651b2 100644 --- a/src/components/nowPlayingBar/nowPlayingBar.js +++ b/src/components/nowPlayingBar/nowPlayingBar.js @@ -17,6 +17,7 @@ import appFooter from '../appFooter/appFooter'; import itemShortcuts from '../shortcuts'; import './nowPlayingBar.scss'; import '../../elements/emby-slider/emby-slider'; +import { getImageUrl } from 'apps/stable/features/playback/utils/image'; let currentPlayer; let currentPlayerSupportedCommands = []; @@ -470,61 +471,6 @@ function setLyricButtonActiveStatus() { lyricButton.classList.toggle('buttonActive', isLyricPageActive); } -function seriesImageUrl(item, options) { - if (!item) { - throw new Error('item cannot be null!'); - } - - if (item.Type !== 'Episode') { - return null; - } - - options = options || {}; - options.type = options.type || 'Primary'; - - if (options.type === 'Primary' && item.SeriesPrimaryImageTag) { - options.tag = item.SeriesPrimaryImageTag; - - return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.SeriesId, options); - } - - if (options.type === 'Thumb') { - if (item.SeriesThumbImageTag) { - options.tag = item.SeriesThumbImageTag; - - return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.SeriesId, options); - } - if (item.ParentThumbImageTag) { - options.tag = item.ParentThumbImageTag; - - return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.ParentThumbItemId, options); - } - } - - return null; -} - -function imageUrl(item, options) { - if (!item) { - throw new Error('item cannot be null!'); - } - - options = options || {}; - options.type = options.type || 'Primary'; - - if (item.ImageTags?.[options.type]) { - options.tag = item.ImageTags[options.type]; - return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.PrimaryImageItemId || item.Id, options); - } - - if (item.AlbumId && item.AlbumPrimaryImageTag) { - options.tag = item.AlbumPrimaryImageTag; - return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.AlbumId, options); - } - - return null; -} - function updateNowPlayingInfo(state) { const nowPlayingItem = state.NowPlayingItem; @@ -554,11 +500,9 @@ function updateNowPlayingInfo(state) { const imgHeight = 70; - const url = nowPlayingItem ? (seriesImageUrl(nowPlayingItem, { + const url = nowPlayingItem ? getImageUrl(nowPlayingItem, { height: imgHeight - }) || imageUrl(nowPlayingItem, { - height: imgHeight - })) : null; + }) : null; if (url !== nowPlayingImageUrl) { if (url) { diff --git a/src/components/playback/mediasession.js b/src/components/playback/mediasession.js deleted file mode 100644 index d7d2dcabbe..0000000000 --- a/src/components/playback/mediasession.js +++ /dev/null @@ -1,259 +0,0 @@ -import { playbackManager } from '../playback/playbackmanager'; -import nowPlayingHelper from '../playback/nowplayinghelper'; -import Events from '../../utils/events.ts'; -import ServerConnections from '../ServerConnections'; -import shell from '../../scripts/shell'; - -// Reports media playback to the device for lock screen control - -let currentPlayer; - -function seriesImageUrl(item, options = {}) { - options.type = options.type || 'Primary'; - - if (item.Type !== 'Episode') { - return null; - } else if (options.type === 'Primary' && item.SeriesPrimaryImageTag) { - options.tag = item.SeriesPrimaryImageTag; - - return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.SeriesId, options); - } else if (options.type === 'Thumb') { - if (item.SeriesThumbImageTag) { - options.tag = item.SeriesThumbImageTag; - - return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.SeriesId, options); - } else if (item.ParentThumbImageTag) { - options.tag = item.ParentThumbImageTag; - - return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.ParentThumbItemId, options); - } - } - - return null; -} - -function imageUrl(item, options = {}) { - options.type = options.type || 'Primary'; - - if (item.ImageTags?.[options.type]) { - options.tag = item.ImageTags[options.type]; - - return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.Id, options); - } else if (item.AlbumId && item.AlbumPrimaryImageTag) { - options.tag = item.AlbumPrimaryImageTag; - - return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.AlbumId, options); - } - - return null; -} - -function getImageUrl(item, imageOptions = {}) { - const url = seriesImageUrl(item, imageOptions) || imageUrl(item, imageOptions); - - if (url) { - const height = imageOptions.height || imageOptions.maxHeight; - - return { - src: url, - sizes: height + 'x' + height - }; - } else { - return null; - } -} - -function getImageUrls(item, imageSizes = [96, 128, 192, 256, 384, 512]) { - const list = []; - - imageSizes.forEach((size) => { - const url = getImageUrl(item, { height: size }); - if (url !== null) { - list.push(url); - } - }); - - return list; -} - -function updatePlayerState(player, state, eventName) { - // Don't go crazy reporting position changes - if (eventName === 'timeupdate') { - // Only report if this item hasn't been reported yet, or if there's an actual playback change. - // Don't report on simple time updates - return; - } - - const item = state.NowPlayingItem; - - if (!item) { - hideMediaControls(); - return; - } - - if (eventName === 'init') { // transform "init" event into "timeupdate" to restraint update rate - eventName = 'timeupdate'; - } - - const isVideo = item.MediaType === 'Video'; - const isLocalPlayer = player.isLocalPlayer || false; - - // Local players do their own notifications - if (isLocalPlayer && isVideo) { - return; - } - - const playState = state.PlayState || {}; - const parts = nowPlayingHelper.getNowPlayingNames(item); - const artist = parts[parts.length - 1].text; - const title = parts.length === 1 ? '' : parts[0].text; - - const album = item.Album || ''; - const itemId = item.Id; - - // Convert to ms - const duration = parseInt(item.RunTimeTicks ? (item.RunTimeTicks / 10000) : 0, 10); - const currentTime = parseInt(playState.PositionTicks ? (playState.PositionTicks / 10000) : 0, 10); - - const isPaused = playState.IsPaused || false; - const canSeek = playState.CanSeek || false; - - if ('mediaSession' in navigator) { - /* eslint-disable-next-line compat/compat */ - navigator.mediaSession.metadata = new MediaMetadata({ - title: title, - artist: artist, - album: album, - artwork: getImageUrls(item) - }); - } else { - const itemImageUrl = seriesImageUrl(item, { maxHeight: 3000 }) || imageUrl(item, { maxHeight: 3000 }); - shell.updateMediaSession({ - action: eventName, - isLocalPlayer: isLocalPlayer, - itemId: itemId, - title: title, - artist: artist, - album: album, - duration: duration, - position: currentTime, - imageUrl: itemImageUrl, - canSeek: canSeek, - isPaused: isPaused - }); - } -} - -function onGeneralEvent(e) { - const state = playbackManager.getPlayerState(this); - - updatePlayerState(this, state, e.type); -} - -function onStateChanged(e, state) { - updatePlayerState(this, state, 'statechange'); -} - -function onPlaybackStart(e, state) { - updatePlayerState(this, state, e.type); -} - -function onPlaybackStopped() { - hideMediaControls(); -} - -function releaseCurrentPlayer() { - if (currentPlayer) { - Events.off(currentPlayer, 'playbackstart', onPlaybackStart); - Events.off(currentPlayer, 'playbackstop', onPlaybackStopped); - Events.off(currentPlayer, 'unpause', onGeneralEvent); - Events.off(currentPlayer, 'pause', onGeneralEvent); - Events.off(currentPlayer, 'statechange', onStateChanged); - Events.off(currentPlayer, 'timeupdate', onGeneralEvent); - - currentPlayer = null; - - hideMediaControls(); - } -} - -function hideMediaControls() { - if ('mediaSession' in navigator) { - /* eslint-disable-next-line compat/compat */ - navigator.mediaSession.metadata = null; - } else { - shell.hideMediaSession(); - } -} - -function bindToPlayer(player) { - releaseCurrentPlayer(); - - if (!player) { - return; - } - - currentPlayer = player; - - const state = playbackManager.getPlayerState(player); - updatePlayerState(player, state, 'init'); - - Events.on(currentPlayer, 'playbackstart', onPlaybackStart); - Events.on(currentPlayer, 'playbackstop', onPlaybackStopped); - Events.on(currentPlayer, 'unpause', onGeneralEvent); - Events.on(currentPlayer, 'pause', onGeneralEvent); - Events.on(currentPlayer, 'statechange', onStateChanged); - Events.on(currentPlayer, 'timeupdate', onGeneralEvent); -} - -function execute(name) { - playbackManager[name](currentPlayer); -} - -if ('mediaSession' in navigator) { - /* eslint-disable-next-line compat/compat */ - navigator.mediaSession.setActionHandler('previoustrack', function () { - execute('previousTrack'); - }); - - /* eslint-disable-next-line compat/compat */ - navigator.mediaSession.setActionHandler('nexttrack', function () { - execute('nextTrack'); - }); - - /* eslint-disable-next-line compat/compat */ - navigator.mediaSession.setActionHandler('play', function () { - execute('unpause'); - }); - - /* eslint-disable-next-line compat/compat */ - navigator.mediaSession.setActionHandler('pause', function () { - execute('pause'); - }); - - /* eslint-disable-next-line compat/compat */ - navigator.mediaSession.setActionHandler('seekbackward', function () { - execute('rewind'); - }); - - /* eslint-disable-next-line compat/compat */ - navigator.mediaSession.setActionHandler('seekforward', function () { - execute('fastForward'); - }); - - /* eslint-disable-next-line compat/compat */ - navigator.mediaSession.setActionHandler('seekto', function (object) { - const item = playbackManager.getPlayerState(currentPlayer).NowPlayingItem; - // Convert to ms - const duration = parseInt(item.RunTimeTicks ? (item.RunTimeTicks / 10000) : 0, 10); - const wantedTime = object.seekTime * 1000; - playbackManager.seekPercent(wantedTime / duration * 100, currentPlayer); - }); -} - -Events.on(playbackManager, 'playerchange', function () { - bindToPlayer(playbackManager.getCurrentPlayer()); -}); - -bindToPlayer(playbackManager.getCurrentPlayer()); - diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index 86acdff7a6..d49981076d 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -21,8 +21,9 @@ import { includesAny } from '../../utils/container.ts'; import { getItems } from '../../utils/jellyfin-apiclient/getItems.ts'; import { getItemBackdropImageUrl } from '../../utils/jellyfin-apiclient/backdropImage'; -import { bindMediaSegmentManager } from 'apps/stable/features/playback/utils/mediaSegmentManager'; import { PlayerEvent } from 'apps/stable/features/playback/constants/playerEvent'; +import { bindMediaSegmentManager } from 'apps/stable/features/playback/utils/mediaSegmentManager'; +import { bindMediaSessionSubscriber } from 'apps/stable/features/playback/utils/mediaSessionSubscriber'; import { MediaError } from 'types/mediaError'; import { getMediaError } from 'utils/mediaError'; import { toApi } from 'utils/jellyfin-apiclient/compat'; @@ -3686,7 +3687,6 @@ export class PlaybackManager { }); } - bindMediaSegmentManager(self); if (!browser.tv && !browser.xboxOne && !browser.ps4) { this._skipSegment = bindSkipSegment(self); } @@ -4253,6 +4253,8 @@ export class PlaybackManager { } export const playbackManager = new PlaybackManager(); +bindMediaSegmentManager(playbackManager); +bindMediaSessionSubscriber(playbackManager); window.addEventListener('beforeunload', function () { try { diff --git a/src/components/remotecontrol/remotecontrol.js b/src/components/remotecontrol/remotecontrol.js index 31b88a8e8e..8702da65a6 100644 --- a/src/components/remotecontrol/remotecontrol.js +++ b/src/components/remotecontrol/remotecontrol.js @@ -22,6 +22,7 @@ import ServerConnections from '../ServerConnections'; import toast from '../toast/toast'; import { appRouter } from '../router/appRouter'; import { getDefaultBackgroundClass } from '../cardbuilder/cardBuilderUtils'; +import { getImageUrl } from 'apps/stable/features/playback/utils/image'; let showMuteButton = true; let showVolumeSlider = true; @@ -91,50 +92,6 @@ function getNowPlayingNameHtml(nowPlayingItem, includeNonNameInfo) { }).join('
'); } -function seriesImageUrl(item, options) { - if (item.Type !== 'Episode') { - return null; - } - - options = options || {}; - options.type = options.type || 'Primary'; - if (options.type === 'Primary' && item.SeriesPrimaryImageTag) { - options.tag = item.SeriesPrimaryImageTag; - return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.SeriesId, options); - } - - if (options.type === 'Thumb') { - if (item.SeriesThumbImageTag) { - options.tag = item.SeriesThumbImageTag; - return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.SeriesId, options); - } - - if (item.ParentThumbImageTag) { - options.tag = item.ParentThumbImageTag; - return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.ParentThumbItemId, options); - } - } - - return null; -} - -function imageUrl(item, options) { - options = options || {}; - options.type = options.type || 'Primary'; - - if (item.ImageTags?.[options.type]) { - options.tag = item.ImageTags[options.type]; - return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.PrimaryImageItemId || item.Id, options); - } - - if (item.AlbumId && item.AlbumPrimaryImageTag) { - options.tag = item.AlbumPrimaryImageTag; - return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.AlbumId, options); - } - - return null; -} - function updateNowPlayingInfo(context, state, serverId) { const item = state.NowPlayingItem; const displayName = item ? getNowPlayingNameHtml(item).replace('
', ' - ') : ''; @@ -193,9 +150,7 @@ function updateNowPlayingInfo(context, state, serverId) { context.querySelector('.nowPlayingPageTitle').classList.add('hide'); } - const url = seriesImageUrl(item, { - maxHeight: 300 - }) || imageUrl(item, { + const url = getImageUrl(item, { maxHeight: 300 }); diff --git a/src/constants/time.ts b/src/constants/time.ts index 4555b91192..8d2f720b0b 100644 --- a/src/constants/time.ts +++ b/src/constants/time.ts @@ -1,8 +1,11 @@ +/** The number of milliseconds per second */ +export const MILLISECONDS_PER_SECOND = 1_000; + /** The number of ticks per millisecond */ export const TICKS_PER_MILLISECOND = 10_000; /** The number of ticks per second */ -export const TICKS_PER_SECOND = 1_000 * TICKS_PER_MILLISECOND; +export const TICKS_PER_SECOND = MILLISECONDS_PER_SECOND * TICKS_PER_MILLISECOND; /** The number of ticks per minute */ export const TICKS_PER_MINUTE = 60 * TICKS_PER_SECOND; diff --git a/src/index.jsx b/src/index.jsx index 1db2bcebef..99f01e297a 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -184,11 +184,6 @@ function loadPlatformFeatures() { import('./components/playback/volumeosd'); } - /* eslint-disable-next-line compat/compat */ - if (navigator.mediaSession || window.NativeShell) { - import('./components/playback/mediasession'); - } - if (!browser.tv && !browser.xboxOne) { import('./components/playback/playbackorientation'); registerServiceWorker(); diff --git a/src/types/playbackStopInfo.ts b/src/types/playbackStopInfo.ts index 4826d98449..a050fc8723 100644 --- a/src/types/playbackStopInfo.ts +++ b/src/types/playbackStopInfo.ts @@ -6,6 +6,8 @@ import type { PlayerStateInfo } from '@jellyfin/sdk/lib/generated-client'; +import type { ItemDto } from 'types/base/models/item-dto'; + export interface BufferedRange { start?: number; end?: number; @@ -31,7 +33,7 @@ export interface MediaSource extends MediaSourceInfo { export interface PlayerState { PlayState: PlayState; - NowPlayingItem: BaseItemDto | null; + NowPlayingItem: ItemDto | null; NextItem: BaseItemDto | null; NextMediaType: MediaType | null; MediaSource: MediaSource | null; diff --git a/src/types/plugin.ts b/src/types/plugin.ts index b2896012df..acc07bcc5b 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -11,3 +11,7 @@ export interface Plugin { type: PluginType | string priority?: number } + +export interface PlayerPlugin extends Plugin { + isLocalPlayer?: boolean +}