"use strict"; import { v4 as uuid } from "uuid"; import logger from "../../lib/logger"; import eventMessaging from "../eventMessaging"; import type { ErrorCallback, LoadSuccessCallback, MediaListener, MessageListener, SuccessCallback, UpdateListener } from "../types"; import { MediaStatus, ReceiverMediaMessage, SenderMediaMessage, SenderMessage, _MediaCommand } from "./types"; import { SessionStatus } from "./enums"; import type { Image, Receiver, SenderApplication } from "./classes"; import { MediaCommand } from "./media/enums"; import type { LoadRequest, QueueLoadRequest, QueueItem } from "./media/classes"; import Media, { NS_MEDIA } from "./media/Media"; /** * Takes a media object and a media status object and merges the status * with the existing media object, updating it with new properties. */ function updateMedia(media: Media, status: MediaStatus) { if (status.currentTime) { media._lastUpdateTime = Date.now(); } // Copy basic props if (status.currentTime) media.currentTime = status.currentTime; if (status.customData) media.customData = status.customData; if (status.idleReason) media.idleReason = status.idleReason; if (status.media) media.media = status.media; if (status.mediaSessionId) media.mediaSessionId = status.mediaSessionId; if (status.playbackRate) media.playbackRate = status.playbackRate; if (status.playerState) media.playerState = status.playerState; if (status.repeatMode) media.repeatMode = status.repeatMode; if (status.volume) media.volume = status.volume; // Convert supportedMediaCommands bitflags to string array const supportedMediaCommands: string[] = []; if (status.supportedMediaCommands & _MediaCommand.PAUSE) { supportedMediaCommands.push(MediaCommand.PAUSE); } else if (status.supportedMediaCommands & _MediaCommand.SEEK) { supportedMediaCommands.push(MediaCommand.SEEK); } else if (status.supportedMediaCommands & _MediaCommand.STREAM_VOLUME) { supportedMediaCommands.push(MediaCommand.STREAM_VOLUME); } else if (status.supportedMediaCommands & _MediaCommand.STREAM_MUTE) { supportedMediaCommands.push(MediaCommand.STREAM_MUTE); } else if (status.supportedMediaCommands & _MediaCommand.QUEUE_NEXT) { supportedMediaCommands.push("queue_next"); } else if (status.supportedMediaCommands & _MediaCommand.QUEUE_PREV) { supportedMediaCommands.push("queue_prev"); } media.supportedMediaCommands = supportedMediaCommands; // Update queue state if (status.items) { const newItems: QueueItem[] = []; for (const newItem of status.items) { if (!newItem.media) { // Existing queue item with the same ID const existingItem = media.items?.find( item => item.itemId === newItem.itemId ); /** * Use existing queue item's media info if available * otherwise, if the current queue item, use the main * media item. */ if (existingItem?.media) { newItem.media = existingItem.media; } else if ( media.media && newItem.itemId === media.currentItemId ) { newItem.media = media.media; } } newItems.push(newItem); } media.items = newItems; } } export default class Session { #loadMediaSuccessCallback?: (media: Media) => void; #loadMediaErrorCallback?: ErrorCallback; #loadMediaRequest?: LoadRequest; _messageListeners = new Map>(); _updateListeners = new Set(); _sendMessageCallbacks = new Map< string, [SuccessCallback?, ErrorCallback?] >(); media: Media[] = []; namespaces: Array<{ name: string }> = []; senderApps: SenderApplication[] = []; status = SessionStatus.CONNECTED; statusText: Nullable = null; transportId: string; constructor( public sessionId: string, public appId: string, public displayName: string, public appImages: Image[], public receiver: Receiver ) { this.transportId = sessionId || ""; 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 ); // Handle Media creation if (!media) { media = new Media( this.sessionId, mediaStatus.mediaSessionId, this.#sendMediaMessage ); this.media.push(media); this.#loadMediaSuccessCallback?.(media); } updateMedia(media, mediaStatus); for (const listener of media._updateListeners) { listener(true); } break; } } } }; /** * Sends a media message to the app receiver. */ #sendMediaMessage = ( message: DistributiveOmit ) => { return new Promise((resolve, reject) => { this.sendMessage( NS_MEDIA, { ...message, requestId: 0 }, resolve, reject ); }); }; #sendReceiverMessage = ( message: DistributiveOmit ) => { return new Promise((resolve, reject) => { const messageId = uuid(); eventMessaging.page.sendMessage({ subject: "bridge:sendCastReceiverMessage", data: { sessionId: this.sessionId, messageData: message as SenderMessage, messageId } }); this._sendMessageCallbacks.set(messageId, [resolve, reject]); }); }; addMediaListener(_mediaListener: MediaListener) { logger.info("STUB :: Session#addMediaListener"); } removeMediaListener(_mediaListener: MediaListener) { logger.info("STUB :: Session#removeMediaListener"); } addMessageListener(namespace: string, listener: MessageListener) { if (!this._messageListeners.has(namespace)) { this._messageListeners.set(namespace, new Set()); } this._messageListeners.get(namespace)?.add(listener); } removeMessageListener(namespace: string, listener: MessageListener) { this._messageListeners.get(namespace)?.delete(listener); } addUpdateListener(listener: UpdateListener) { this._updateListeners.add(listener); } removeUpdateListener(listener: UpdateListener) { this._updateListeners.delete(listener); } leave(_successCallback?: SuccessCallback, _errorCallback?: ErrorCallback) { logger.info("STUB :: Session#leave"); } loadMedia( loadRequest: LoadRequest, successCallback?: LoadSuccessCallback, errorCallback?: ErrorCallback ) { this.#loadMediaSuccessCallback = successCallback; this.#loadMediaErrorCallback = errorCallback; loadRequest.sessionId = this.sessionId; this.#sendMediaMessage(loadRequest).catch(errorCallback); } queueLoad( _queueLoadRequest: QueueLoadRequest, _successCallback?: LoadSuccessCallback, _errorCallback?: ErrorCallback ) { logger.info("STUB :: Session#queueLoad"); } sendMessage( namespace: string, message: object | string, successCallback?: SuccessCallback, errorCallback?: ErrorCallback ) { const messageId = uuid(); eventMessaging.page.sendMessage({ subject: "bridge:sendCastSessionMessage", data: { sessionId: this.sessionId, namespace, messageData: message, messageId } }); this._sendMessageCallbacks.set(messageId, [ successCallback, errorCallback ]); } setReceiverMuted( muted: boolean, successCallback?: SuccessCallback, errorCallback?: ErrorCallback ) { this.#sendReceiverMessage({ type: "SET_VOLUME", volume: { muted } }) .then(successCallback) .catch(errorCallback); } setReceiverVolumeLevel( newLevel: number, successCallback?: SuccessCallback, errorCallback?: ErrorCallback ) { this.#sendReceiverMessage({ type: "SET_VOLUME", volume: { level: newLevel } }) .then(successCallback) .catch(errorCallback); } stop(successCallback?: SuccessCallback, errorCallback?: ErrorCallback) { this.#sendReceiverMessage({ type: "STOP", sessionId: this.sessionId }) .then(successCallback) .catch(errorCallback); } }