From 353fd6ed1755ed9dc961be614c029c0190b7bbbe Mon Sep 17 00:00:00 2001 From: hawken Date: Sat, 12 Dec 2020 19:08:43 +0100 Subject: [PATCH 01/14] refactor mediaManager to playerManager, remove mgr --- src/components/jellyfinActions.ts | 2 +- src/components/maincontroller.ts | 92 +++++++++++++++++-------------- src/helpers.ts | 6 +- src/types/global.d.ts | 2 +- 4 files changed, 56 insertions(+), 46 deletions(-) diff --git a/src/components/jellyfinActions.ts b/src/components/jellyfinActions.ts index 0d408bba..27e09c6e 100644 --- a/src/components/jellyfinActions.ts +++ b/src/components/jellyfinActions.ts @@ -230,7 +230,7 @@ export function play($scope: GlobalScope): void { DocumentManager.getAppStatus() == 'audio' ) { setTimeout(() => { - window.mediaManager.play(); + window.playerManager.play(); if ($scope.mediaType == 'Audio') { DocumentManager.setAppStatus('audio'); diff --git a/src/components/maincontroller.ts b/src/components/maincontroller.ts index 21f93a45..c6c7bf77 100644 --- a/src/components/maincontroller.ts +++ b/src/components/maincontroller.ts @@ -32,16 +32,14 @@ import { MediaSourceInfo } from '~/api/generated/models/media-source-info'; import { GlobalScope, PlayRequest } from '~/types/global'; window.castReceiverContext = cast.framework.CastReceiverContext.getInstance(); -window.mediaManager = window.castReceiverContext.getPlayerManager(); +window.playerManager = window.castReceiverContext.getPlayerManager(); -const playbackMgr = new playbackManager(window.mediaManager); +const playbackMgr = new playbackManager(window.playerManager); -CommandHandler.configure(window.mediaManager, playbackMgr); +CommandHandler.configure(window.playerManager, playbackMgr); resetPlaybackScope($scope); -const mgr = window.mediaManager; - let broadcastToServer = new Date(); let hasReportedCapabilities = false; @@ -106,7 +104,7 @@ function onMediaElementVolumeChange(event: framework.system.Event): void { * */ export function enableTimeUpdateListener(): void { - window.mediaManager.addEventListener( + window.playerManager.addEventListener( cast.framework.events.EventType.TIME_UPDATE, onMediaElementTimeUpdate ); @@ -114,11 +112,11 @@ export function enableTimeUpdateListener(): void { cast.framework.system.EventType.SYSTEM_VOLUME_CHANGED, onMediaElementVolumeChange ); - window.mediaManager.addEventListener( + window.playerManager.addEventListener( cast.framework.events.EventType.PAUSE, onMediaElementPause ); - window.mediaManager.addEventListener( + window.playerManager.addEventListener( cast.framework.events.EventType.PLAYING, onMediaElementPlaying ); @@ -128,7 +126,7 @@ export function enableTimeUpdateListener(): void { * */ export function disableTimeUpdateListener(): void { - window.mediaManager.removeEventListener( + window.playerManager.removeEventListener( cast.framework.events.EventType.TIME_UPDATE, onMediaElementTimeUpdate ); @@ -136,11 +134,11 @@ export function disableTimeUpdateListener(): void { cast.framework.system.EventType.SYSTEM_VOLUME_CHANGED, onMediaElementVolumeChange ); - window.mediaManager.removeEventListener( + window.playerManager.removeEventListener( cast.framework.events.EventType.PAUSE, onMediaElementPause ); - window.mediaManager.removeEventListener( + window.playerManager.removeEventListener( cast.framework.events.EventType.PLAYING, onMediaElementPlaying ); @@ -154,14 +152,20 @@ window.addEventListener('beforeunload', () => { reportPlaybackStopped($scope, getReportingParams($scope)); }); -mgr.addEventListener(cast.framework.events.EventType.PLAY, (): void => { - play($scope); - reportPlaybackProgress($scope, getReportingParams($scope)); -}); +window.playerManager.addEventListener( + cast.framework.events.EventType.PLAY, + (): void => { + play($scope); + reportPlaybackProgress($scope, getReportingParams($scope)); + } +); -mgr.addEventListener(cast.framework.events.EventType.PAUSE, (): void => { - reportPlaybackProgress($scope, getReportingParams($scope)); -}); +window.playerManager.addEventListener( + cast.framework.events.EventType.PAUSE, + (): void => { + reportPlaybackProgress($scope, getReportingParams($scope)); + } +); /** * @@ -170,34 +174,40 @@ function defaultOnStop(): void { playbackMgr.stop(); } -mgr.addEventListener( +window.playerManager.addEventListener( cast.framework.events.EventType.MEDIA_FINISHED, defaultOnStop ); -mgr.addEventListener(cast.framework.events.EventType.ABORT, defaultOnStop); +window.playerManager.addEventListener( + cast.framework.events.EventType.ABORT, + defaultOnStop +); -mgr.addEventListener(cast.framework.events.EventType.ENDED, () => { - // Ignore - if ($scope.isChangingStream) { - return; - } +window.playerManager.addEventListener( + cast.framework.events.EventType.ENDED, + (): void => { + // Ignore + if ($scope.isChangingStream) { + return; + } - reportPlaybackStopped($scope, getReportingParams($scope)); - resetPlaybackScope($scope); + reportPlaybackStopped($scope, getReportingParams($scope)); + resetPlaybackScope($scope); - if (!playbackMgr.playNextItem()) { - window.playlist = []; - window.currentPlaylistIndex = -1; - DocumentManager.startBackdropInterval(); + if (!playbackMgr.playNextItem()) { + window.playlist = []; + window.currentPlaylistIndex = -1; + DocumentManager.startBackdropInterval(); + } } -}); +); // Set the active subtitle track once the player has loaded -window.mediaManager.addEventListener( +window.playerManager.addEventListener( cast.framework.events.EventType.PLAYER_LOAD_COMPLETE, () => { setTextTrack( - window.mediaManager.getMediaInformation().customData + window.playerManager.getMediaInformation().customData .subtitleStreamIndex ); } @@ -424,10 +434,10 @@ export async function changeStream( params: any = undefined ): Promise { if ( - window.mediaManager.getMediaInformation().customData.canClientSeek && + window.playerManager.getMediaInformation().customData.canClientSeek && params == null ) { - window.mediaManager.seek(ticks / 10000000); + window.playerManager.seek(ticks / 10000000); reportPlaybackProgress($scope, getReportingParams($scope)); return Promise.resolve(); @@ -492,12 +502,12 @@ export async function changeStream( const requiresStoppingTranscoding = false; if (requiresStoppingTranscoding) { - window.mediaManager.pause(); + window.playerManager.pause(); await stopActiveEncodings(playSessionId); } - window.mediaManager.load(loadRequest); - window.mediaManager.play(); + window.playerManager.load(loadRequest); + window.playerManager.play(); $scope.subtitleStreamIndex = subtitleStreamIndex; $scope.audioStreamIndex = audioStreamIndex; } @@ -714,7 +724,7 @@ export function checkDirectPlay(mediaSource: MediaSourceInfo): void { */ export function setTextTrack(index: number | null): void { try { - const textTracksManager = window.mediaManager.getTextTracksManager(); + const textTracksManager = window.playerManager.getTextTracksManager(); if (index == null) { // docs: null is okay @@ -870,7 +880,7 @@ if (!PRODUCTION) { // quits once the client closes the connection. // options.maxInactivity = 3600; - window.mediaManager.addEventListener( + window.playerManager.addEventListener( cast.framework.events.category.CORE, (event: framework.events.Event) => { console.log(`Core event: ${event.type}`); diff --git a/src/helpers.ts b/src/helpers.ts index 962433bd..ec092b08 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -16,8 +16,8 @@ import { GlobalScope, BusMessage, ItemIndex, ItemQuery } from './types/global'; * @returns position in ticks */ export function getCurrentPositionTicks($scope: GlobalScope): number { - let positionTicks = window.mediaManager.getCurrentTimeSec() * 10000000; - const mediaInformation = window.mediaManager.getMediaInformation(); + let positionTicks = window.playerManager.getCurrentTimeSec() * 10000000; + const mediaInformation = window.playerManager.getMediaInformation(); if (mediaInformation && !mediaInformation.customData.canClientSeek) { positionTicks += $scope.startPositionTicks || 0; @@ -43,7 +43,7 @@ export function getReportingParams($scope: GlobalScope): PlaybackProgressInfo { CanSeek: $scope.canSeek, IsMuted: window.volume.muted, IsPaused: - window.mediaManager.getPlayerState() === + window.playerManager.getPlayerState() === cast.framework.messages.PlayerState.PAUSED, ItemId: $scope.itemId, LiveStreamId: $scope.liveStreamId, diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 2018cfb8..6735e15a 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -104,7 +104,7 @@ declare global { export interface Window { deviceInfo: DeviceInfo; mediaElement: HTMLElement | null; - mediaManager: PlayerManager; + playerManager: PlayerManager; castReceiverContext: CastReceiverContext; playlist: Array; currentPlaylistIndex: number; From 065392462721dbe51a350f1726ac749376a34f1b Mon Sep 17 00:00:00 2001 From: hawken Date: Fri, 1 Jan 2021 00:56:01 +0100 Subject: [PATCH 02/14] move window.deviceInfo to JellyfinApi --- src/app.ts | 12 ---------- src/components/jellyfinActions.ts | 2 +- src/components/jellyfinApi.ts | 39 ++++++++++++++++++++++++++----- src/components/maincontroller.ts | 27 +++++++-------------- src/helpers.ts | 10 -------- src/types/global.d.ts | 7 ------ 6 files changed, 43 insertions(+), 54 deletions(-) diff --git a/src/app.ts b/src/app.ts index 18b4dd16..add4403b 100644 --- a/src/app.ts +++ b/src/app.ts @@ -3,18 +3,6 @@ import './components/maincontroller'; import './css/glyphicons.css'; import './css/jellyfin.css'; -const senders = cast.framework.CastReceiverContext.getInstance().getSenders(); -const id = - senders.length !== 0 && senders[0].id - ? senders[0].id - : new Date().getTime(); - -window.deviceInfo = { - deviceId: id, - deviceName: 'Google Cast', - versionNumber: RECEIVERVERSION -}; - window.mediaElement = document.getElementById('video-player'); window.playlist = []; diff --git a/src/components/jellyfinActions.ts b/src/components/jellyfinActions.ts index 27e09c6e..8c74deff 100644 --- a/src/components/jellyfinActions.ts +++ b/src/components/jellyfinActions.ts @@ -409,7 +409,7 @@ export async function detectBitrate(): Promise { */ export function stopActiveEncodings($scope: GlobalScope): Promise { const options = { - deviceId: window.deviceInfo.deviceId, + deviceId: JellyfinApi.deviceId, PlaySessionId: undefined }; diff --git a/src/components/jellyfinApi.ts b/src/components/jellyfinApi.ts index 8221ea07..07e43e19 100644 --- a/src/components/jellyfinApi.ts +++ b/src/components/jellyfinApi.ts @@ -11,17 +11,46 @@ export abstract class JellyfinApi { // Address of server public static serverAddress: string | null = null; + // device name + public static deviceName = 'Google Cast'; + + // unique id + public static deviceId = ''; + + // version + public static versionNumber = RECEIVERVERSION; + public static setServerInfo( userId: string, accessToken: string, - serverAddress: string + serverAddress: string, + receiverName: string ): void { console.debug( - `JellyfinApi.setServerInfo: user:${userId}, token:${accessToken}, server:${serverAddress}` + `JellyfinApi.setServerInfo: user:${userId}, token:${accessToken}, ` + + `server:${serverAddress}, name:${receiverName}` ); this.userId = userId; this.accessToken = accessToken; this.serverAddress = serverAddress; + + // remove special characters from the receiver name + receiverName = receiverName.replace(/[^\w\s]/gi, ''); + + if (receiverName) { + this.deviceName = receiverName; + // deviceId just needs to be unique-ish + this.deviceId = btoa(receiverName); + } else { + const senders = + cast.framework.CastReceiverContext.getInstance().getSenders(); + + this.deviceName = 'Google Cast'; + this.deviceId = + senders.length !== 0 && senders[0].id + ? senders[0].id + : new Date().getTime().toString(); + } } // create the necessary headers for authentication @@ -29,10 +58,8 @@ export abstract class JellyfinApi { // TODO throw error if this fails let auth = - `Emby Client="Chromecast", ` + - `Device="${window.deviceInfo.deviceName}", ` + - `DeviceId="${window.deviceInfo.deviceId}", ` + - `Version="${window.deviceInfo.versionNumber}"`; + `Emby Client="Chromecast", Device="${this.deviceName}", ` + + `DeviceId="${this.deviceId}", Version="${this.versionNumber}"`; if (this.userId) { auth += `, UserId="${this.userId}"`; diff --git a/src/components/maincontroller.ts b/src/components/maincontroller.ts index c6c7bf77..ae9f7f67 100644 --- a/src/components/maincontroller.ts +++ b/src/components/maincontroller.ts @@ -10,8 +10,7 @@ import { translateRequestedItems, extend, broadcastToMessageBus, - broadcastConnectionErrorMessage, - cleanName + broadcastConnectionErrorMessage } from '../helpers'; import { reportPlaybackProgress, @@ -261,38 +260,30 @@ export function processMessage(data: any): void { return; } + data.options = data.options || {}; + // Items will have properties - Id, Name, Type, MediaType, IsFolder JellyfinApi.setServerInfo( data.userId, data.accessToken, - data.serverAddress + data.serverAddress, + data.receiverName || '' ); if (data.subtitleAppearance) { window.subtitleAppearance = data.subtitleAppearance; } + if (data.maxBitrate) { + window.MaxBitrate = data.maxBitrate; + } + // Report device capabilities if (!hasReportedCapabilities) { reportDeviceCapabilities(); } - data.options = data.options || {}; - - const cleanReceiverName = cleanName(data.receiverName || ''); - - window.deviceInfo.deviceName = - cleanReceiverName || window.deviceInfo.deviceName; - // deviceId just needs to be unique-ish - window.deviceInfo.deviceId = cleanReceiverName - ? btoa(cleanReceiverName) - : window.deviceInfo.deviceId; - - if (data.maxBitrate) { - window.MaxBitrate = data.maxBitrate; - } - CommandHandler.processMessage(data, data.command); if (window.reportEventType) { diff --git a/src/helpers.ts b/src/helpers.ts index ec092b08..d0ee9487 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -859,13 +859,3 @@ export function broadcastToMessageBus(message: BusMessage): void { export function broadcastConnectionErrorMessage(): void { broadcastToMessageBus({ message: '', type: 'connectionerror' }); } - -/** - * Remove all special characters from a string - * - * @param name - input string - * @returns string with non-whitespace non-word characters removed - */ -export function cleanName(name: string): string { - return name.replace(/[^\w\s]/gi, ''); -} diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 6735e15a..f89689e5 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -6,12 +6,6 @@ import { SystemVolumeData } from 'chromecast-caf-receiver/cast.framework.system' import { RepeatMode } from '../api/generated/models/repeat-mode'; import { BaseItemDto } from '../api/generated/models/base-item-dto'; -export interface DeviceInfo { - deviceId: string | number; - deviceName: string; - versionNumber: string; -} - export interface GlobalScope { [key: string]: any; } @@ -102,7 +96,6 @@ declare global { export const RECEIVERVERSION: string; export const $scope: GlobalScope; export interface Window { - deviceInfo: DeviceInfo; mediaElement: HTMLElement | null; playerManager: PlayerManager; castReceiverContext: CastReceiverContext; From 20bf120052ce82b6725d802b3f5510974f141076 Mon Sep 17 00:00:00 2001 From: hawken Date: Tue, 29 Dec 2020 16:32:42 +0100 Subject: [PATCH 03/14] Refactor out extend method --- src/components/jellyfinActions.ts | 15 ++++++++++++--- src/components/maincontroller.ts | 15 ++++++++++----- src/helpers.ts | 18 ------------------ 3 files changed, 22 insertions(+), 26 deletions(-) diff --git a/src/components/jellyfinActions.ts b/src/components/jellyfinActions.ts index 8c74deff..cbce5eb4 100644 --- a/src/components/jellyfinActions.ts +++ b/src/components/jellyfinActions.ts @@ -1,7 +1,6 @@ import { getSenderReportingData, resetPlaybackScope, - extend, broadcastToMessageBus } from '../helpers'; @@ -199,12 +198,22 @@ export function pingTranscoder( */ export function load( $scope: GlobalScope, - customData: PlaybackProgressInfo, + customData: any, serverItem: BaseItemDto ): void { resetPlaybackScope($scope); - extend($scope, customData); + // These are set up in maincontroller.createMediaInformation + $scope.playSessionId = customData.playSessionId; + $scope.audioStreamIndex = customData.audioStreamIndex; + $scope.subtitleStreamIndex = customData.subtitleStreamIndex; + $scope.startPositionTicks = customData.startPositionTicks; + $scope.canSeek = customData.canSeek; + $scope.itemId = customData.itemId; + $scope.liveStreamId = customData.liveStreamId; + $scope.mediaSourceId = customData.mediaSourceId; + $scope.playMethod = customData.playMethod; + $scope.runtimeTicks = customData.runtimeTicks; $scope.item = serverItem; diff --git a/src/components/maincontroller.ts b/src/components/maincontroller.ts index ae9f7f67..4e5335f9 100644 --- a/src/components/maincontroller.ts +++ b/src/components/maincontroller.ts @@ -8,7 +8,6 @@ import { getShuffleItems, getInstantMixItems, translateRequestedItems, - extend, broadcastToMessageBus, broadcastConnectionErrorMessage } from '../helpers'; @@ -591,8 +590,15 @@ export async function shuffle( } /** - * @param item - * @param options + * This function fetches the full information for an item before playing it. + * The provided item is not complete. + * + * Old behavior: The original properties would be copied over to the fetched one, + * but just the fetched item should be fine + * + * @param item - Item to look up + * @param options - Extra information about how it should be played back. + * @returns Promise waiting for the item to be loaded for playback */ export async function onStopPlayerBeforePlaybackDone( item: BaseItemDto, @@ -603,8 +609,6 @@ export async function onStopPlayerBeforePlaybackDone( type: 'GET' }); - // Attach the custom properties we created like userId, serverAddress, itemId, etc - extend(data, item); playbackMgr.playItemInternal(data, options); broadcastConnectionErrorMessage(); } @@ -807,6 +811,7 @@ export function createMediaInformation( mediaInfo.contentId = streamInfo.url; mediaInfo.contentType = streamInfo.contentType; + // TODO make a type for this mediaInfo.customData = { audioStreamIndex: streamInfo.audioStreamIndex, canClientSeek: streamInfo.canClientSeek, diff --git a/src/helpers.ts b/src/helpers.ts index d0ee9487..787b269e 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -228,7 +228,6 @@ export function resetPlaybackScope($scope: GlobalScope): void { $scope.playMethod = ''; $scope.canSeek = false; - $scope.canClientSeek = false; $scope.isChangingStream = false; $scope.playNextItem = true; @@ -811,23 +810,6 @@ export async function translateRequestedItems( }; } -/** - * Take all properties of source and copy them over to target - * - * TODO can we remove this crap - * - * @param target - object that gets populated with entries - * @param source - object that the entries are copied from - * @returns reference to target object - */ -export function extend(target: any, source: any): any { - for (const i in source) { - target[i] = source[i]; - } - - return target; -} - /** * Parse a date.. Just a wrapper around new Date, * but could be useful to deal with weird date strings From 71cac202a1f0ede5a562024990f1c23756e84d88 Mon Sep 17 00:00:00 2001 From: hawken <1411903+hawken93@users.noreply.github.com> Date: Mon, 22 Feb 2021 20:30:29 +0100 Subject: [PATCH 04/14] Comment updates (plus small start position fix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fernando Fernández --- src/components/maincontroller.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/maincontroller.ts b/src/components/maincontroller.ts index 4e5335f9..aef5bf27 100644 --- a/src/components/maincontroller.ts +++ b/src/components/maincontroller.ts @@ -590,11 +590,8 @@ export async function shuffle( } /** - * This function fetches the full information for an item before playing it. - * The provided item is not complete. - * - * Old behavior: The original properties would be copied over to the fetched one, - * but just the fetched item should be fine + * This function fetches the full information of an item before playing it. + * Only item.Id needs to be set. * * @param item - Item to look up * @param options - Extra information about how it should be played back. @@ -837,7 +834,9 @@ export function createMediaInformation( ); } - mediaInfo.customData.startPositionTicks = streamInfo.startPosition || 0; + // If the client actually sets startPosition: + // if(streamInfo.startPosition) + // mediaInfo.customData.startPositionTicks = streamInfo.startPosition return mediaInfo; } From 5f96fe0a77eba95deb4080024cd8a704118f0299 Mon Sep 17 00:00:00 2001 From: hawken Date: Thu, 31 Dec 2020 23:56:30 +0100 Subject: [PATCH 05/14] use window width instead because HLS is strict --- src/components/codecSupportHelper.ts | 34 +++++++++++++++----------- src/components/deviceprofileBuilder.ts | 2 +- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/components/codecSupportHelper.ts b/src/components/codecSupportHelper.ts index bc4ffb2f..bf15e024 100644 --- a/src/components/codecSupportHelper.ts +++ b/src/components/codecSupportHelper.ts @@ -102,22 +102,28 @@ export function getMaxBitrateSupport(): number { /** * Get the max supported video width the active Cast device supports. * - * @param deviceId - Cast device id. * @returns Max supported width. */ -export function getMaxWidthSupport(deviceId: number): number { - switch (deviceId) { - case deviceIds.ULTRA: - case deviceIds.CCGTV: - return 3840; - case deviceIds.GEN1AND2: - case deviceIds.GEN3: - return 1920; - case deviceIds.NESTHUBANDMAX: - return 1280; - } - - return 0; +export function getMaxWidthSupport(): number { + // with HLS, it will produce a manifest error if we + // send any stream larger than the screen size... + return window.innerWidth; + + // mkv playback can use the device limitations. + // The devices are capable of decoding and downscaling, + // they just refuse to do it with HLS. This increases + // the rate of direct playback. + //switch (deviceId) { + // case deviceIds.ULTRA: + // case deviceIds.CCGTV: + // return 3840; + // case deviceIds.GEN1AND2: + // case deviceIds.GEN3: + // return 1920; + // case deviceIds.NESTHUBANDMAX: + // return 1280; + //} + //return 0; } /** diff --git a/src/components/deviceprofileBuilder.ts b/src/components/deviceprofileBuilder.ts index 7ba74082..52afb353 100644 --- a/src/components/deviceprofileBuilder.ts +++ b/src/components/deviceprofileBuilder.ts @@ -195,7 +195,7 @@ function getCodecProfiles(): Array { CodecProfiles.push(aacConditions); - const maxWidth: number = getMaxWidthSupport(currentDeviceId); + const maxWidth: number = getMaxWidthSupport(); const h26xLevel: number = getH26xLevelSupport(currentDeviceId); const h26xProfile: string = getH26xProfileSupport(currentDeviceId); From 114aab79137f2c0b32ac1cd3ed15ef7697dbd04b Mon Sep 17 00:00:00 2001 From: hawken Date: Mon, 21 Dec 2020 20:07:05 +0100 Subject: [PATCH 06/14] Fix start seek --- src/components/playbackManager.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/playbackManager.ts b/src/components/playbackManager.ts index f59b9786..70921137 100644 --- a/src/components/playbackManager.ts +++ b/src/components/playbackManager.ts @@ -213,6 +213,13 @@ export class playbackManager { loadRequestData.media = mediaInfo; loadRequestData.autoplay = true; + // If we should seek at the start, translate it + // to seconds and give it to loadRequestData :) + if (mediaInfo.customData.startPositionTicks > 0) { + loadRequestData.currentTime = + mediaInfo.customData.startPositionTicks / 10000000; + } + load($scope, mediaInfo.customData, item); this.playerManager.load(loadRequestData); From 18b3db2c1135d8363697dec3eab5888fbdcd4f79 Mon Sep 17 00:00:00 2001 From: hawken Date: Mon, 21 Dec 2020 21:16:31 +0100 Subject: [PATCH 07/14] add event listeners for start/stop reporting where the player probably has a useful position --- src/components/maincontroller.ts | 17 ++++++++++++++++- src/components/playbackManager.ts | 21 ++------------------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/components/maincontroller.ts b/src/components/maincontroller.ts index aef5bf27..33030cbd 100644 --- a/src/components/maincontroller.ts +++ b/src/components/maincontroller.ts @@ -12,6 +12,7 @@ import { broadcastConnectionErrorMessage } from '../helpers'; import { + reportPlaybackStart, reportPlaybackProgress, reportPlaybackStopped, play, @@ -154,7 +155,6 @@ window.playerManager.addEventListener( cast.framework.events.EventType.PLAY, (): void => { play($scope); - reportPlaybackProgress($scope, getReportingParams($scope)); } ); @@ -200,6 +200,21 @@ window.playerManager.addEventListener( } ); +// Notify of playback start as soon as the media is playing. Only then is the tick position good. +window.playerManager.addEventListener( + cast.framework.events.EventType.PLAYING, + (): void => { + reportPlaybackStart($scope, getReportingParams($scope)); + } +); +// Notify of playback end just before stopping it, to get a good tick position +window.playerManager.addEventListener( + cast.framework.events.EventType.REQUEST_STOP, + (): void => { + reportPlaybackStopped($scope, getReportingParams($scope)); + } +); + // Set the active subtitle track once the player has loaded window.playerManager.addEventListener( cast.framework.events.EventType.PLAYER_LOAD_COMPLETE, diff --git a/src/components/playbackManager.ts b/src/components/playbackManager.ts index 70921137..49b5bec2 100644 --- a/src/components/playbackManager.ts +++ b/src/components/playbackManager.ts @@ -2,7 +2,6 @@ import { getNextPlaybackItemInfo, getIntros, broadcastConnectionErrorMessage, - getReportingParams, createStreamInfo } from '../helpers'; @@ -10,10 +9,8 @@ import { getPlaybackInfo, getLiveStream, load, - reportPlaybackStart, stop, - stopPingInterval, - reportPlaybackStopped + stopPingInterval } from './jellyfinActions'; import { getDeviceProfile } from './deviceprofileBuilder'; @@ -230,36 +227,22 @@ export class playbackManager { DocumentManager.setPlayerBackdrop(item); - reportPlaybackStart($scope, getReportingParams($scope)); - // We use false as we do not want to broadcast the new status yet // we will broadcast manually when the media has been loaded, this // is to be sure the duration has been updated in the media element this.playerManager.setMediaInformation(mediaInfo, false); } - stop(continuing = false): Promise { + stop(continuing = false): void { $scope.playNextItem = continuing; stop(); - const reportingParams = getReportingParams($scope); - - let promise; - stopPingInterval(); - if (reportingParams.ItemId) { - promise = reportPlaybackStopped($scope, reportingParams); - } - this.playerManager.stop(); this.activePlaylist = []; this.activePlaylistIndex = -1; DocumentManager.startBackdropInterval(); - - promise = promise || Promise.resolve(); - - return promise; } } From a76e8b8bf81dff46844b2250cb2afbb008e71a0c Mon Sep 17 00:00:00 2001 From: hawken Date: Fri, 25 Dec 2020 17:30:12 +0100 Subject: [PATCH 08/14] fix stream change --- src/components/maincontroller.ts | 104 +++++++----------------------- src/components/playbackManager.ts | 9 +-- 2 files changed, 29 insertions(+), 84 deletions(-) diff --git a/src/components/maincontroller.ts b/src/components/maincontroller.ts index 33030cbd..ae147a47 100644 --- a/src/components/maincontroller.ts +++ b/src/components/maincontroller.ts @@ -3,7 +3,6 @@ import { getReportingParams, resetPlaybackScope, getMetadata, - createStreamInfo, getStreamByIndex, getShuffleItems, getInstantMixItems, @@ -16,8 +15,6 @@ import { reportPlaybackProgress, reportPlaybackStopped, play, - getPlaybackInfo, - stopActiveEncodings, detectBitrate } from './jellyfinActions'; import { getDeviceProfile } from './deviceprofileBuilder'; @@ -450,71 +447,31 @@ export async function changeStream( params = params || {}; - const playSessionId = $scope.playSessionId; - const liveStreamId = $scope.liveStreamId; - - const item = $scope.item; - const maxBitrate = await getMaxBitrate(); - - const deviceProfile = getDeviceProfile({ - bitrateSetting: maxBitrate, - enableHls: true + // TODO Could be useful for garbage collection. + // It needs to predict if the server side transcode needs + // to restart. + // Possibility: Always assume it will. Downside: VTT subs switching doesn't + // need to restart the transcode. + //const requiresStoppingTranscoding = false; + // + //if (requiresStoppingTranscoding) { + // window.playerManager.pause(); + // await stopActiveEncodings($scope.playSessionId); + //} + + return await playbackMgr.playItemInternal($scope.item, { + audioStreamIndex: + params.AudioStreamIndex == null + ? $scope.audioStreamIndex + : params.AudioStreamIndex, + liveStreamId: $scope.liveStreamId, + mediaSourceId: $scope.mediaSourceId, + startPositionTicks: ticks, + subtitleStreamIndex: + params.SubtitleStreamIndex == null + ? $scope.subtitleStreamIndex + : params.SubtitleStreamIndex }); - const audioStreamIndex = - params.AudioStreamIndex == null - ? $scope.audioStreamIndex - : params.AudioStreamIndex; - const subtitleStreamIndex = - params.SubtitleStreamIndex == null - ? $scope.subtitleStreamIndex - : params.SubtitleStreamIndex; - - const playbackInformation = await getPlaybackInfo( - item, - maxBitrate, - deviceProfile, - ticks, - $scope.mediaSourceId, - audioStreamIndex, - subtitleStreamIndex, - liveStreamId - ); - - if (!validatePlaybackInfoResult(playbackInformation)) { - return; - } - - const mediaSource = playbackInformation.MediaSources[0]; - const streamInfo = createStreamInfo(item, mediaSource, ticks); - - if (!streamInfo.url) { - showPlaybackInfoErrorMessage('NoCompatibleStream'); - - return; - } - - const mediaInformation = createMediaInformation( - playSessionId, - item, - streamInfo - ); - const loadRequest = new cast.framework.messages.LoadRequestData(); - - loadRequest.media = mediaInformation; - loadRequest.autoplay = true; - - // TODO something to do with HLS? - const requiresStoppingTranscoding = false; - - if (requiresStoppingTranscoding) { - window.playerManager.pause(); - await stopActiveEncodings(playSessionId); - } - - window.playerManager.load(loadRequest); - window.playerManager.play(); - $scope.subtitleStreamIndex = subtitleStreamIndex; - $scope.audioStreamIndex = audioStreamIndex; } // Create a message handler for the custome namespace channel @@ -665,19 +622,6 @@ export async function getMaxBitrate(): Promise { } } -/** - * @param result - */ -export function validatePlaybackInfoResult(result: any): boolean { - if (result.ErrorCode) { - showPlaybackInfoErrorMessage(result.ErrorCode); - - return false; - } - - return true; -} - /** * @param error */ diff --git a/src/components/playbackManager.ts b/src/components/playbackManager.ts index 49b5bec2..3e0b8249 100644 --- a/src/components/playbackManager.ts +++ b/src/components/playbackManager.ts @@ -140,7 +140,8 @@ export class playbackManager { options.startPositionTicks, options.mediaSourceId, options.audioStreamIndex, - options.subtitleStreamIndex + options.subtitleStreamIndex, + options.liveStreamId ).catch(broadcastConnectionErrorMessage); if (playbackInfo.ErrorCode) { @@ -198,8 +199,6 @@ export class playbackManager { options.startPositionTicks ); - const url = streamInfo.url; - const mediaInfo = createMediaInformation( playSessionId, item, @@ -220,10 +219,12 @@ export class playbackManager { load($scope, mediaInfo.customData, item); this.playerManager.load(loadRequestData); + console.log(`setting src to ${streamInfo.url}`); $scope.PlaybackMediaSource = mediaSource; - console.log(`setting src to ${url}`); $scope.mediaSource = mediaSource; + $scope.audioStreamIndex = streamInfo.audioStreamIndex; + $scope.subtitleStreamIndex = streamInfo.subtitleStreamIndex; DocumentManager.setPlayerBackdrop(item); From 94aba9c030b39ca0c34f2995d86e602c9bb55ee0 Mon Sep 17 00:00:00 2001 From: hawken Date: Fri, 25 Dec 2020 23:11:26 +0100 Subject: [PATCH 09/14] fix playerManager.stop -> onStopped to avoid telling the cast to stop when it shouldn't --- src/components/jellyfinActions.ts | 9 --------- src/components/maincontroller.ts | 8 ++++---- src/components/playbackManager.ts | 26 +++++++++++++++++++------- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/components/jellyfinActions.ts b/src/components/jellyfinActions.ts index cbce5eb4..07ed833d 100644 --- a/src/components/jellyfinActions.ts +++ b/src/components/jellyfinActions.ts @@ -250,15 +250,6 @@ export function play($scope: GlobalScope): void { } } -/** - * Don't actually stop, just show the idle view after 20ms - */ -export function stop(): void { - setTimeout(() => { - DocumentManager.setAppStatus('waiting'); - }, 20); -} - /** * @param item * @param maxBitrate diff --git a/src/components/maincontroller.ts b/src/components/maincontroller.ts index ae147a47..92c1c9e6 100644 --- a/src/components/maincontroller.ts +++ b/src/components/maincontroller.ts @@ -165,17 +165,17 @@ window.playerManager.addEventListener( /** * */ -function defaultOnStop(): void { - playbackMgr.stop(); +function defaultOnStopped(): void { + playbackMgr.onStopped(true); } window.playerManager.addEventListener( cast.framework.events.EventType.MEDIA_FINISHED, - defaultOnStop + defaultOnStopped ); window.playerManager.addEventListener( cast.framework.events.EventType.ABORT, - defaultOnStop + defaultOnStopped ); window.playerManager.addEventListener( diff --git a/src/components/playbackManager.ts b/src/components/playbackManager.ts index 3e0b8249..ff9a4462 100644 --- a/src/components/playbackManager.ts +++ b/src/components/playbackManager.ts @@ -9,7 +9,6 @@ import { getPlaybackInfo, getLiveStream, load, - stop, stopPingInterval } from './jellyfinActions'; import { getDeviceProfile } from './deviceprofileBuilder'; @@ -118,7 +117,7 @@ export class playbackManager { stopPlayer = false ): Promise { if (stopPlayer) { - await this.stop(true); + this.stop(); } return await onStopPlayerBeforePlaybackDone(item, options); @@ -234,13 +233,26 @@ export class playbackManager { this.playerManager.setMediaInformation(mediaInfo, false); } - stop(continuing = false): void { - $scope.playNextItem = continuing; - stop(); + /** + * stop playback, as requested by the client + */ + stop(): void { + this.playerManager.stop(); + // onStopped will be called when playback comes to a halt. + } - stopPingInterval(); + /** + * Called when media stops playing. + * TODO avoid doing this between tracks in a playlist + * + * @param _continue - If we have another item coming up in the playlist. Only used for notifying the cast sender. + */ + onStopped(_continue: boolean): void { + $scope.playNextItem = _continue; - this.playerManager.stop(); + DocumentManager.setAppStatus('waiting'); + + stopPingInterval(); this.activePlaylist = []; this.activePlaylistIndex = -1; From 921467a4b9de97c3da6d06ba0fdc03fe83b41ab6 Mon Sep 17 00:00:00 2001 From: hawken Date: Tue, 29 Dec 2020 17:02:12 +0100 Subject: [PATCH 10/14] getMaxBitrate: Always cap to device max bitrate --- src/components/maincontroller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/maincontroller.ts b/src/components/maincontroller.ts index 92c1c9e6..ca7a2244 100644 --- a/src/components/maincontroller.ts +++ b/src/components/maincontroller.ts @@ -613,7 +613,7 @@ export async function getMaxBitrate(): Promise { lastBitrateDetect = new Date().getTime(); detectedBitrate = bitrate; - return detectedBitrate; + return Math.min(detectedBitrate, getMaxBitrateSupport()); } catch (e) { // The client can set this number console.log('Error detecting bitrate, will return device maximum.'); From 482cf586c11cdc359160eed8121991cbee07b4e5 Mon Sep 17 00:00:00 2001 From: hawken Date: Tue, 29 Dec 2020 17:10:21 +0100 Subject: [PATCH 11/14] try to get more accurate bitrate --- src/components/jellyfinActions.ts | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/components/jellyfinActions.ts b/src/components/jellyfinActions.ts index 07ed833d..a94a30b6 100644 --- a/src/components/jellyfinActions.ts +++ b/src/components/jellyfinActions.ts @@ -369,11 +369,14 @@ export async function getDownloadSpeed(byteSize: number): Promise { const now = new Date().getTime(); - await JellyfinApi.authAjax(path, { + const response = await JellyfinApi.authAjax(path, { timeout: 5000, type: 'GET' }); + // Force javascript to download the whole response before calculating bitrate + await response.blob(); + const responseTimeSeconds = (new Date().getTime() - now) / 1000; const bytesPerSecond = byteSize / responseTimeSeconds; const bitrate = Math.round(bytesPerSecond * 8); @@ -383,22 +386,29 @@ export async function getDownloadSpeed(byteSize: number): Promise { /** * Function to detect the bitrate. - * It first tries 1MB and if bitrate is above 1Mbit/s it tries again with 2.4MB. + * It starts at 500kB and doubles it every time it takes under 2s, for max 10MB. + * This should get an accurate bitrate relatively fast on any connection * + * @param numBytes - Number of bytes to start with, default 500k * @returns bitrate in bits/s */ -export async function detectBitrate(): Promise { - // First try a small amount so that we don't hang up their mobile connection - - let bitrate = await getDownloadSpeed(1000000); +export async function detectBitrate(numBytes = 500000): Promise { + // Jellyfin has a 10MB limit on the test size + const byteLimit = 10000000; - if (bitrate < 1000000) { - return Math.round(bitrate * 0.8); + if (numBytes > byteLimit) { + numBytes = byteLimit; } - bitrate = await getDownloadSpeed(2400000); + const bitrate = await getDownloadSpeed(numBytes); - return Math.round(bitrate * 0.8); + if (bitrate * (2 / 8.0) < numBytes || numBytes >= byteLimit) { + // took > 2s, or numBytes hit the limit + return Math.round(bitrate * 0.8); + } else { + // If that produced a fairly high speed, try again with a larger size to get a more accurate result + return await detectBitrate(numBytes * 2); + } } /** From 4f17065bc24f5f7829d13b9041a29985a874e07c Mon Sep 17 00:00:00 2001 From: hawken Date: Fri, 1 Jan 2021 05:19:12 +0100 Subject: [PATCH 12/14] refactor window.playlist into playbackmanager and make playbackmanager static --- src/app.ts | 2 - src/components/commandHandler.ts | 24 ++--- src/components/maincontroller.ts | 28 +++--- src/components/playbackManager.ts | 143 ++++++++++++++++++++++-------- src/helpers.ts | 52 +---------- src/types/global.d.ts | 2 - 6 files changed, 129 insertions(+), 122 deletions(-) diff --git a/src/app.ts b/src/app.ts index add4403b..c63630c3 100644 --- a/src/app.ts +++ b/src/app.ts @@ -5,6 +5,4 @@ import './css/jellyfin.css'; window.mediaElement = document.getElementById('video-player'); -window.playlist = []; -window.currentPlaylistIndex = -1; window.repeatMode = RepeatMode.RepeatNone; diff --git a/src/components/commandHandler.ts b/src/components/commandHandler.ts index 0e69dd1e..529cea5a 100644 --- a/src/components/commandHandler.ts +++ b/src/components/commandHandler.ts @@ -19,13 +19,12 @@ import { import { reportPlaybackProgress } from './jellyfinActions'; -import { playbackManager } from './playbackManager'; +import { PlaybackManager } from './playbackManager'; import { DocumentManager } from './documentManager'; export abstract class CommandHandler { private static playerManager: framework.PlayerManager; - private static playbackManager: playbackManager; private static supportedCommands: SupportedCommands = { DisplayContent: CommandHandler.displayContentHandler, Identify: CommandHandler.IdentifyHandler, @@ -52,12 +51,8 @@ export abstract class CommandHandler { VolumeUp: CommandHandler.VolumeUpHandler }; - static configure( - playerManager: framework.PlayerManager, - playbackManager: playbackManager - ): void { + static configure(playerManager: framework.PlayerManager): void { this.playerManager = playerManager; - this.playbackManager = playbackManager; } static playNextHandler(data: DataMessage): void { @@ -89,23 +84,20 @@ export abstract class CommandHandler { } static displayContentHandler(data: DataMessage): void { - if (!this.playbackManager.isPlaying()) { + if (!PlaybackManager.isPlaying()) { DocumentManager.showItemId((data.options).ItemId); } } static nextTrackHandler(): void { - if ( - window.playlist && - window.currentPlaylistIndex < window.playlist.length - 1 - ) { - this.playbackManager.playNextItem({}, true); + if (PlaybackManager.hasNextItem()) { + PlaybackManager.playNextItem({}, true); } } static previousTrackHandler(): void { - if (window.playlist && window.currentPlaylistIndex > 0) { - this.playbackManager.playPreviousItem({}); + if (PlaybackManager.hasPrevItem()) { + PlaybackManager.playPreviousItem({}); } } @@ -138,7 +130,7 @@ export abstract class CommandHandler { } static IdentifyHandler(): void { - if (!this.playbackManager.isPlaying()) { + if (!PlaybackManager.isPlaying()) { DocumentManager.startBackdropInterval(); } else { // When a client connects send back the initial device state (volume etc) via a playbackstop message diff --git a/src/components/maincontroller.ts b/src/components/maincontroller.ts index ca7a2244..9ff5ebf8 100644 --- a/src/components/maincontroller.ts +++ b/src/components/maincontroller.ts @@ -19,10 +19,9 @@ import { } from './jellyfinActions'; import { getDeviceProfile } from './deviceprofileBuilder'; import { JellyfinApi } from './jellyfinApi'; -import { playbackManager } from './playbackManager'; +import { PlaybackManager } from './playbackManager'; import { CommandHandler } from './commandHandler'; import { getMaxBitrateSupport } from './codecSupportHelper'; -import { DocumentManager } from './documentManager'; import { BaseItemDto } from '~/api/generated/models/base-item-dto'; import { MediaSourceInfo } from '~/api/generated/models/media-source-info'; import { GlobalScope, PlayRequest } from '~/types/global'; @@ -30,9 +29,9 @@ import { GlobalScope, PlayRequest } from '~/types/global'; window.castReceiverContext = cast.framework.CastReceiverContext.getInstance(); window.playerManager = window.castReceiverContext.getPlayerManager(); -const playbackMgr = new playbackManager(window.playerManager); +PlaybackManager.setPlayerManager(window.playerManager); -CommandHandler.configure(window.playerManager, playbackMgr); +CommandHandler.configure(window.playerManager); resetPlaybackScope($scope); @@ -166,7 +165,7 @@ window.playerManager.addEventListener( * */ function defaultOnStopped(): void { - playbackMgr.onStopped(true); + PlaybackManager.onStopped(); } window.playerManager.addEventListener( @@ -189,10 +188,9 @@ window.playerManager.addEventListener( reportPlaybackStopped($scope, getReportingParams($scope)); resetPlaybackScope($scope); - if (!playbackMgr.playNextItem()) { - window.playlist = []; - window.currentPlaylistIndex = -1; - DocumentManager.startBackdropInterval(); + if (!PlaybackManager.playNextItem()) { + PlaybackManager.resetPlaylist(); + PlaybackManager.onStopped(); } } ); @@ -459,7 +457,7 @@ export async function changeStream( // await stopActiveEncodings($scope.playSessionId); //} - return await playbackMgr.playItemInternal($scope.item, { + return await PlaybackManager.playItemInternal($scope.item, { audioStreamIndex: params.AudioStreamIndex == null ? $scope.audioStreamIndex @@ -522,10 +520,10 @@ export async function translateItems( if (method == 'PlayNext' || method == 'PlayLast') { for (let i = 0, length = options.items.length; i < length; i++) { - window.playlist.push(options.items[i]); + PlaybackManager.enqueue(options.items[i]); } } else { - playbackMgr.playFromOptions(data.options); + PlaybackManager.playFromOptions(data.options); } } @@ -542,7 +540,7 @@ export async function instantMix( const result = await getInstantMixItems(data.userId, item); options.items = result.Items; - playbackMgr.playFromOptions(data.options); + PlaybackManager.playFromOptions(data.options); } /** @@ -558,7 +556,7 @@ export async function shuffle( const result = await getShuffleItems(data.userId, item); options.items = result.Items; - playbackMgr.playFromOptions(data.options); + PlaybackManager.playFromOptions(data.options); } /** @@ -578,7 +576,7 @@ export async function onStopPlayerBeforePlaybackDone( type: 'GET' }); - playbackMgr.playItemInternal(data, options); + PlaybackManager.playItemInternal(data, options); broadcastConnectionErrorMessage(); } diff --git a/src/components/playbackManager.ts b/src/components/playbackManager.ts index ff9a4462..f3e0530a 100644 --- a/src/components/playbackManager.ts +++ b/src/components/playbackManager.ts @@ -1,5 +1,4 @@ import { - getNextPlaybackItemInfo, getIntros, broadcastConnectionErrorMessage, createStreamInfo @@ -26,19 +25,18 @@ import { DocumentManager } from './documentManager'; import { BaseItemDto } from '~/api/generated/models/base-item-dto'; import { MediaSourceInfo } from '~/api/generated/models/media-source-info'; -export class playbackManager { - private playerManager: framework.PlayerManager; - // TODO remove any - private activePlaylist: Array; - private activePlaylistIndex: number; +import { ItemIndex } from '~/types/global'; - constructor(playerManager: framework.PlayerManager) { +export abstract class PlaybackManager { + private static playerManager: framework.PlayerManager; + private static activePlaylist: Array; + private static activePlaylistIndex: number; + + static setPlayerManager(playerManager: framework.PlayerManager): void { // Parameters this.playerManager = playerManager; - // Properties - this.activePlaylist = []; - this.activePlaylistIndex = 0; + this.resetPlaylist(); } /* This is used to check if we can switch to @@ -47,7 +45,7 @@ export class playbackManager { * Returns true when playing or paused. * (before: true only when playing) * */ - isPlaying(): boolean { + static isPlaying(): boolean { return ( this.playerManager.getPlayerState() === cast.framework.messages.PlayerState.PLAYING || @@ -56,7 +54,7 @@ export class playbackManager { ); } - async playFromOptions(options: any): Promise { + static async playFromOptions(options: any): Promise { const firstItem = options.items[0]; if (options.startPositionTicks || firstItem.MediaType !== 'Video') { @@ -70,26 +68,46 @@ export class playbackManager { return this.playFromOptionsInternal(options); } - playFromOptionsInternal(options: any): boolean { + private static playFromOptionsInternal(options: any): boolean { const stopPlayer = this.activePlaylist && this.activePlaylist.length > 0; this.activePlaylist = options.items; - window.currentPlaylistIndex = -1; - window.playlist = this.activePlaylist; + // We need to set -1 so the next index will be 0 + this.activePlaylistIndex = -1; + + console.log('Loaded new playlist:', this.activePlaylist); return this.playNextItem(options, stopPlayer); } - playNextItem(options: any = {}, stopPlayer = false): boolean { - const nextItemInfo = getNextPlaybackItemInfo(); + // add item to playlist + static enqueue(item: BaseItemDto): void { + this.activePlaylist.push(item); + } + + static resetPlaylist(): void { + this.activePlaylistIndex = -1; + this.activePlaylist = []; + } + + // If there are items in the queue after the current one + static hasNextItem(): boolean { + return this.activePlaylistIndex < this.activePlaylist.length - 1; + } + + // If there are items in the queue before the current one + static hasPrevItem(): boolean { + return this.activePlaylistIndex > 0; + } + + static playNextItem(options: any = {}, stopPlayer = false): boolean { + const nextItemInfo = this.getNextPlaybackItemInfo(); if (nextItemInfo) { this.activePlaylistIndex = nextItemInfo.index; - const item = nextItemInfo.item; - - this.playItem(item, options, stopPlayer); + this.playItem(options, stopPlayer); return true; } @@ -97,13 +115,11 @@ export class playbackManager { return false; } - playPreviousItem(options: any = {}): boolean { + static playPreviousItem(options: any = {}): boolean { if (this.activePlaylist && this.activePlaylistIndex > 0) { this.activePlaylistIndex--; - const item = this.activePlaylist[this.activePlaylistIndex]; - - this.playItem(item, options, true); + this.playItem(options, true); return true; } @@ -111,8 +127,8 @@ export class playbackManager { return false; } - async playItem( - item: BaseItemDto, + // play item from playlist + private static async playItem( options: any, stopPlayer = false ): Promise { @@ -120,10 +136,18 @@ export class playbackManager { this.stop(); } + const item = this.activePlaylist[this.activePlaylistIndex]; + + console.log(`Playing index ${this.activePlaylistIndex}`, item); + return await onStopPlayerBeforePlaybackDone(item, options); } - async playItemInternal(item: BaseItemDto, options: any): Promise { + // Would set private, but some refactorings need to happen first. + static async playItemInternal( + item: BaseItemDto, + options: any + ): Promise { $scope.isChangingStream = false; DocumentManager.setAppStatus('loading'); @@ -183,8 +207,7 @@ export class playbackManager { ); } - // TODO eradicate any - playMediaSource( + private static playMediaSource( playSessionId: string, item: BaseItemDto, mediaSource: MediaSourceInfo, @@ -236,7 +259,7 @@ export class playbackManager { /** * stop playback, as requested by the client */ - stop(): void { + static stop(): void { this.playerManager.stop(); // onStopped will be called when playback comes to a halt. } @@ -244,18 +267,62 @@ export class playbackManager { /** * Called when media stops playing. * TODO avoid doing this between tracks in a playlist + */ + static onStopped(): void { + if (this.getNextPlaybackItemInfo()) { + $scope.playNextItem = true; + } else { + $scope.playNextItem = false; + + DocumentManager.setAppStatus('waiting'); + + stopPingInterval(); + + DocumentManager.startBackdropInterval(); + } + } + + /** + * Get information about the next item to play from window.playlist * - * @param _continue - If we have another item coming up in the playlist. Only used for notifying the cast sender. + * @returns item and index, or null to end playback */ - onStopped(_continue: boolean): void { - $scope.playNextItem = _continue; + static getNextPlaybackItemInfo(): ItemIndex | null { + if (this.activePlaylist.length < 1) { + return null; + } - DocumentManager.setAppStatus('waiting'); + let newIndex: number; + + if (this.activePlaylistIndex < 0) { + // negative = play the first item + newIndex = 0; + } else { + switch (window.repeatMode) { + case 'RepeatOne': + newIndex = this.activePlaylistIndex; + break; + case 'RepeatAll': + newIndex = this.activePlaylistIndex + 1; + + if (newIndex >= this.activePlaylist.length) { + newIndex = 0; + } + + break; + default: + newIndex = this.activePlaylistIndex + 1; + break; + } + } - stopPingInterval(); + if (newIndex < this.activePlaylist.length) { + return { + index: newIndex, + item: this.activePlaylist[newIndex] + }; + } - this.activePlaylist = []; - this.activePlaylistIndex = -1; - DocumentManager.startBackdropInterval(); + return null; } } diff --git a/src/helpers.ts b/src/helpers.ts index 787b269e..9f5f14e0 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,5 +1,6 @@ import { JellyfinApi } from './components/jellyfinApi'; import { DocumentManager } from './components/documentManager'; +import { PlaybackManager } from './components/playbackManager'; import { BaseItemDtoQueryResult } from './api/generated/models/base-item-dto-query-result'; import { PlaybackProgressInfo } from './api/generated/models/playback-progress-info'; @@ -7,7 +8,7 @@ import { MediaSourceInfo } from './api/generated/models/media-source-info'; import { BaseItemDto } from './api/generated/models/base-item-dto'; import { BaseItemPerson } from './api/generated/models/base-item-person'; import { UserDto } from './api/generated/models/user-dto'; -import { GlobalScope, BusMessage, ItemIndex, ItemQuery } from './types/global'; +import { GlobalScope, BusMessage, ItemQuery } from './types/global'; /** * Get current playback position in ticks, adjusted for server seeking @@ -57,53 +58,6 @@ export function getReportingParams($scope: GlobalScope): PlaybackProgressInfo { }; } -/** - * Get information about the next item to play from window.playlist - * - * @returns ItemIndex including item and index, or null to end playback - */ -export function getNextPlaybackItemInfo(): ItemIndex | null { - const playlist = window.playlist; - - if (!playlist) { - return null; - } - - let newIndex: number; - - if (window.currentPlaylistIndex == -1) { - newIndex = 0; - } else { - switch (window.repeatMode) { - case 'RepeatOne': - newIndex = window.currentPlaylistIndex; - break; - case 'RepeatAll': - newIndex = window.currentPlaylistIndex + 1; - - if (newIndex >= window.playlist.length) { - newIndex = 0; - } - - break; - default: - newIndex = window.currentPlaylistIndex + 1; - break; - } - } - - if (newIndex < playlist.length) { - const item = playlist[newIndex]; - - return { - index: newIndex, - item: item - }; - } - - return null; -} - /** * This is used in playback reporting to find out information * about the item that is currently playing. This is sent over the cast protocol over to @@ -196,7 +150,7 @@ export function getSenderReportingData( } if ($scope.playNextItem) { - const nextItemInfo = getNextPlaybackItemInfo(); + const nextItemInfo = PlaybackManager.getNextPlaybackItemInfo(); if (nextItemInfo) { state.NextMediaType = nextItemInfo.item.MediaType; diff --git a/src/types/global.d.ts b/src/types/global.d.ts index f89689e5..0570ce93 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -99,8 +99,6 @@ declare global { mediaElement: HTMLElement | null; playerManager: PlayerManager; castReceiverContext: CastReceiverContext; - playlist: Array; - currentPlaylistIndex: number; repeatMode: RepeatMode; reportEventType: 'repeatmodechange'; subtitleAppearance: any; From c8b1b1504d3f4777db5d8ba5f4b8a6ea66e325f5 Mon Sep 17 00:00:00 2001 From: hawken Date: Mon, 4 Jan 2021 16:23:24 +0100 Subject: [PATCH 13/14] try to fix the startIndex issue --- src/components/playbackManager.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/playbackManager.ts b/src/components/playbackManager.ts index f3e0530a..208dce33 100644 --- a/src/components/playbackManager.ts +++ b/src/components/playbackManager.ts @@ -54,7 +54,7 @@ export abstract class PlaybackManager { ); } - static async playFromOptions(options: any): Promise { + static async playFromOptions(options: any): Promise { const firstItem = options.items[0]; if (options.startPositionTicks || firstItem.MediaType !== 'Video') { @@ -68,17 +68,18 @@ export abstract class PlaybackManager { return this.playFromOptionsInternal(options); } - private static playFromOptionsInternal(options: any): boolean { + private static playFromOptionsInternal(options: any): Promise { const stopPlayer = this.activePlaylist && this.activePlaylist.length > 0; this.activePlaylist = options.items; - // We need to set -1 so the next index will be 0 - this.activePlaylistIndex = -1; + this.activePlaylistIndex = options.startIndex || 0; console.log('Loaded new playlist:', this.activePlaylist); - return this.playNextItem(options, stopPlayer); + // When starting playback initially, don't use + // the next item facility. + return this.playItem(options, stopPlayer); } // add item to playlist From f613aee9061c17243215179856c20a04e2710240 Mon Sep 17 00:00:00 2001 From: hawken Date: Thu, 14 Jan 2021 02:41:43 +0100 Subject: [PATCH 14/14] fix bug when client reconnects and chromecast is in details page --- src/components/commandHandler.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/commandHandler.ts b/src/components/commandHandler.ts index 529cea5a..84912a89 100644 --- a/src/components/commandHandler.ts +++ b/src/components/commandHandler.ts @@ -131,6 +131,7 @@ export abstract class CommandHandler { static IdentifyHandler(): void { if (!PlaybackManager.isPlaying()) { + DocumentManager.setAppStatus('waiting'); DocumentManager.startBackdropInterval(); } else { // When a client connects send back the initial device state (volume etc) via a playbackstop message