mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-13 02:49:58 +00:00
Rename directory: ext -> extension
This commit is contained in:
487
extension/src/cast/sdk/media/Media.ts
Normal file
487
extension/src/cast/sdk/media/Media.ts
Normal file
@@ -0,0 +1,487 @@
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
import { Logger } from "../../../lib/logger";
|
||||
import { getEstimatedTime } from "../../utils";
|
||||
import type { SenderMediaMessage } from "../types";
|
||||
|
||||
import { Volume, Error as CastError } from "../classes";
|
||||
import { ErrorCode } from "../enums";
|
||||
|
||||
import {
|
||||
BreakStatus,
|
||||
EditTracksInfoRequest,
|
||||
GetStatusRequest,
|
||||
LiveSeekableRange,
|
||||
MediaInfo,
|
||||
PauseRequest,
|
||||
PlayRequest,
|
||||
QueueData,
|
||||
QueueJumpRequest,
|
||||
QueueInsertItemsRequest,
|
||||
QueueItem,
|
||||
QueueSetPropertiesRequest,
|
||||
QueueRemoveItemsRequest,
|
||||
QueueReorderItemsRequest,
|
||||
QueueUpdateItemsRequest,
|
||||
SeekRequest,
|
||||
StopRequest,
|
||||
VideoInformation,
|
||||
VolumeRequest
|
||||
} from "./classes";
|
||||
import { PlayerState, RepeatMode } from "./enums";
|
||||
|
||||
const logger = new Logger("fx_cast [sdk :: cast.Media]");
|
||||
|
||||
export const NS_MEDIA = "urn:x-cast:com.google.cast.media";
|
||||
|
||||
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>();
|
||||
|
||||
/** Creates a Media object and initializes private data. */
|
||||
export function createMedia(
|
||||
mediaArgs: ConstructorParameters<typeof Media>,
|
||||
mediaMessageCallback: MediaMessageCallback
|
||||
) {
|
||||
const media = new Media(...mediaArgs);
|
||||
mediaMessageCallbacks.set(media, mediaMessageCallback);
|
||||
mediaUpdateListeners.set(media, new Set());
|
||||
mediaLastUpdateTimes.set(media, 0);
|
||||
|
||||
return media;
|
||||
}
|
||||
|
||||
type UpdateListener = (isAlive: boolean) => void;
|
||||
|
||||
export default class Media {
|
||||
#id = uuid();
|
||||
|
||||
get #updateListeners() {
|
||||
const updateListeners = mediaUpdateListeners.get(this);
|
||||
if (!updateListeners)
|
||||
throw logger.error("Missing media update listeners!");
|
||||
return updateListeners;
|
||||
}
|
||||
get #mediaMessageCallback() {
|
||||
const callback = mediaMessageCallbacks.get(this);
|
||||
if (!callback) throw logger.error("Missing media message callback!");
|
||||
return callback;
|
||||
}
|
||||
get #lastUpdateTime() {
|
||||
const lastUpdateTime = mediaLastUpdateTimes.get(this);
|
||||
if (lastUpdateTime === undefined)
|
||||
throw logger.error("Missing last update time!");
|
||||
return lastUpdateTime;
|
||||
}
|
||||
|
||||
activeTrackIds: Nullable<number[]> = null;
|
||||
breakStatus?: BreakStatus;
|
||||
currentTime = 0;
|
||||
customData: unknown = null;
|
||||
idleReason: Nullable<string> = null;
|
||||
liveSeekableRange?: LiveSeekableRange;
|
||||
media: Nullable<MediaInfo> = null;
|
||||
playbackRate = 1;
|
||||
playerState = PlayerState.IDLE;
|
||||
repeatMode = RepeatMode.OFF;
|
||||
supportedMediaCommands: string[] = [];
|
||||
videoInfo?: VideoInformation;
|
||||
volume: Volume = new Volume();
|
||||
|
||||
// Queues
|
||||
items: Nullable<QueueItem[]> = null;
|
||||
currentItemId: Nullable<number> = null;
|
||||
loadingItemId: Nullable<number> = null;
|
||||
preloadedItemId: Nullable<number> = null;
|
||||
queueData?: QueueData;
|
||||
|
||||
constructor(public sessionId: string, public mediaSessionId: number) {}
|
||||
|
||||
addUpdateListener(listener: UpdateListener) {
|
||||
this.#updateListeners?.add(listener);
|
||||
}
|
||||
removeUpdateListener(listener: UpdateListener) {
|
||||
this.#updateListeners?.delete(listener);
|
||||
}
|
||||
|
||||
editTracksInfo(
|
||||
editTracksInfoRequest: EditTracksInfoRequest,
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
this.#mediaMessageCallback?.({
|
||||
...editTracksInfoRequest,
|
||||
type: "EDIT_TRACKS_INFO",
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimates the current break clip position based on the last
|
||||
* information reported by the receiver.
|
||||
*/
|
||||
getEstimatedBreakClipTime() {
|
||||
if (this.breakStatus?.currentBreakClipTime === undefined) return;
|
||||
if (this.playerState === PlayerState.PLAYING) {
|
||||
return getEstimatedTime({
|
||||
currentTime: this.breakStatus.currentBreakClipTime,
|
||||
lastUpdateTime: this.#lastUpdateTime
|
||||
});
|
||||
}
|
||||
|
||||
return this.breakStatus.currentBreakClipTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimates the current break position based on the last
|
||||
* information reported by the receiver.
|
||||
*/
|
||||
getEstimatedBreakTime() {
|
||||
if (this.breakStatus?.currentBreakTime === undefined) return;
|
||||
if (this.playerState === PlayerState.PLAYING) {
|
||||
return getEstimatedTime({
|
||||
currentTime: this.breakStatus.currentBreakTime,
|
||||
lastUpdateTime: this.#lastUpdateTime
|
||||
});
|
||||
}
|
||||
|
||||
return this.breakStatus.currentBreakTime;
|
||||
}
|
||||
|
||||
getEstimatedLiveSeekableRange() {
|
||||
logger.info("STUB :: Media#getEstimatedLiveSeekableRange");
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimates the current playback position based on the last
|
||||
* information reported by the receiver.
|
||||
*/
|
||||
getEstimatedTime(): number {
|
||||
if (this.playerState === PlayerState.PLAYING) {
|
||||
return getEstimatedTime({
|
||||
currentTime: this.currentTime,
|
||||
lastUpdateTime: this.#lastUpdateTime,
|
||||
playbackRate: this.playbackRate,
|
||||
duration: this.media?.duration
|
||||
});
|
||||
}
|
||||
|
||||
return this.currentTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request media status from the receiver application. This will
|
||||
* also trigger any added media update listeners.
|
||||
*/
|
||||
getStatus(
|
||||
getStatusRequest = new GetStatusRequest(),
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
this.#mediaMessageCallback?.({
|
||||
...getStatusRequest,
|
||||
type: "MEDIA_GET_STATUS",
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
pause(
|
||||
pauseRequest = new PauseRequest(),
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
this.#mediaMessageCallback?.({
|
||||
...pauseRequest,
|
||||
type: "PAUSE",
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
play(
|
||||
playRequest = new PlayRequest(),
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
this.#mediaMessageCallback?.({
|
||||
...playRequest,
|
||||
type: "PLAY",
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
queueAppendItem(
|
||||
item: QueueItem,
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
this.#mediaMessageCallback?.({
|
||||
...new QueueInsertItemsRequest([item]),
|
||||
type: "QUEUE_INSERT",
|
||||
sessionId: this.sessionId,
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
queueInsertItems(
|
||||
queueInsertItemsRequest: QueueInsertItemsRequest,
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
this.#mediaMessageCallback?.({
|
||||
...queueInsertItemsRequest,
|
||||
type: "QUEUE_INSERT",
|
||||
sessionId: this.sessionId,
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
queueJumpToItem(
|
||||
itemId: number,
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
if (this.items?.find(item => item.itemId === itemId)) {
|
||||
const jumpRequest = new QueueJumpRequest();
|
||||
jumpRequest.currentItemId = itemId;
|
||||
|
||||
this.#mediaMessageCallback?.({
|
||||
...jumpRequest,
|
||||
type: "QUEUE_UPDATE",
|
||||
sessionId: this.sessionId,
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
}
|
||||
|
||||
queueMoveItemToNewIndex(
|
||||
itemId: number,
|
||||
newIndex: number,
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
// 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 CastError(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.#mediaMessageCallback?.({
|
||||
...reorderItemsRequest,
|
||||
type: "QUEUE_REORDER",
|
||||
sessionId: this.sessionId,
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
}
|
||||
|
||||
queueNext(
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
const jumpRequest = new QueueJumpRequest();
|
||||
jumpRequest.jump = 1;
|
||||
|
||||
this.#mediaMessageCallback?.({
|
||||
...jumpRequest,
|
||||
type: "QUEUE_UPDATE",
|
||||
sessionId: this.sessionId,
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
queuePrev(
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
const jumpRequest = new QueueJumpRequest();
|
||||
jumpRequest.jump = -1;
|
||||
|
||||
this.#mediaMessageCallback?.({
|
||||
...jumpRequest,
|
||||
type: "QUEUE_UPDATE",
|
||||
sessionId: this.sessionId,
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
queueRemoveItem(
|
||||
itemId: number,
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
const item = this.items?.find(item => item.itemId === itemId);
|
||||
if (item) {
|
||||
this.queueRemoveItems(
|
||||
new QueueRemoveItemsRequest([itemId]),
|
||||
successCallback,
|
||||
errorCallback
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
queueRemoveItems(
|
||||
queueRemoveItemsRequest: QueueRemoveItemsRequest,
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
this.#mediaMessageCallback?.({
|
||||
...queueRemoveItemsRequest,
|
||||
|
||||
mediaSessionId: this.mediaSessionId,
|
||||
type: "QUEUE_REMOVE",
|
||||
sessionId: this.sessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
queueReorderItems(
|
||||
queueReorderItemsRequest: QueueReorderItemsRequest,
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
this.#mediaMessageCallback?.({
|
||||
...queueReorderItemsRequest,
|
||||
|
||||
mediaSessionId: this.mediaSessionId,
|
||||
type: "QUEUE_REORDER",
|
||||
sessionId: this.sessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
queueSetRepeatMode(
|
||||
repeatMode: string,
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
const setPropertiesRequest = new QueueSetPropertiesRequest();
|
||||
setPropertiesRequest.repeatMode = repeatMode;
|
||||
|
||||
this.#mediaMessageCallback?.({
|
||||
...setPropertiesRequest,
|
||||
type: "QUEUE_UPDATE",
|
||||
sessionId: this.sessionId,
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
queueUpdateItems(
|
||||
queueUpdateItemsRequest: QueueUpdateItemsRequest,
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
this.#mediaMessageCallback?.({
|
||||
...queueUpdateItemsRequest,
|
||||
type: "QUEUE_UPDATE",
|
||||
sessionId: this.sessionId,
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
seek(
|
||||
seekRequest: SeekRequest,
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
this.#mediaMessageCallback?.({
|
||||
...seekRequest,
|
||||
type: "SEEK",
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
setVolume(
|
||||
volumeRequest: VolumeRequest,
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
this.#mediaMessageCallback?.({
|
||||
...volumeRequest,
|
||||
type: "MEDIA_SET_VOLUME",
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
stop(
|
||||
stopRequest?: StopRequest,
|
||||
successCallback?: () => void,
|
||||
errorCallback?: (err: CastError) => void
|
||||
) {
|
||||
if (!stopRequest) {
|
||||
stopRequest = new StopRequest();
|
||||
}
|
||||
|
||||
this.#mediaMessageCallback?.({
|
||||
...stopRequest,
|
||||
type: "STOP",
|
||||
mediaSessionId: this.mediaSessionId
|
||||
})
|
||||
.then(() => {
|
||||
if (successCallback) {
|
||||
successCallback();
|
||||
}
|
||||
})
|
||||
.catch(errorCallback);
|
||||
}
|
||||
|
||||
supportsCommand(command: string): boolean {
|
||||
return this.supportedMediaCommands.includes(command);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user