"use strict"; import { v1 as uuid } from "uuid"; import logger from "../../../lib/logger"; import { Volume, Error as _Error } from "../dataClasses"; import { BreakStatus, EditTracksInfoRequest, GetStatusRequest, LiveSeekableRange, MediaInfo, PauseRequest, PlayRequest, QueueData, QueueJumpRequest, QueueInsertItemsRequest, QueueItem, QueueSetPropertiesRequest, QueueRemoveItemsRequest, QueueReorderItemsRequest, QueueUpdateItemsRequest, SeekRequest, StopRequest, VideoInformation, VolumeRequest } from "./dataClasses"; import { PlayerState, RepeatMode } from "./enums"; import { ErrorCode } from "../enums"; import { ErrorCallback, SuccessCallback, UpdateListener } from "../../types"; import { SenderMediaMessage } from "../types"; export default class Media { #id = uuid(); // Timestamp of last status update _lastUpdateTime = 0; _updateListeners = new Set(); activeTrackIds: Nullable = null; breakStatus?: BreakStatus; currentTime = 0; customData: any = null; idleReason: Nullable = null; liveSeekableRange?: LiveSeekableRange; media: Nullable = null; playbackRate = 1; playerState = PlayerState.IDLE; repeatMode = RepeatMode.OFF; supportedMediaCommands: string[] = []; videoInfo?: VideoInformation; volume: Volume = new Volume(); // Queues items: Nullable = null; currentItemId: Nullable = null; loadingItemId: Nullable = null; preloadedItemId: Nullable = null; queueData?: QueueData; constructor( public sessionId: string, public mediaSessionId: number, public _sendMediaMessage: ( message: DistributiveOmit ) => Promise ) {} addUpdateListener(listener: UpdateListener) { this._updateListeners.add(listener); } removeUpdateListener(listener: UpdateListener) { this._updateListeners.delete(listener); } editTracksInfo( editTracksInfoRequest: EditTracksInfoRequest, successCallback?: SuccessCallback, errorCallback?: ErrorCallback ) { this._sendMediaMessage({ ...editTracksInfoRequest, type: "EDIT_TRACKS_INFO", mediaSessionId: this.mediaSessionId }) .then(successCallback) .catch(errorCallback); } getEstimatedBreakClipTime() { logger.info("STUB :: Media#getEstimatedBreakClipTime"); } getEstimatedBreakTime() { logger.info("STUB :: Media#getEstimatedBreakTime"); } getEstimatedLiveSeekableRange() { logger.info("STUB :: Media#getEstimatedLiveSeekableRange"); } /** * Estimate the current playback position based on the last * time reported by the receiver and the current playback * rate. */ getEstimatedTime(): number { if (this.playerState === PlayerState.PLAYING && this._lastUpdateTime) { let estimatedTime = this.currentTime + (Date.now() - this._lastUpdateTime) / 1000; // Enforce valid range if (estimatedTime < 0) { estimatedTime = 0; } else if ( this.media?.duration && estimatedTime > this.media.duration ) { estimatedTime = this.media.duration; } return estimatedTime; } return this.currentTime; } /** * Request media status from the receiver application. This * will also trigger any added media update listeners. */ getStatus( getStatusRequest = new GetStatusRequest(), successCallback?: SuccessCallback, errorCallback?: ErrorCallback ) { this._sendMediaMessage({ ...getStatusRequest, type: "MEDIA_GET_STATUS", mediaSessionId: this.mediaSessionId }) .then(successCallback) .catch(errorCallback); } pause( pauseRequest = new PauseRequest(), successCallback?: SuccessCallback, errorCallback?: ErrorCallback ) { this._sendMediaMessage({ ...pauseRequest, type: "PAUSE", mediaSessionId: this.mediaSessionId }) .then(successCallback) .catch(errorCallback); } play( playRequest = new PlayRequest(), successCallback?: SuccessCallback, errorCallback?: ErrorCallback ) { this._sendMediaMessage({ ...playRequest, type: "PLAY", mediaSessionId: this.mediaSessionId }) .then(successCallback) .catch(errorCallback); } queueAppendItem( item: QueueItem, successCallback?: SuccessCallback, errorCallback?: ErrorCallback ) { this._sendMediaMessage({ ...new QueueInsertItemsRequest([item]), type: "QUEUE_INSERT", sessionId: this.sessionId, mediaSessionId: this.mediaSessionId }) .then(successCallback) .catch(errorCallback); } queueInsertItems( queueInsertItemsRequest: QueueInsertItemsRequest, successCallback?: SuccessCallback, errorCallback?: ErrorCallback ) { this._sendMediaMessage({ ...queueInsertItemsRequest, type: "QUEUE_INSERT", sessionId: this.sessionId, mediaSessionId: this.mediaSessionId }) .then(successCallback) .catch(errorCallback); } queueJumpToItem( itemId: number, successCallback?: SuccessCallback, errorCallback?: ErrorCallback ) { if (this.items?.find(item => item.itemId === itemId)) { const jumpRequest = new QueueJumpRequest(); jumpRequest.currentItemId = itemId; this._sendMediaMessage({ ...jumpRequest, type: "QUEUE_UPDATE", sessionId: this.sessionId, mediaSessionId: this.mediaSessionId }) .then(successCallback) .catch(errorCallback); } } queueMoveItemToNewIndex( itemId: number, newIndex: number, successCallback?: SuccessCallback, errorCallback?: ErrorCallback ) { // Return early if not in queue if (!this.items) { return; } const itemIndex = this.items.findIndex(item => item.itemId === itemId); if (itemIndex !== -1) { // New index must not be negative if (newIndex < 0) { if (errorCallback) { errorCallback(new _Error(ErrorCode.INVALID_PARAMETER)); } } else if (newIndex == itemIndex) { if (successCallback) { successCallback(); } } } else { if (newIndex > itemIndex) { newIndex++; } const reorderItemsRequest = new QueueReorderItemsRequest([itemId]); if (newIndex < this.items.length) { const existingItem = this.items[newIndex]; reorderItemsRequest.insertBefore = existingItem.itemId; } this._sendMediaMessage({ ...reorderItemsRequest, type: "QUEUE_REORDER", sessionId: this.sessionId, mediaSessionId: this.mediaSessionId }) .then(successCallback) .catch(errorCallback); } } queueNext( successCallback?: SuccessCallback, errorCallback?: ErrorCallback ) { const jumpRequest = new QueueJumpRequest(); jumpRequest.jump = 1; this._sendMediaMessage({ ...jumpRequest, type: "QUEUE_UPDATE", sessionId: this.sessionId, mediaSessionId: this.mediaSessionId }) .then(successCallback) .catch(errorCallback); } queuePrev( successCallback?: SuccessCallback, errorCallback?: ErrorCallback ) { const jumpRequest = new QueueJumpRequest(); jumpRequest.jump = -1; this._sendMediaMessage({ ...jumpRequest, type: "QUEUE_UPDATE", sessionId: this.sessionId, mediaSessionId: this.mediaSessionId }) .then(successCallback) .catch(errorCallback); } queueRemoveItem( itemId: number, successCallback?: SuccessCallback, errorCallback?: ErrorCallback ) { const item = this.items?.find(item => item.itemId === itemId); if (item) { this.queueRemoveItems( new QueueRemoveItemsRequest([itemId]), successCallback, errorCallback ); } } queueRemoveItems( queueRemoveItemsRequest: QueueRemoveItemsRequest, successCallback?: SuccessCallback, errorCallback?: ErrorCallback ) { this._sendMediaMessage({ ...queueRemoveItemsRequest, mediaSessionId: this.mediaSessionId, type: "QUEUE_REMOVE", sessionId: this.sessionId }) .then(successCallback) .catch(errorCallback); } queueReorderItems( queueReorderItemsRequest: QueueReorderItemsRequest, successCallback?: SuccessCallback, errorCallback?: ErrorCallback ) { this._sendMediaMessage({ ...queueReorderItemsRequest, mediaSessionId: this.mediaSessionId, type: "QUEUE_REORDER", sessionId: this.sessionId }) .then(successCallback) .catch(errorCallback); } queueSetRepeatMode( repeatMode: string, successCallback?: SuccessCallback, errorCallback?: ErrorCallback ) { const setPropertiesRequest = new QueueSetPropertiesRequest(); setPropertiesRequest.repeatMode = repeatMode; this._sendMediaMessage({ ...setPropertiesRequest, type: "QUEUE_UPDATE", sessionId: this.sessionId, mediaSessionId: this.mediaSessionId }) .then(successCallback) .catch(errorCallback); } queueUpdateItems( queueUpdateItemsRequest: QueueUpdateItemsRequest, successCallback?: SuccessCallback, errorCallback?: ErrorCallback ) { this._sendMediaMessage({ ...queueUpdateItemsRequest, type: "QUEUE_UPDATE", sessionId: this.sessionId, mediaSessionId: this.mediaSessionId }) .then(successCallback) .catch(errorCallback); } seek( seekRequest: SeekRequest, successCallback?: SuccessCallback, errorCallback?: ErrorCallback ) { this._sendMediaMessage({ ...seekRequest, type: "SEEK", mediaSessionId: this.mediaSessionId }) .then(successCallback) .catch(errorCallback); } setVolume( volumeRequest: VolumeRequest, successCallback?: SuccessCallback, errorCallback?: ErrorCallback ) { this._sendMediaMessage({ ...volumeRequest, type: "MEDIA_SET_VOLUME", mediaSessionId: this.mediaSessionId }) .then(successCallback) .catch(errorCallback); } stop( stopRequest?: StopRequest, successCallback?: SuccessCallback, errorCallback?: ErrorCallback ) { if (!stopRequest) { stopRequest = new StopRequest(); } this._sendMediaMessage({ ...stopRequest, type: "STOP", mediaSessionId: this.mediaSessionId }) .then(() => { if (successCallback) { successCallback(); } }) .catch(errorCallback); } supportsCommand(command: string): boolean { return this.supportedMediaCommands.includes(command); } }