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 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<string, Set<MessageListener>>
>();
export const SessionUpdateListeners = new WeakMap<
export const sessionUpdateListeners = new WeakMap<
Session,
Set<UpdateListener>
>();
export const SessionSendMessageCallbacks = new WeakMap<
export const sessionSendMessageCallbacks = new WeakMap<
Session,
Map<string, SendMessageCallback>
>();
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<SenderMediaMessage, "requestId">
) => 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. */
export function createSession(
sessionArgs: ConstructorParameters<typeof Session>
) {
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<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) => {
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(

View File

@@ -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;

View File

@@ -38,9 +38,9 @@ type MediaMessageCallback = (
message: DistributiveOmit<SenderMediaMessage, "requestId">
) => Promise<void>;
const MediaMessageCallbacks = new WeakMap<Media, MediaMessageCallback>();
export const MediaUpdateListeners = new WeakMap<Media, Set<UpdateListener>>();
export const MediaLastUpdateTimes = new WeakMap<Media, number>();
const mediaMessageCallbacks = new WeakMap<Media, MediaMessageCallback>();
export const mediaUpdateListeners = new WeakMap<Media, Set<UpdateListener>>();
export const mediaLastUpdateTimes = new WeakMap<Media, number>();
/** 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
});
}

View File

@@ -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<number>;
customData: unknown;
idleReason?: IdleReason;
items?: QueueItem[];
currentTime: Nullable<number>;
supportedMediaCommands: number;
liveSeekableRange?: LiveSeekableRange;
media?: MediaInfo;
mediaSessionId: number;
playbackRate: number;
playerState: PlayerState;
repeatMode?: RepeatMode;
supportedMediaCommands: number;
volume: Volume;
customData: unknown;
}
export interface ReceiverApplication {

View File

@@ -77,11 +77,13 @@ export function convertSupportedMediaCommandsFlags(flags: _MediaCommand) {
interface GetEstimatedTimeOpts {
currentTime: number;
lastUpdateTime: number;
playbackRate?: number;
duration?: Nullable<number>;
}
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) {