Minor media session tweaks

This commit is contained in:
hensm
2022-09-12 00:45:25 +01:00
parent 1baa41a981
commit f76468bfd9
5 changed files with 164 additions and 106 deletions

View File

@@ -23,8 +23,8 @@ import {
import type { LoadRequest, QueueLoadRequest, QueueItem } from "./media/classes"; import type { LoadRequest, QueueLoadRequest, QueueItem } from "./media/classes";
import Media, { import Media, {
createMedia, createMedia,
MediaLastUpdateTimes, mediaLastUpdateTimes,
MediaUpdateListeners, mediaUpdateListeners,
NS_MEDIA NS_MEDIA
} from "./media/Media"; } from "./media/Media";
@@ -36,10 +36,11 @@ const logger = new Logger("fx_cast [sdk :: cast.Session]");
*/ */
export function updateMedia(media: Media, status: MediaStatus) { export function updateMedia(media: Media, status: MediaStatus) {
if (status.currentTime) { if (status.currentTime) {
MediaLastUpdateTimes.set(media, Date.now()); mediaLastUpdateTimes.set(media, Date.now());
} }
// Copy basic props // Copy basic props
if (status.breakStatus) media.breakStatus = status.breakStatus;
if (status.currentTime) media.currentTime = status.currentTime; if (status.currentTime) media.currentTime = status.currentTime;
if (status.customData) media.customData = status.customData; if (status.customData) media.customData = status.customData;
if (status.idleReason) media.idleReason = status.idleReason; 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, Session,
Map<string, Set<MessageListener>> Map<string, Set<MessageListener>>
>(); >();
export const SessionUpdateListeners = new WeakMap< export const sessionUpdateListeners = new WeakMap<
Session, Session,
Set<UpdateListener> Set<UpdateListener>
>(); >();
export const SessionSendMessageCallbacks = new WeakMap< export const sessionSendMessageCallbacks = new WeakMap<
Session, Session,
Map<string, SendMessageCallback> Map<string, SendMessageCallback>
>(); >();
export const SessionLeaveSuccessCallback = new WeakMap< export const sessionLeaveSuccessCallback = new WeakMap<
Session, Session,
Optional<() => void> Optional<() => void>
>(); >();
@@ -108,24 +109,58 @@ export const SessionLeaveSuccessCallback = new WeakMap<
type SendMediaMessage = ( type SendMediaMessage = (
message: DistributiveOmit<SenderMediaMessage, "requestId"> message: DistributiveOmit<SenderMediaMessage, "requestId">
) => Promise<void>; ) => Promise<void>;
export const SessionSendMediaMessage = new WeakMap<Session, SendMediaMessage>(); export const sessionSendMediaMessage = new WeakMap<Session, SendMediaMessage>();
interface MediaRequest {
successCallback: () => void;
errorCallback: (error: CastError) => void;
message: SenderMediaMessage;
requestId: number;
}
const sessionMediaRequests = new WeakMap<Session, Map<number, MediaRequest>>();
/** Creates a Session object and initializes private data. */ /** Creates a Session object and initializes private data. */
export function createSession( export function createSession(
sessionArgs: ConstructorParameters<typeof Session> sessionArgs: ConstructorParameters<typeof Session>
) { ) {
const session = new Session(...sessionArgs); const session = new Session(...sessionArgs);
SessionUpdateListeners.set(session, new Set()); sessionUpdateListeners.set(session, new Set());
SessionSendMessageCallbacks.set(session, new Map()); sessionSendMessageCallbacks.set(session, new Map());
SessionSendMediaMessage.set(session, message => { // Record of pending media requests
// FIXME: Handle request timeouts
const mediaRequests = new Map<number, MediaRequest>();
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<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
session.sendMessage( const requestId = mediaRequestId++;
NS_MEDIA, const request: MediaRequest = {
{ ...message, requestId: 0 }, successCallback: () => {
resolve, mediaRequests.delete(requestId);
reject 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; #loadMediaErrorCallback?: (err: CastError) => void;
get #messageListeners() { get #messageListeners() {
const messageListeners = SessionMessageListeners.get(this); const messageListeners = sessionMessageListeners.get(this);
if (!messageListeners) if (!messageListeners)
throw logger.error("Missing session message listeners!"); throw logger.error("Missing session message listeners!");
return messageListeners; return messageListeners;
} }
get #updateListeners() { get #updateListeners() {
const updateListeners = SessionUpdateListeners.get(this); const updateListeners = sessionUpdateListeners.get(this);
if (!updateListeners) if (!updateListeners)
throw logger.error("Missing session update listeners!"); throw logger.error("Missing session update listeners!");
return updateListeners; return updateListeners;
} }
get #sendMessageCallbacks() { get #sendMessageCallbacks() {
const sendMessageCallback = SessionSendMessageCallbacks.get(this); const sendMessageCallback = sessionSendMessageCallbacks.get(this);
if (!sendMessageCallback) if (!sendMessageCallback)
throw logger.error("Missing session sendMessage callback!"); throw logger.error("Missing session sendMessage callback!");
return sendMessageCallback; return sendMessageCallback;
} }
get #sendMediaMessage() { get #sendMediaMessage() {
const sendMediaMessage = SessionSendMediaMessage.get(this); const sendMediaMessage = sessionSendMediaMessage.get(this);
if (!sendMediaMessage) if (!sendMediaMessage)
throw logger.error("Missing send media message function!"); throw logger.error("Missing send media message function!");
return sendMediaMessage; return sendMediaMessage;
} }
get #mediaRequests() {
const mediaRequests = sessionMediaRequests.get(this);
if (!mediaRequests)
throw logger.error("Missing session media requests!");
return mediaRequests;
}
get #leaveSuccessCallback() { get #leaveSuccessCallback() {
return SessionLeaveSuccessCallback.get(this); return sessionLeaveSuccessCallback.get(this);
} }
set #leaveSuccessCallback(successCallback: Optional<() => void>) { set #leaveSuccessCallback(successCallback: Optional<() => void>) {
SessionLeaveSuccessCallback.set(this, successCallback); sessionLeaveSuccessCallback.set(this, successCallback);
} }
media: Media[] = []; media: Media[] = [];
@@ -190,42 +232,48 @@ export default class Session {
) { ) {
this.transportId = sessionId || ""; this.transportId = sessionId || "";
SessionMessageListeners.set(this, new Map()); sessionMessageListeners.set(this, new Map());
this.addMessageListener(NS_MEDIA, this.#mediaMessageListener); this.addMessageListener(NS_MEDIA, this.#mediaMessageListener);
} }
#mediaMessageListener = (namespace: string, messageString: string) => { #mediaMessageListener = (namespace: string, messageString: string) => {
if (namespace !== NS_MEDIA) return; if (namespace !== NS_MEDIA) return;
const message: ReceiverMediaMessage = JSON.parse(messageString); const message: ReceiverMediaMessage = JSON.parse(messageString);
switch (message.type) { if (message.type !== "MEDIA_STATUS") return;
case "MEDIA_STATUS": {
// Update media
for (const mediaStatus of message.status) {
let media = this.media.find(
media =>
media.mediaSessionId === mediaStatus.mediaSessionId
);
// Handle Media creation for (const status of message.status) {
if (!media) { let media = this.media.find(
media = createMedia( media => media.mediaSessionId === status.mediaSessionId
[this.sessionId, mediaStatus.mediaSessionId], );
this.#sendMediaMessage
);
this.media.push(media); if (!media) {
updateMedia(media, mediaStatus); media = createMedia(
this.#loadMediaSuccessCallback?.(media); [this.sessionId, status.mediaSessionId],
} else { this.#sendMediaMessage
updateMedia(media, mediaStatus); );
const updateListeners = MediaUpdateListeners.get(media); updateMedia(media, status);
if (updateListeners) { this.media.push(media);
for (const listener of updateListeners) { } else {
listener(true); 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; this.#loadMediaErrorCallback = errorCallback;
loadRequest.sessionId = this.sessionId; loadRequest.sessionId = this.sessionId;
this.#sendMediaMessage(loadRequest).catch(errorCallback); this.#sendMediaMessage(loadRequest)
.then(() => {
successCallback?.(this.media[this.media.length - 1]);
})
.catch(errorCallback);
} }
queueLoad( queueLoad(

View File

@@ -33,11 +33,11 @@ import {
import Session, { import Session, {
createSession, createSession,
SessionLeaveSuccessCallback, sessionLeaveSuccessCallback,
SessionMessageListeners, sessionMessageListeners,
SessionSendMediaMessage, sessionSendMediaMessage,
SessionSendMessageCallbacks, sessionSendMessageCallbacks,
SessionUpdateListeners, sessionUpdateListeners,
updateMedia updateMedia
} from "./Session"; } from "./Session";
@@ -186,7 +186,7 @@ export default class {
const media = createMedia( const media = createMedia(
[status.sessionId, status.media.mediaSessionId], [status.sessionId, status.media.mediaSessionId],
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
SessionSendMediaMessage.get(session)! sessionSendMediaMessage.get(session)!
); );
updateMedia(media, status.media); updateMedia(media, status.media);
session.media = [media]; session.media = [media];
@@ -223,7 +223,7 @@ export default class {
session.namespaces = status.namespaces; session.namespaces = status.namespaces;
session.receiver.volume = status.volume; session.receiver.volume = status.volume;
const updateListeners = SessionUpdateListeners.get(session); const updateListeners = sessionUpdateListeners.get(session);
if (updateListeners) { if (updateListeners) {
for (const listener of updateListeners) { for (const listener of updateListeners) {
listener(session.status !== SessionStatus.STOPPED); listener(session.status !== SessionStatus.STOPPED);
@@ -238,7 +238,7 @@ export default class {
if (session?.status === SessionStatus.CONNECTED) { if (session?.status === SessionStatus.CONNECTED) {
session.status = SessionStatus.STOPPED; session.status = SessionStatus.STOPPED;
const updateListeners = SessionUpdateListeners.get(session); const updateListeners = sessionUpdateListeners.get(session);
if (updateListeners) { if (updateListeners) {
for (const listener of updateListeners) { for (const listener of updateListeners) {
listener(false); listener(false);
@@ -254,9 +254,9 @@ export default class {
if (session?.status === SessionStatus.CONNECTED) { if (session?.status === SessionStatus.CONNECTED) {
session.status = SessionStatus.DISCONNECTED; session.status = SessionStatus.DISCONNECTED;
SessionLeaveSuccessCallback.get(session)?.(); sessionLeaveSuccessCallback.get(session)?.();
const updateListeners = SessionUpdateListeners.get(session); const updateListeners = sessionUpdateListeners.get(session);
if (updateListeners) { if (updateListeners) {
for (const listener of updateListeners) { for (const listener of updateListeners) {
listener(true); listener(true);
@@ -271,8 +271,9 @@ export default class {
const { sessionId, namespace, messageData } = message.data; const { sessionId, namespace, messageData } = message.data;
const session = this.#sessions.get(sessionId); const session = this.#sessions.get(sessionId);
if (session) { if (session) {
const listeners = const listeners = sessionMessageListeners
SessionMessageListeners.get(session)?.get(namespace); .get(session)
?.get(namespace);
if (listeners) { if (listeners) {
for (const listener of listeners) { for (const listener of listeners) {
listener(namespace, messageData); listener(namespace, messageData);
@@ -291,8 +292,9 @@ export default class {
break; break;
} }
const sendMessageCallback = const sendMessageCallback = sessionSendMessageCallbacks
SessionSendMessageCallbacks.get(session)?.get(messageId); .get(session)
?.get(messageId);
if (sendMessageCallback) { if (sendMessageCallback) {
const [successCallback, errorCallback] = const [successCallback, errorCallback] =
sendMessageCallback; sendMessageCallback;

View File

@@ -38,9 +38,9 @@ type MediaMessageCallback = (
message: DistributiveOmit<SenderMediaMessage, "requestId"> message: DistributiveOmit<SenderMediaMessage, "requestId">
) => Promise<void>; ) => Promise<void>;
const MediaMessageCallbacks = new WeakMap<Media, MediaMessageCallback>(); const mediaMessageCallbacks = new WeakMap<Media, MediaMessageCallback>();
export const MediaUpdateListeners = new WeakMap<Media, Set<UpdateListener>>(); export const mediaUpdateListeners = new WeakMap<Media, Set<UpdateListener>>();
export const MediaLastUpdateTimes = new WeakMap<Media, number>(); export const mediaLastUpdateTimes = new WeakMap<Media, number>();
/** Creates a Media object and initializes private data. */ /** Creates a Media object and initializes private data. */
export function createMedia( export function createMedia(
@@ -48,9 +48,9 @@ export function createMedia(
mediaMessageCallback: MediaMessageCallback mediaMessageCallback: MediaMessageCallback
) { ) {
const media = new Media(...mediaArgs); const media = new Media(...mediaArgs);
MediaMessageCallbacks.set(media, mediaMessageCallback); mediaMessageCallbacks.set(media, mediaMessageCallback);
MediaUpdateListeners.set(media, new Set()); mediaUpdateListeners.set(media, new Set());
MediaLastUpdateTimes.set(media, 0); mediaLastUpdateTimes.set(media, 0);
return media; return media;
} }
@@ -61,18 +61,18 @@ export default class Media {
#id = uuid(); #id = uuid();
get #updateListeners() { get #updateListeners() {
const updateListeners = MediaUpdateListeners.get(this); const updateListeners = mediaUpdateListeners.get(this);
if (!updateListeners) if (!updateListeners)
throw logger.error("Missing media update listeners!"); throw logger.error("Missing media update listeners!");
return updateListeners; return updateListeners;
} }
get #mediaMessageCallback() { get #mediaMessageCallback() {
const callback = MediaMessageCallbacks.get(this); const callback = mediaMessageCallbacks.get(this);
if (!callback) throw logger.error("Missing media message callback!"); if (!callback) throw logger.error("Missing media message callback!");
return callback; return callback;
} }
get #lastUpdateTime() { get #lastUpdateTime() {
const lastUpdateTime = MediaLastUpdateTimes.get(this); const lastUpdateTime = mediaLastUpdateTimes.get(this);
if (lastUpdateTime === undefined) if (lastUpdateTime === undefined)
throw logger.error("Missing last update time!"); throw logger.error("Missing last update time!");
return lastUpdateTime; return lastUpdateTime;
@@ -127,18 +127,15 @@ export default class Media {
* information reported by the receiver. * information reported by the receiver.
*/ */
getEstimatedBreakClipTime() { 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( return this.breakStatus.currentBreakClipTime;
breakClip => breakClip.id === this.breakStatus?.breakClipId
);
if (!currentBreakClip) return;
return getEstimatedTime({
currentTime: this.breakStatus.currentBreakClipTime,
lastUpdateTime: this.#lastUpdateTime,
duration: currentBreakClip.duration
});
} }
/** /**
@@ -146,18 +143,15 @@ export default class Media {
* information reported by the receiver. * information reported by the receiver.
*/ */
getEstimatedBreakTime() { 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( return this.breakStatus.currentBreakTime;
break_ => break_.id === this.breakStatus?.breakId
);
if (!currentBreak) return;
return getEstimatedTime({
currentTime: this.breakStatus.currentBreakTime,
lastUpdateTime: this.#lastUpdateTime,
duration: currentBreak.duration
});
} }
getEstimatedLiveSeekableRange() { getEstimatedLiveSeekableRange() {
@@ -173,6 +167,7 @@ export default class Media {
return getEstimatedTime({ return getEstimatedTime({
currentTime: this.currentTime, currentTime: this.currentTime,
lastUpdateTime: this.#lastUpdateTime, lastUpdateTime: this.#lastUpdateTime,
playbackRate: this.playbackRate,
duration: this.media?.duration duration: this.media?.duration
}); });
} }

View File

@@ -4,7 +4,12 @@
*/ */
import type { SenderApplication, Volume, Image } from "./classes"; 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 { import type {
IdleReason, IdleReason,
PlayerState, PlayerState,
@@ -14,18 +19,20 @@ import type {
export interface MediaStatus { export interface MediaStatus {
activeTrackIds?: number[]; activeTrackIds?: number[];
breakStatus?: BreakStatus;
currentItemId?: number; currentItemId?: number;
mediaSessionId: number; currentTime: Nullable<number>;
media?: MediaInfo; customData: unknown;
playbackRate: number;
playerState: PlayerState;
idleReason?: IdleReason; idleReason?: IdleReason;
items?: QueueItem[]; items?: QueueItem[];
currentTime: Nullable<number>; liveSeekableRange?: LiveSeekableRange;
supportedMediaCommands: number; media?: MediaInfo;
mediaSessionId: number;
playbackRate: number;
playerState: PlayerState;
repeatMode?: RepeatMode; repeatMode?: RepeatMode;
supportedMediaCommands: number;
volume: Volume; volume: Volume;
customData: unknown;
} }
export interface ReceiverApplication { export interface ReceiverApplication {

View File

@@ -77,11 +77,13 @@ export function convertSupportedMediaCommandsFlags(flags: _MediaCommand) {
interface GetEstimatedTimeOpts { interface GetEstimatedTimeOpts {
currentTime: number; currentTime: number;
lastUpdateTime: number; lastUpdateTime: number;
playbackRate?: number;
duration?: Nullable<number>; duration?: Nullable<number>;
} }
export function getEstimatedTime(opts: GetEstimatedTimeOpts) { export function getEstimatedTime(opts: GetEstimatedTimeOpts) {
let estimatedTime = let estimatedTime =
opts.currentTime + (Date.now() - opts.lastUpdateTime) / 1000; opts.currentTime +
(opts.playbackRate ?? 1) * ((Date.now() - opts.lastUpdateTime) / 1000);
// Enforce valid range // Enforce valid range
if (estimatedTime < 0) { if (estimatedTime < 0) {