diff --git a/ext/src/cast/sdk/Session.ts b/ext/src/cast/sdk/Session.ts index a4407f9..1757aa3 100644 --- a/ext/src/cast/sdk/Session.ts +++ b/ext/src/cast/sdk/Session.ts @@ -23,8 +23,8 @@ import { import type { LoadRequest, QueueLoadRequest, QueueItem } from "./media/classes"; import Media, { createMedia, - MediaLastUpdateTimes, - MediaUpdateListeners, + mediaLastUpdateTimes, + mediaUpdateListeners, NS_MEDIA } from "./media/Media"; @@ -36,10 +36,11 @@ const logger = new Logger("fx_cast [sdk :: cast.Session]"); */ export function updateMedia(media: Media, status: MediaStatus) { if (status.currentTime) { - MediaLastUpdateTimes.set(media, Date.now()); + mediaLastUpdateTimes.set(media, Date.now()); } // Copy basic props + if (status.breakStatus) media.breakStatus = status.breakStatus; if (status.currentTime) media.currentTime = status.currentTime; if (status.customData) media.customData = status.customData; if (status.idleReason) media.idleReason = status.idleReason; @@ -87,20 +88,20 @@ export function updateMedia(media: Media, status: MediaStatus) { } } -export const SessionMessageListeners = new WeakMap< +export const sessionMessageListeners = new WeakMap< Session, Map> >(); -export const SessionUpdateListeners = new WeakMap< +export const sessionUpdateListeners = new WeakMap< Session, Set >(); -export const SessionSendMessageCallbacks = new WeakMap< +export const sessionSendMessageCallbacks = new WeakMap< Session, Map >(); -export const SessionLeaveSuccessCallback = new WeakMap< +export const sessionLeaveSuccessCallback = new WeakMap< Session, Optional<() => void> >(); @@ -108,24 +109,58 @@ export const SessionLeaveSuccessCallback = new WeakMap< type SendMediaMessage = ( message: DistributiveOmit ) => Promise; -export const SessionSendMediaMessage = new WeakMap(); +export const sessionSendMediaMessage = new WeakMap(); + +interface MediaRequest { + successCallback: () => void; + errorCallback: (error: CastError) => void; + message: SenderMediaMessage; + requestId: number; +} + +const sessionMediaRequests = new WeakMap>(); /** Creates a Session object and initializes private data. */ export function createSession( sessionArgs: ConstructorParameters ) { const session = new Session(...sessionArgs); - SessionUpdateListeners.set(session, new Set()); - SessionSendMessageCallbacks.set(session, new Map()); + sessionUpdateListeners.set(session, new Set()); + sessionSendMessageCallbacks.set(session, new Map()); - SessionSendMediaMessage.set(session, message => { + // Record of pending media requests + // FIXME: Handle request timeouts + const mediaRequests = new Map(); + sessionMediaRequests.set(session, mediaRequests); + + // Current media request ID + let mediaRequestId = 1; + + /** + * Stores callbacks for request response, then adds current request + * ID to the message and sends it. + */ + sessionSendMediaMessage.set(session, message => { return new Promise((resolve, reject) => { - session.sendMessage( - NS_MEDIA, - { ...message, requestId: 0 }, - resolve, - reject - ); + const requestId = mediaRequestId++; + const request: MediaRequest = { + successCallback: () => { + mediaRequests.delete(requestId); + resolve(); + }, + errorCallback: () => { + mediaRequests.delete(requestId); + reject(); + }, + message: { ...message, requestId }, + requestId + }; + + mediaRequests.set(request.requestId, request); + session.sendMessage(NS_MEDIA, request.message, undefined, () => { + mediaRequests.delete(requestId); + reject(); + }); }); }); @@ -142,36 +177,43 @@ export default class Session { #loadMediaErrorCallback?: (err: CastError) => void; get #messageListeners() { - const messageListeners = SessionMessageListeners.get(this); + const messageListeners = sessionMessageListeners.get(this); if (!messageListeners) throw logger.error("Missing session message listeners!"); return messageListeners; } get #updateListeners() { - const updateListeners = SessionUpdateListeners.get(this); + const updateListeners = sessionUpdateListeners.get(this); if (!updateListeners) throw logger.error("Missing session update listeners!"); return updateListeners; } get #sendMessageCallbacks() { - const sendMessageCallback = SessionSendMessageCallbacks.get(this); + const sendMessageCallback = sessionSendMessageCallbacks.get(this); if (!sendMessageCallback) throw logger.error("Missing session sendMessage callback!"); return sendMessageCallback; } get #sendMediaMessage() { - const sendMediaMessage = SessionSendMediaMessage.get(this); + const sendMediaMessage = sessionSendMediaMessage.get(this); if (!sendMediaMessage) throw logger.error("Missing send media message function!"); return sendMediaMessage; } + get #mediaRequests() { + const mediaRequests = sessionMediaRequests.get(this); + if (!mediaRequests) + throw logger.error("Missing session media requests!"); + return mediaRequests; + } + get #leaveSuccessCallback() { - return SessionLeaveSuccessCallback.get(this); + return sessionLeaveSuccessCallback.get(this); } set #leaveSuccessCallback(successCallback: Optional<() => void>) { - SessionLeaveSuccessCallback.set(this, successCallback); + sessionLeaveSuccessCallback.set(this, successCallback); } media: Media[] = []; @@ -190,42 +232,48 @@ export default class Session { ) { this.transportId = sessionId || ""; - SessionMessageListeners.set(this, new Map()); + sessionMessageListeners.set(this, new Map()); this.addMessageListener(NS_MEDIA, this.#mediaMessageListener); } #mediaMessageListener = (namespace: string, messageString: string) => { if (namespace !== NS_MEDIA) return; - const message: ReceiverMediaMessage = JSON.parse(messageString); - switch (message.type) { - case "MEDIA_STATUS": { - // Update media - for (const mediaStatus of message.status) { - let media = this.media.find( - media => - media.mediaSessionId === mediaStatus.mediaSessionId - ); + if (message.type !== "MEDIA_STATUS") return; - // Handle Media creation - if (!media) { - media = createMedia( - [this.sessionId, mediaStatus.mediaSessionId], - this.#sendMediaMessage - ); + for (const status of message.status) { + let media = this.media.find( + media => media.mediaSessionId === status.mediaSessionId + ); - this.media.push(media); - updateMedia(media, mediaStatus); - this.#loadMediaSuccessCallback?.(media); - } else { - updateMedia(media, mediaStatus); - const updateListeners = MediaUpdateListeners.get(media); - if (updateListeners) { - for (const listener of updateListeners) { - listener(true); - } - } - } + if (!media) { + media = createMedia( + [this.sessionId, status.mediaSessionId], + this.#sendMediaMessage + ); + updateMedia(media, status); + this.media.push(media); + } else { + updateMedia(media, status); + } + } + + // Handle media request responses + const mediaRequest = this.#mediaRequests.get(message.requestId); + if (mediaRequest) { + mediaRequest.successCallback(); + } + + for (const status of message.status) { + const media = this.media.find( + media => media.mediaSessionId === status.mediaSessionId + ); + if (!media) continue; + + const updateListeners = mediaUpdateListeners.get(media); + if (updateListeners) { + for (const listener of updateListeners) { + listener(true); } } } @@ -307,7 +355,11 @@ export default class Session { this.#loadMediaErrorCallback = errorCallback; loadRequest.sessionId = this.sessionId; - this.#sendMediaMessage(loadRequest).catch(errorCallback); + this.#sendMediaMessage(loadRequest) + .then(() => { + successCallback?.(this.media[this.media.length - 1]); + }) + .catch(errorCallback); } queueLoad( diff --git a/ext/src/cast/sdk/index.ts b/ext/src/cast/sdk/index.ts index 856e920..91a4b49 100644 --- a/ext/src/cast/sdk/index.ts +++ b/ext/src/cast/sdk/index.ts @@ -33,11 +33,11 @@ import { import Session, { createSession, - SessionLeaveSuccessCallback, - SessionMessageListeners, - SessionSendMediaMessage, - SessionSendMessageCallbacks, - SessionUpdateListeners, + sessionLeaveSuccessCallback, + sessionMessageListeners, + sessionSendMediaMessage, + sessionSendMessageCallbacks, + sessionUpdateListeners, updateMedia } from "./Session"; @@ -186,7 +186,7 @@ export default class { const media = createMedia( [status.sessionId, status.media.mediaSessionId], // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - SessionSendMediaMessage.get(session)! + sessionSendMediaMessage.get(session)! ); updateMedia(media, status.media); session.media = [media]; @@ -223,7 +223,7 @@ export default class { session.namespaces = status.namespaces; session.receiver.volume = status.volume; - const updateListeners = SessionUpdateListeners.get(session); + const updateListeners = sessionUpdateListeners.get(session); if (updateListeners) { for (const listener of updateListeners) { listener(session.status !== SessionStatus.STOPPED); @@ -238,7 +238,7 @@ export default class { if (session?.status === SessionStatus.CONNECTED) { session.status = SessionStatus.STOPPED; - const updateListeners = SessionUpdateListeners.get(session); + const updateListeners = sessionUpdateListeners.get(session); if (updateListeners) { for (const listener of updateListeners) { listener(false); @@ -254,9 +254,9 @@ export default class { if (session?.status === SessionStatus.CONNECTED) { session.status = SessionStatus.DISCONNECTED; - SessionLeaveSuccessCallback.get(session)?.(); + sessionLeaveSuccessCallback.get(session)?.(); - const updateListeners = SessionUpdateListeners.get(session); + const updateListeners = sessionUpdateListeners.get(session); if (updateListeners) { for (const listener of updateListeners) { listener(true); @@ -271,8 +271,9 @@ export default class { const { sessionId, namespace, messageData } = message.data; const session = this.#sessions.get(sessionId); if (session) { - const listeners = - SessionMessageListeners.get(session)?.get(namespace); + const listeners = sessionMessageListeners + .get(session) + ?.get(namespace); if (listeners) { for (const listener of listeners) { listener(namespace, messageData); @@ -291,8 +292,9 @@ export default class { break; } - const sendMessageCallback = - SessionSendMessageCallbacks.get(session)?.get(messageId); + const sendMessageCallback = sessionSendMessageCallbacks + .get(session) + ?.get(messageId); if (sendMessageCallback) { const [successCallback, errorCallback] = sendMessageCallback; diff --git a/ext/src/cast/sdk/media/Media.ts b/ext/src/cast/sdk/media/Media.ts index d4f8563..efe5c0b 100644 --- a/ext/src/cast/sdk/media/Media.ts +++ b/ext/src/cast/sdk/media/Media.ts @@ -38,9 +38,9 @@ type MediaMessageCallback = ( message: DistributiveOmit ) => Promise; -const MediaMessageCallbacks = new WeakMap(); -export const MediaUpdateListeners = new WeakMap>(); -export const MediaLastUpdateTimes = new WeakMap(); +const mediaMessageCallbacks = new WeakMap(); +export const mediaUpdateListeners = new WeakMap>(); +export const mediaLastUpdateTimes = new WeakMap(); /** Creates a Media object and initializes private data. */ export function createMedia( @@ -48,9 +48,9 @@ export function createMedia( mediaMessageCallback: MediaMessageCallback ) { const media = new Media(...mediaArgs); - MediaMessageCallbacks.set(media, mediaMessageCallback); - MediaUpdateListeners.set(media, new Set()); - MediaLastUpdateTimes.set(media, 0); + mediaMessageCallbacks.set(media, mediaMessageCallback); + mediaUpdateListeners.set(media, new Set()); + mediaLastUpdateTimes.set(media, 0); return media; } @@ -61,18 +61,18 @@ export default class Media { #id = uuid(); get #updateListeners() { - const updateListeners = MediaUpdateListeners.get(this); + const updateListeners = mediaUpdateListeners.get(this); if (!updateListeners) throw logger.error("Missing media update listeners!"); return updateListeners; } get #mediaMessageCallback() { - const callback = MediaMessageCallbacks.get(this); + const callback = mediaMessageCallbacks.get(this); if (!callback) throw logger.error("Missing media message callback!"); return callback; } get #lastUpdateTime() { - const lastUpdateTime = MediaLastUpdateTimes.get(this); + const lastUpdateTime = mediaLastUpdateTimes.get(this); if (lastUpdateTime === undefined) throw logger.error("Missing last update time!"); return lastUpdateTime; @@ -127,18 +127,15 @@ export default class Media { * information reported by the receiver. */ getEstimatedBreakClipTime() { - if (!this.breakStatus?.currentBreakClipTime) return; + if (this.breakStatus?.currentBreakClipTime === undefined) return; + if (this.playerState === PlayerState.PLAYING) { + return getEstimatedTime({ + currentTime: this.breakStatus.currentBreakClipTime, + lastUpdateTime: this.#lastUpdateTime + }); + } - const currentBreakClip = this.media?.breakClips?.find( - breakClip => breakClip.id === this.breakStatus?.breakClipId - ); - if (!currentBreakClip) return; - - return getEstimatedTime({ - currentTime: this.breakStatus.currentBreakClipTime, - lastUpdateTime: this.#lastUpdateTime, - duration: currentBreakClip.duration - }); + return this.breakStatus.currentBreakClipTime; } /** @@ -146,18 +143,15 @@ export default class Media { * information reported by the receiver. */ getEstimatedBreakTime() { - if (!this.breakStatus?.currentBreakTime) return; + if (this.breakStatus?.currentBreakTime === undefined) return; + if (this.playerState === PlayerState.PLAYING) { + return getEstimatedTime({ + currentTime: this.breakStatus.currentBreakTime, + lastUpdateTime: this.#lastUpdateTime + }); + } - const currentBreak = this.media?.breaks?.find( - break_ => break_.id === this.breakStatus?.breakId - ); - if (!currentBreak) return; - - return getEstimatedTime({ - currentTime: this.breakStatus.currentBreakTime, - lastUpdateTime: this.#lastUpdateTime, - duration: currentBreak.duration - }); + return this.breakStatus.currentBreakTime; } getEstimatedLiveSeekableRange() { @@ -173,6 +167,7 @@ export default class Media { return getEstimatedTime({ currentTime: this.currentTime, lastUpdateTime: this.#lastUpdateTime, + playbackRate: this.playbackRate, duration: this.media?.duration }); } diff --git a/ext/src/cast/sdk/types.ts b/ext/src/cast/sdk/types.ts index f2b34a0..8923c42 100644 --- a/ext/src/cast/sdk/types.ts +++ b/ext/src/cast/sdk/types.ts @@ -4,7 +4,12 @@ */ import type { SenderApplication, Volume, Image } from "./classes"; -import type { MediaInfo, QueueItem } from "./media/classes"; +import type { + BreakStatus, + LiveSeekableRange, + MediaInfo, + QueueItem +} from "./media/classes"; import type { IdleReason, PlayerState, @@ -14,18 +19,20 @@ import type { export interface MediaStatus { activeTrackIds?: number[]; + breakStatus?: BreakStatus; currentItemId?: number; - mediaSessionId: number; - media?: MediaInfo; - playbackRate: number; - playerState: PlayerState; + currentTime: Nullable; + customData: unknown; idleReason?: IdleReason; items?: QueueItem[]; - currentTime: Nullable; - supportedMediaCommands: number; + liveSeekableRange?: LiveSeekableRange; + media?: MediaInfo; + mediaSessionId: number; + playbackRate: number; + playerState: PlayerState; repeatMode?: RepeatMode; + supportedMediaCommands: number; volume: Volume; - customData: unknown; } export interface ReceiverApplication { diff --git a/ext/src/cast/utils.ts b/ext/src/cast/utils.ts index 8f6a155..cdde5b1 100644 --- a/ext/src/cast/utils.ts +++ b/ext/src/cast/utils.ts @@ -77,11 +77,13 @@ export function convertSupportedMediaCommandsFlags(flags: _MediaCommand) { interface GetEstimatedTimeOpts { currentTime: number; lastUpdateTime: number; + playbackRate?: number; duration?: Nullable; } export function getEstimatedTime(opts: GetEstimatedTimeOpts) { let estimatedTime = - opts.currentTime + (Date.now() - opts.lastUpdateTime) / 1000; + opts.currentTime + + (opts.playbackRate ?? 1) * ((Date.now() - opts.lastUpdateTime) / 1000); // Enforce valid range if (estimatedTime < 0) {