Move cast API code to cast/sdk/ and create wrapper class

This commit is contained in:
hensm
2022-04-25 15:59:58 +01:00
parent 43f5d325f8
commit 4bccdecaa3
35 changed files with 707 additions and 1188 deletions

318
ext/src/cast/sdk/Session.ts Normal file
View File

@@ -0,0 +1,318 @@
"use strict";
import { v4 as uuid } from "uuid";
import logger from "../../lib/logger";
import { sendMessageResponse } from "../eventMessageChannel";
import {
ErrorCallback,
LoadSuccessCallback,
MediaListener,
MessageListener,
SuccessCallback,
UpdateListener
} from "../types";
import {
MediaStatus,
ReceiverMediaMessage,
SenderMediaMessage,
SenderMessage
} from "./types";
import { SessionStatus } from "./enums";
import { Image, Receiver, SenderApplication } from "./classes";
import { MediaCommand } from "./media/enums";
import { LoadRequest, QueueLoadRequest, QueueItem } from "./media/classes";
import Media, { NS_MEDIA } from "./media/Media";
/** supportedMediaCommands bitflag returned in MEDIA_STATUS messages */
enum _MediaCommand {
PAUSE = 1,
SEEK = 2,
STREAM_VOLUME = 4,
STREAM_MUTE = 8,
QUEUE_NEXT = 64,
QUEUE_PREV = 128
}
/**
* 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<string, Set<MessageListener>>();
_updateListeners = new Set<UpdateListener>();
_sendMessageCallbacks = new Map<
string,
[SuccessCallback?, ErrorCallback?]
>();
media: Media[] = [];
namespaces: Array<{ name: string }> = [];
senderApps: SenderApplication[] = [];
status = SessionStatus.CONNECTED;
statusText: Nullable<string> = 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<SenderMediaMessage, "requestId">
) => {
return new Promise<void>((resolve, reject) => {
this.sendMessage(
NS_MEDIA,
{ ...message, requestId: 0 },
resolve,
reject
);
});
};
#sendReceiverMessage = (
message: DistributiveOmit<SenderMessage, "requestId">
) => {
return new Promise<void>((resolve, reject) => {
const messageId = uuid();
sendMessageResponse({
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();
sendMessageResponse({
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);
}
}

104
ext/src/cast/sdk/classes.ts Normal file
View File

@@ -0,0 +1,104 @@
"use strict";
import Session from "./Session";
import {
AutoJoinPolicy,
Capability,
DefaultActionPolicy,
ReceiverType,
VolumeControlType
} from "./enums";
export class ApiConfig {
constructor(
public sessionRequest: SessionRequest,
public sessionListener: (session: Session) => void,
public receiverListener: (availability: string) => void,
public autoJoinPolicy: string = AutoJoinPolicy.TAB_AND_ORIGIN_SCOPED,
public defaultActionPolicy: string = DefaultActionPolicy.CREATE_SESSION
) {}
}
export class CredentialsData {
constructor(public credentials: string, public credentialsData: string) {}
}
export class DialRequest {
constructor(
public appName: string,
public launchParameter: Nullable<string> = null
) {}
}
export class Error {
constructor(
public code: string,
public description: Nullable<string> = null,
public details: any = null
) {}
}
export class Image {
width: Nullable<number> = null;
height: Nullable<number> = null;
constructor(public url: string) {}
}
export class Receiver {
displayStatus: Nullable<ReceiverDisplayStatus> = null;
isActiveInput: Nullable<boolean> = null;
receiverType = ReceiverType.CAST;
constructor(
public label: string,
public friendlyName: string,
public capabilities: Capability[] = [],
public volume: Nullable<Volume> = null
) {}
}
export class ReceiverDisplayStatus {
showStop: Nullable<boolean> = null;
constructor(public statusText: string, public appImages: Image[]) {}
}
export class SenderApplication {
packageId: Nullable<string> = null;
url: Nullable<string> = null;
constructor(public platform: string) {}
}
export class SessionRequest {
language: Nullable<string> = null;
constructor(
public appId: string,
public capabilities: Capability[] = [],
public requestSessionTimeout = new Timeout().requestSession,
public androidReceiverCompatible = false,
public credentialsData: Nullable<CredentialsData> = null
) {}
}
export class Timeout {
leaveSession = 3000;
requestSession = 60000;
sendCustomMessage = 3000;
setReceiverVolume = 3000;
stopSession = 3000;
}
export class Volume {
controlType?: VolumeControlType;
stepInterval?: number;
constructor(
public level: Nullable<number> = null,
public muted: Nullable<boolean> = null
) {}
}

75
ext/src/cast/sdk/enums.ts Normal file
View File

@@ -0,0 +1,75 @@
"use strict";
export enum AutoJoinPolicy {
TAB_AND_ORIGIN_SCOPED = "tab_and_origin_scoped",
ORIGIN_SCOPED = "origin_scoped",
PAGE_SCOPED = "page_scoped",
CUSTOM_CONTROLLER_SCOPED = "custom_controller_scoped"
}
export enum Capability {
VIDEO_OUT = "video_out",
AUDIO_OUT = "audio_out",
VIDEO_IN = "video_in",
AUDIO_IN = "audio_in",
MULTIZONE_GROUP = "multizone_group"
}
export enum DefaultActionPolicy {
CREATE_SESSION = "create_session",
CAST_THIS_TAB = "cast_this_tab"
}
export enum DialAppState {
RUNNING = "running",
STOPPED = "stopped",
ERROR = "error"
}
export enum ErrorCode {
CANCEL = "cancel",
TIMEOUT = "timeout",
API_NOT_INITIALIZED = "api_not_initialized",
INVALID_PARAMETER = "invalid_parameter",
EXTENSION_NOT_COMPATIBLE = "extension_not_compatible",
EXTENSION_MISSING = "extension_missing",
RECEIVER_UNAVAILABLE = "receiver_unavailable",
SESSION_ERROR = "session_error",
CHANNEL_ERROR = "channel_error",
LOAD_MEDIA_FAILED = "load_media_failed"
}
export enum ReceiverAction {
CAST = "cast",
STOP = "stop"
}
export enum ReceiverAvailability {
AVAILABLE = "available",
UNAVAILABLE = "unavailable"
}
export enum ReceiverType {
CAST = "cast",
DIAL = "dial",
HANGOUT = "hangout",
CUSTOM = "custom"
}
export enum SenderPlatform {
CHROME = "chrome",
IOS = "ios",
ANDROID = "android"
}
export enum SessionStatus {
CONNECTED = "connected",
DISCONNECTED = "disconnected",
STOPPED = "stopped"
}
export enum VolumeControlType {
ATTENUATION = "attenuation",
FIXED = "fixed",
MASTER = "master"
}

512
ext/src/cast/sdk/index.ts Normal file
View File

@@ -0,0 +1,512 @@
"use strict";
import logger from "../../lib/logger";
import {
ReceiverDevice,
ReceiverDeviceCapabilities as ReceiverDeviceCapabilities
} from "../../types";
import { ErrorCallback, SuccessCallback } from "../types";
import { onMessage, sendMessageResponse } from "../eventMessageChannel";
import {
AutoJoinPolicy,
Capability,
DefaultActionPolicy,
DialAppState,
ErrorCode,
ReceiverAction,
ReceiverAvailability,
ReceiverType,
SenderPlatform,
SessionStatus,
VolumeControlType
} from "./enums";
import {
ApiConfig,
CredentialsData,
DialRequest,
Error as Error_,
Image,
Receiver,
ReceiverDisplayStatus,
SenderApplication,
SessionRequest,
Timeout,
Volume
} from "./classes";
import Session from "./Session";
import media from "./media";
import { Message } from "../../messaging";
type ReceiverActionListener = (
receiver: Receiver,
receiverAction: string
) => void;
type RequestSessionSuccessCallback = (session: Session) => void;
/**
* Create `chrome.cast.Receiver` object from receiver device info.
*/
function createReceiver(device: ReceiverDevice) {
// Convert capabilities bitflag to string array
const capabilities: Capability[] = [];
if (device.capabilities & ReceiverDeviceCapabilities.VIDEO_OUT) {
capabilities.push(Capability.VIDEO_OUT);
} else if (device.capabilities & ReceiverDeviceCapabilities.VIDEO_IN) {
capabilities.push(Capability.VIDEO_IN);
} else if (device.capabilities & ReceiverDeviceCapabilities.AUDIO_OUT) {
capabilities.push(Capability.AUDIO_OUT);
} else if (device.capabilities & ReceiverDeviceCapabilities.AUDIO_IN) {
capabilities.push(Capability.AUDIO_IN);
} else if (
device.capabilities & ReceiverDeviceCapabilities.MULTIZONE_GROUP
) {
capabilities.push(Capability.MULTIZONE_GROUP);
}
const receiver = new Receiver(device.id, device.friendlyName, capabilities);
// Currently only supports CAST receivers
receiver.receiverType = ReceiverType.CAST;
return receiver;
}
/** Cast SDK root class */
export default class {
#receiverDevices = new Map<string, ReceiverDevice>();
#apiConfig?: ApiConfig;
#sessionRequest?: SessionRequest;
#requestSessionSuccessCallback?: RequestSessionSuccessCallback;
#requestSessionErrorCallback?: ErrorCallback;
#sessions = new Map<string, Session>();
#receiverActionListeners = new Set<ReceiverActionListener>();
// Enums
AutoJoinPolicy = AutoJoinPolicy;
Capability = Capability;
DefaultActionPolicy = DefaultActionPolicy;
DialAppState = DialAppState;
ErrorCode = ErrorCode;
ReceiverAction = ReceiverAction;
ReceiverAvailability = ReceiverAvailability;
ReceiverType = ReceiverType;
SenderPlatform = SenderPlatform;
SessionStatus = SessionStatus;
VolumeControlType = VolumeControlType;
// Classes
ApiConfig = ApiConfig;
CredentialsData = CredentialsData;
DialRequest = DialRequest;
Error = Error_;
Image = Image;
Receiver = Receiver;
ReceiverDisplayStatus = ReceiverDisplayStatus;
SenderApplication = SenderApplication;
SessionRequest = SessionRequest;
Timeout = Timeout;
Volume = Volume;
Session = Session;
media = media;
VERSION = [1, 2];
isAvailable = false;
timeout = new Timeout();
constructor() {
onMessage(this.#onMessage.bind(this));
}
#sendSessionRequest(
sessionRequest: SessionRequest,
receiverDevice: ReceiverDevice
) {
for (const listener of this.#receiverActionListeners) {
listener(createReceiver(receiverDevice), ReceiverAction.CAST);
}
sendMessageResponse({
subject: "bridge:createCastSession",
data: {
appId: sessionRequest.appId,
receiverDevice: receiverDevice
}
});
}
#onMessage(message: Message) {
switch (message.subject) {
case "cast:initialized": {
this.isAvailable = true;
break;
}
/**
* Once the bridge detects a session creation, session info
* and data needed to create cast API objects is sent.
*/
case "cast:sessionCreated": {
// Notify background to close UI
sendMessageResponse({
subject: "main:closeReceiverSelector"
});
const status = message.data;
const receiverDevice = this.#receiverDevices.get(
status.receiverId
);
if (!receiverDevice) {
logger.error(
`Could not find receiver device "${status.receiverFriendlyName}" (${status.receiverId})`
);
break;
}
const receiver = createReceiver(receiverDevice);
receiver.volume = status.volume;
receiver.displayStatus = new ReceiverDisplayStatus(
status.statusText,
status.appImages
);
const session = new Session(
status.sessionId, // sessionId
status.appId, // appId
status.displayName, // displayName
status.appImages, // appImages
receiver // receiver
);
session.senderApps = status.senderApps;
session.transportId = status.transportId;
this.#sessions.set(session.sessionId, session);
}
// eslint-disable-next-line no-fallthrough
case "cast:sessionUpdated": {
const status = message.data;
const session = this.#sessions.get(status.sessionId);
if (!session) {
logger.error(`Session not found (${status.sessionId})`);
return;
}
session.statusText = status.statusText;
session.namespaces = status.namespaces;
session.receiver.volume = status.volume;
/**
* If session created via requestSession, the success
* callback will be set, otherwise the session was created
* by the extension and the session listener should be
* called instead.
*/
if (this.#requestSessionSuccessCallback) {
this.#requestSessionSuccessCallback(session);
this.#requestSessionSuccessCallback = undefined;
this.#requestSessionErrorCallback = undefined;
} else {
this.#apiConfig?.sessionListener(session);
}
break;
}
case "cast:sessionStopped": {
const { sessionId } = message.data;
const session = this.#sessions.get(sessionId);
if (session) {
session.status = SessionStatus.STOPPED;
const updateListeners = session?._updateListeners;
if (updateListeners) {
for (const listener of updateListeners) {
listener(false);
}
}
}
break;
}
case "cast:receivedSessionMessage": {
const { sessionId, namespace, messageData } = message.data;
const session = this.#sessions.get(sessionId);
if (session) {
const _messageListeners = session._messageListeners;
const listeners = _messageListeners.get(namespace);
if (listeners) {
for (const listener of listeners) {
listener(namespace, messageData);
}
}
}
break;
}
case "cast:impl_sendMessage": {
const { sessionId, messageId, error } = message.data;
const session = this.#sessions.get(sessionId);
if (!session) {
break;
}
const callbacks = session._sendMessageCallbacks.get(messageId);
if (callbacks) {
const [successCallback, errorCallback] = callbacks;
if (error) {
errorCallback?.(new Error_(error));
return;
}
successCallback?.();
}
break;
}
case "cast:receiverDeviceUp": {
const { receiverDevice } = message.data;
if (this.#receiverDevices.has(receiverDevice.id)) {
break;
}
this.#receiverDevices.set(receiverDevice.id, receiverDevice);
if (this.#apiConfig) {
// Notify listeners of new cast destination
this.#apiConfig.receiverListener(
ReceiverAvailability.AVAILABLE
);
}
break;
}
case "cast:receiverDeviceDown": {
const { receiverDeviceId } = message.data;
this.#receiverDevices.delete(receiverDeviceId);
if (this.#receiverDevices.size === 0) {
if (this.#apiConfig) {
this.#apiConfig.receiverListener(
ReceiverAvailability.UNAVAILABLE
);
}
}
break;
}
case "cast:selectReceiver/selected": {
logger.info("Selected receiver");
if (this.#sessionRequest) {
this.#sendSessionRequest(
this.#sessionRequest,
message.data.receiverDevice
);
this.#sessionRequest = undefined;
}
break;
}
case "cast:selectReceiver/stopped": {
const { receiverDevice } = message.data;
logger.info("Stopped receiver");
if (this.#sessionRequest) {
this.#sessionRequest = undefined;
for (const listener of this.#receiverActionListeners) {
listener(
// TODO: Use existing receiver object?
createReceiver(receiverDevice),
ReceiverAction.STOP
);
}
}
break;
}
// Popup closed before session established
case "cast:selectReceiver/cancelled": {
if (this.#sessionRequest) {
this.#sessionRequest = undefined;
this.#requestSessionErrorCallback?.(
new Error_(ErrorCode.CANCEL)
);
}
break;
}
// Session request initiated via receiver selector
case "cast:launchApp": {
if (this.#sessionRequest) {
logger.error("Session request already in progress.");
break;
}
if (!this.#apiConfig?.sessionRequest) {
logger.error("Session request not found!");
break;
}
this.#sendSessionRequest(
this.#apiConfig.sessionRequest,
message.data.receiverDevice
);
break;
}
}
}
initialize(
apiConfig: ApiConfig,
successCallback?: SuccessCallback,
errorCallback?: ErrorCallback
) {
logger.info("cast.initialize");
// Already initialized
if (this.#apiConfig) {
errorCallback?.(new Error_(ErrorCode.INVALID_PARAMETER));
return;
}
this.#apiConfig = apiConfig;
sendMessageResponse({
subject: "main:initializeCast",
data: { appId: this.#apiConfig.sessionRequest.appId }
});
successCallback?.();
this.#apiConfig.receiverListener(
this.#receiverDevices.size
? ReceiverAvailability.AVAILABLE
: ReceiverAvailability.UNAVAILABLE
);
}
requestSession(
successCallback: RequestSessionSuccessCallback,
errorCallback: ErrorCallback,
newSessionRequest?: SessionRequest,
receiverDevice?: ReceiverDevice
) {
logger.info("cast.requestSession");
// Not yet initialized
if (!this.#apiConfig) {
errorCallback?.(new Error_(ErrorCode.API_NOT_INITIALIZED));
return;
}
// Already requesting session
if (this.#sessionRequest) {
errorCallback?.(
new Error_(
ErrorCode.INVALID_PARAMETER,
"Session request already in progress."
)
);
return;
}
// No receivers available
if (!this.#receiverDevices.size) {
errorCallback?.(new Error_(ErrorCode.RECEIVER_UNAVAILABLE));
return;
}
/**
* Store session request for use in return message from
* receiver selection.
*/
this.#sessionRequest =
newSessionRequest ?? this.#apiConfig.sessionRequest;
this.#requestSessionSuccessCallback = successCallback;
this.#requestSessionErrorCallback = errorCallback;
/**
* If a receiver was provided, skip the receiver selector
* process.
*/
if (receiverDevice) {
if (
receiverDevice?.id &&
this.#receiverDevices.has(receiverDevice.id)
) {
this.#sendSessionRequest(this.#sessionRequest, receiverDevice);
}
} else {
// Open receiver selector UI
sendMessageResponse({
subject: "main:selectReceiver"
});
}
}
requestSessionById(_sessionId: string): void {
logger.info("STUB :: cast.requestSessionById");
}
setCustomReceivers(
_receivers: Receiver[],
_successCallback?: SuccessCallback,
_errorCallback?: ErrorCallback
): void {
logger.info("STUB :: cast.setCustomReceivers");
}
setPageContext(_win: Window): void {
logger.info("STUB :: cast.setPageContext");
}
setReceiverDisplayStatus(_sessionId: string): void {
logger.info("STUB :: cast.setReceiverDisplayStatus");
}
unescape(escaped: string): string {
return window.decodeURI(escaped);
}
addReceiverActionListener(listener: ReceiverActionListener) {
this.#receiverActionListeners.add(listener);
}
removeReceiverActionListener(listener: ReceiverActionListener) {
this.#receiverActionListeners.delete(listener);
}
logMessage(message: string) {
logger.info("cast.logMessage", message);
}
precache(_data: string) {
logger.info("STUB :: cast.precache");
}
}

View File

@@ -0,0 +1,440 @@
"use strict";
import { v1 as uuid } from "uuid";
import logger from "../../../lib/logger";
import { Volume, Error as _Error } from "../classes";
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";
import { ErrorCode } from "../enums";
import { ErrorCallback, SuccessCallback, UpdateListener } from "../../types";
import { SenderMediaMessage } from "../types";
export const NS_MEDIA = "urn:x-cast:com.google.cast.media";
export default class Media {
#id = uuid();
// Timestamp of last status update
_lastUpdateTime = 0;
_updateListeners = new Set<UpdateListener>();
activeTrackIds: Nullable<number[]> = null;
breakStatus?: BreakStatus;
currentTime = 0;
customData: any = 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,
public _sendMediaMessage: (
message: DistributiveOmit<SenderMediaMessage, "requestId">
) => Promise<void>
) {}
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);
}
}

View File

@@ -0,0 +1,380 @@
"use strict";
import { Image, Volume } from "../classes";
import {
ContainerType,
HdrType,
HlsSegmentFormat,
HlsVideoSegmentFormat,
MetadataType,
RepeatMode,
ResumeState,
StreamType,
TrackType,
UserAction
} from "./enums";
export class AudiobookChapterMediaMetadata {
bookTitle?: string;
chapterNumber?: number;
chapterTitle?: string;
images?: Image[];
subtitle?: string;
title?: string;
type = MetadataType.AUDIOBOOK_CHAPTER;
}
export class AudiobookContainerMetadata {
authors?: string[];
narrators?: string[];
publisher?: string;
releaseDate?: string;
}
export class Break {
duration?: number;
isEmbedded?: boolean;
isWatched = false;
constructor(
public id: string,
public breakClipIds: string[],
public position: number
) {}
}
export class BreakClip {
clickThroughUrl?: string;
contentId?: string;
contentType?: string;
contentUrl?: string;
customData?: {};
duration?: number;
hlsSegmentFormat?: HlsSegmentFormat;
posterUrl?: string;
title?: string;
vastAdsRequest?: VastAdsRequest;
whenSkippable?: number;
constructor(public id: string) {}
}
export class BreakStatus {
breakClipId?: string;
breakId?: string;
currentBreakClipTime?: number;
currentBreakTime?: number;
whenSkippable?: number;
}
export class ContainerMetadata {
containerDuration?: number;
containerImages?: Image[];
sections?: MediaMetadata[];
title?: string;
constructor(
public containerType: ContainerType = ContainerType.GENERIC_CONTAINER
) {}
}
export class EditTracksInfoRequest {
requestId = 0;
constructor(
public activeTrackIds: Nullable<number[]> = null,
public textTrackStyle: Nullable<string> = null
) {}
}
export class GenericMediaMetadata {
images?: Image[];
metadataType = MetadataType.GENERIC;
releaseDate?: string;
releaseYear?: number;
subtitle?: string;
title?: string;
type = MetadataType.GENERIC;
}
export class GetStatusRequest {
customData: any = null;
}
export class LiveSeekableRange {
constructor(
public start?: number,
public end?: number,
public isMovingWindow?: boolean,
public isLiveDone?: boolean
) {}
}
export class LoadRequest {
activeTrackIds: Nullable<number[]> = null;
atvCredentials?: string;
atvCredentialsType?: string;
autoplay: Nullable<boolean> = true;
currentTime: Nullable<number> = null;
customData: any = null;
media: MediaInfo;
requestId = 0;
sessionId: Nullable<string> = null;
type: "LOAD" = "LOAD";
constructor(mediaInfo: MediaInfo) {
this.media = mediaInfo;
}
}
export type Metadata =
| GenericMediaMetadata
| MovieMediaMetadata
| MusicTrackMediaMetadata
| PhotoMediaMetadata
| TvShowMediaMetadata;
export class MediaInfo {
atvEntity?: string;
breakClips?: BreakClip[];
breaks?: Break[];
customData: any = null;
contentUrl?: string;
duration: Nullable<number> = null;
entity?: string;
hlsSegmentFormat?: HlsSegmentFormat;
hlsVideoSegmentFormat?: HlsVideoSegmentFormat;
metadata: Nullable<Metadata> = null;
startAbsoluteTime?: number;
streamType: string = StreamType.BUFFERED;
textTrackStyle: Nullable<TextTrackStyle> = null;
tracks: Nullable<Track[]> = null;
userActionStates?: UserActionState[];
vmapAdsRequest?: VastAdsRequest;
constructor(public contentId: string, public contentType: string) {}
}
export class MediaMetadata {
queueItemId?: number;
sectionDuration?: number;
sectionStartAbsoluteTime?: number;
sectionStartTimeInContainer?: number;
sectionStartTimeInMedia?: number;
type: MetadataType;
metadataType: MetadataType;
constructor(type: MetadataType) {
this.type = type;
this.metadataType = type;
}
}
export class MovieMediaMetadata {
images?: Image[];
metadataType = MetadataType.MOVIE;
releaseDate?: string;
releaseYear?: number;
studio?: string;
subtitle?: string;
title?: string;
type = MetadataType.MOVIE;
}
export class MusicTrackMediaMetadata {
albumArtist?: string;
albumName?: string;
artist?: string;
artistName?: string;
composer?: string;
discNumber?: number;
images?: Image[];
metadataType = MetadataType.MUSIC_TRACK;
releaseDate?: string;
releaseYear?: number;
songName?: string;
title?: string;
trackNumber?: number;
type = MetadataType.MUSIC_TRACK;
}
export class PauseRequest {
customData: any = null;
}
export class PhotoMediaMetadata {
artist?: string;
creationDateTime?: string;
height?: number;
images?: Image[];
latitude?: number;
location?: string;
longitude?: number;
metadataType = MetadataType.PHOTO;
title?: string;
type = MetadataType.PHOTO;
width?: number;
}
export class PlayRequest {
customData: any = null;
}
export class QueueData {
shuffle = false;
constructor(
public id?: string,
public name?: string,
public description?: string,
public repeatMode?: RepeatMode,
public items?: QueueItem[],
public startIndex?: number,
public startTime?: number
) {}
}
export class QueueInsertItemsRequest {
customData: any = null;
insertBefore: Nullable<number> = null;
requestId: Nullable<number> = null;
sessionId: Nullable<string> = null;
type = "QUEUE_INSERT";
constructor(public items: QueueItem[]) {}
}
export class QueueItem {
activeTrackIds: Nullable<number[]> = null;
autoplay = true;
customData: any = null;
itemId: Nullable<number> = null;
media: MediaInfo;
playbackDuration: Nullable<number> = null;
preloadTime = 0;
startTime = 0;
constructor(mediaInfo: MediaInfo) {
this.media = mediaInfo;
}
}
export class QueueJumpRequest {
type = "QUEUE_UPDATE";
jump: Nullable<number> = null;
currentItemId: Nullable<number> = null;
}
export class QueueLoadRequest {
type = "QUEUE_LOAD";
customData: any = null;
repeatMode: string = RepeatMode.OFF;
startIndex = 0;
constructor(public items: QueueItem[]) {}
}
export class QueueRemoveItemsRequest {
type = "QUEUE_REMOVE";
customData: any = null;
constructor(public itemIds: number[]) {}
}
export class QueueReorderItemsRequest {
customData: any = null;
insertBefore: Nullable<number> = null;
type = "QUEUE_REORDER";
constructor(public itemIds: number[]) {}
}
export class QueueSetPropertiesRequest {
type = "QUEUE_UPDATE";
customData: any = null;
repeatMode: Nullable<string> = null;
}
export class QueueUpdateItemsRequest {
type = "QUEUE_UPDATE";
customData: any = null;
constructor(public items: QueueItem[]) {}
}
export class SeekRequest {
currentTime: Nullable<number> = null;
customData: any = null;
resumeState: Nullable<ResumeState> = null;
}
export class StopRequest {
customData: any = null;
}
export class TextTrackStyle {
backgroundColor: Nullable<string> = null;
customData: any = null;
edgeColor: Nullable<string> = null;
edgeType: Nullable<string> = null;
fontFamily: Nullable<string> = null;
fontGenericFamily: Nullable<string> = null;
fontScale: Nullable<number> = null;
fontStyle: Nullable<string> = null;
foregroundColor: Nullable<string> = null;
windowColor: Nullable<string> = null;
windowRoundedCornerRadius: Nullable<number> = null;
windowType: Nullable<string> = null;
}
export class Track {
customData: any = null;
language: Nullable<string> = null;
name: Nullable<string> = null;
subtype: Nullable<string> = null;
trackContentId: Nullable<string> = null;
trackContentType: Nullable<string> = null;
constructor(public trackId: number, public type: TrackType) {}
}
export class TvShowMediaMetadata {
episode?: number;
episodeNumber?: number;
episodeTitle?: string;
images?: Image[];
metadataType: number = MetadataType.TV_SHOW;
originalAirdate?: string;
releaseYear?: number;
season?: number;
seasonNumber?: number;
seriesTitle?: string;
title?: string;
type = MetadataType.TV_SHOW;
}
export class UserActionState {
customData: any = null;
constructor(public userAction: UserAction) {}
}
export class VastAdsRequest {
adsResponse?: string;
adTagUrl?: string;
}
export class VideoInformation {
constructor(
public width: number,
public height: number,
public hdrType: HdrType
) {}
}
export class VolumeRequest {
customData: any = null;
constructor(public volume: Volume) {}
}

View File

@@ -0,0 +1,139 @@
"use strict";
export enum ContainerType {
GENERIC_CONTAINER,
AUDIOBOOK_CONTAINER
}
export enum HdrType {
SDR = "sdr",
HDR = "hdr",
DV = "dv"
}
export enum HlsSegmentFormat {
AAC = "aac",
AC3 = "ac3",
MP3 = "mp3",
TS = "ts",
TS_AAC = "ts_aac",
E_AC3 = "e_ac3",
FMP4 = "fmp4"
}
export enum HlsVideoSegmentFormat {
MPEG2_TS = "mpeg2_ts",
FMP4 = "fmp4"
}
export enum IdleReason {
CANCELLED = "CANCELLED",
INTERRUPTED = "INTERRUPTED",
FINISHED = "FINISHED",
ERROR = "ERROR"
}
export enum MediaCommand {
PAUSE = "pause",
SEEK = "seek",
STREAM_VOLUME = "stream_volume",
STREAM_MUTE = "stream_mute"
}
export enum MetadataType {
GENERIC,
MOVIE,
TV_SHOW,
MUSIC_TRACK,
PHOTO,
AUDIOBOOK_CHAPTER
}
export enum PlayerState {
IDLE = "IDLE",
PLAYING = "PLAYING",
PAUSED = "PAUSED",
BUFFERING = "BUFFERING"
}
export enum QueueType {
ALBUM = "ALBUM",
PLAYLIST = "PLAYLIST",
AUDIOBOOK = "AUDIOBOOK",
RADIO_STATION = "RADIO_STATION",
PODCAST_SERIES = "PODCAST_SERIES",
TV_SERIES = "TV_SERIES",
VIDEO_PLAYLIST = "VIDEO_PLAYLIST",
LIVE_TV = "LIVETV",
MOVIE = "MOVIE"
}
export enum RepeatMode {
OFF = "REPEAT_OFF",
ALL = "REPEAT_ALL",
SINGLE = "REPEAT_SINGLE",
ALL_AND_SHUFFLE = "REPEAT_ALL_AND_SHUFFLE"
}
export enum ResumeState {
PLAYBACK_START = "PLAYBACK_START",
PLAYBACK_PAUSE = "PLAYBACK_PAUSE"
}
export enum StreamType {
BUFFERED = "BUFFERED",
LIVE = "LIVE",
OTHER = "OTHER"
}
export enum TextTrackEdgeType {
NONE = "NONE",
OUTLINE = "OUTLINE",
DROP_SHADOW = "DROP_SHADOW",
RAISED = "RAISED",
DEPRESSED = "DEPRESSED"
}
export enum TextTrackFontGenericFamily {
SANS_SERIF = "SANS_SERIF",
MONOSPACED_SANS_SERIF = "MONOSPACED_SANS_SERIF",
SERIF = "SERIF",
MONOSPACED_SERIF = "MONOSPACED_SERIF",
CASUAL = "CASUAL",
CURSIVE = "CURSIVE",
SMALL_CAPITALS = "SMALL_CAPITALS"
}
export enum TextTrackFontStyle {
NORMAL = "NORMAL",
BOLD = "BOLD",
BOLD_ITALIC = "BOLD_ITALIC",
ITALIC = "ITALIC"
}
export enum TextTrackType {
SUBTITLES = "SUBTITLES",
CAPTIONS = "CAPTIONS",
DESCRIPTIONS = "DESCRIPTIONS",
CHAPTERS = "CHAPTERS",
METADATA = "METADATA"
}
export enum TextTrackWindowType {
NONE = "NONE",
NORMAL = "NORMAL",
ROUNDED_CORNERS = "ROUNDED_CORNERS"
}
export enum TrackType {
TEXT = "TEXT",
AUDIO = "AUDIO",
VIDEO = "VIDEO"
}
export enum UserAction {
LIKE = "LIKE",
DISLIKE = "DISLIKE",
FOLLOW = "FOLLOW",
UNFOLLOW = "UNFOLLOW"
}

View File

@@ -0,0 +1,140 @@
"use strict";
import Media from "./Media";
import {
ContainerType,
HdrType,
HlsSegmentFormat,
HlsVideoSegmentFormat,
IdleReason,
MediaCommand,
MetadataType,
PlayerState,
QueueType,
RepeatMode,
ResumeState,
StreamType,
TextTrackEdgeType,
TextTrackFontGenericFamily,
TextTrackFontStyle,
TextTrackType,
TextTrackWindowType,
TrackType,
UserAction
} from "./enums";
import {
AudiobookChapterMediaMetadata,
AudiobookContainerMetadata,
Break,
BreakClip,
BreakStatus,
ContainerMetadata,
EditTracksInfoRequest,
GenericMediaMetadata,
GetStatusRequest,
LiveSeekableRange,
LoadRequest,
MediaInfo,
MediaMetadata,
MovieMediaMetadata,
MusicTrackMediaMetadata,
PauseRequest,
PhotoMediaMetadata,
PlayRequest,
QueueData,
QueueInsertItemsRequest,
QueueItem,
QueueJumpRequest,
QueueLoadRequest,
QueueRemoveItemsRequest,
QueueReorderItemsRequest,
QueueSetPropertiesRequest,
QueueUpdateItemsRequest,
SeekRequest,
StopRequest,
TextTrackStyle,
Track,
TvShowMediaMetadata,
UserActionState,
VastAdsRequest,
VideoInformation,
VolumeRequest
} from "./classes";
export default {
DEFAULT_MEDIA_RECEIVER_APP_ID: "CC1AD845",
timeout: {
editTracksInfo: 0,
getStatus: 0,
load: 0,
pause: 0,
play: 0,
queue: 0,
seek: 0,
setVolume: 0,
stop: 0
},
Media,
// Enums
ContainerType,
HdrType,
HlsSegmentFormat,
HlsVideoSegmentFormat,
IdleReason,
MediaCommand,
MetadataType,
PlayerState,
QueueType,
RepeatMode,
ResumeState,
StreamType,
TextTrackEdgeType,
TextTrackFontGenericFamily,
TextTrackFontStyle,
TextTrackType,
TextTrackWindowType,
TrackType,
UserAction,
// Classes
AudiobookChapterMediaMetadata,
AudiobookContainerMetadata,
Break,
BreakClip,
BreakStatus,
ContainerMetadata,
EditTracksInfoRequest,
GenericMediaMetadata,
GetStatusRequest,
LiveSeekableRange,
LoadRequest,
MediaInfo,
MediaMetadata,
MovieMediaMetadata,
MusicTrackMediaMetadata,
PauseRequest,
PhotoMediaMetadata,
PlayRequest,
QueueData,
QueueInsertItemsRequest,
QueueItem,
QueueJumpRequest,
QueueLoadRequest,
QueueRemoveItemsRequest,
QueueReorderItemsRequest,
QueueSetPropertiesRequest,
QueueUpdateItemsRequest,
SeekRequest,
StopRequest,
TextTrackStyle,
Track,
TvShowMediaMetadata,
UserActionState,
VastAdsRequest,
VideoInformation,
VolumeRequest
};

172
ext/src/cast/sdk/types.ts Normal file
View File

@@ -0,0 +1,172 @@
"use strict";
/**
* Keep in sync with bridge types at:
* app/src/bridge/components/cast/types.ts
*/
import { SenderApplication, Volume, Image } from "./classes";
import { MediaInfo, QueueItem } from "./media/classes";
import {
IdleReason,
PlayerState,
RepeatMode,
ResumeState
} from "./media/enums";
export interface MediaStatus {
mediaSessionId: number;
media?: MediaInfo;
playbackRate: number;
playerState: PlayerState;
idleReason?: IdleReason;
items?: QueueItem[];
currentTime: number;
supportedMediaCommands: number;
repeatMode: RepeatMode;
volume: Volume;
customData: unknown;
}
export interface ReceiverApplication {
appId: string;
appType?: string;
displayName: string;
iconUrl: string;
isIdleScreen: boolean;
launchedFromCloud: boolean;
namespaces: Array<{ name: string }>;
sessionId: string;
statusText: string;
transportId: string;
universalAppId: string;
}
export interface ReceiverStatus {
applications?: ReceiverApplication[];
isActiveInput?: boolean;
isStandBy?: boolean;
volume: Volume;
}
export interface CastSessionUpdatedDetails {
sessionId: string;
statusText: string;
namespaces: Array<{ name: string }>;
volume: Volume;
}
export interface CastSessionCreatedDetails extends CastSessionUpdatedDetails {
appId: string;
appImages: Image[];
displayName: string;
receiverId: string;
receiverFriendlyName: string;
senderApps: SenderApplication[];
transportId: string;
}
interface ReqBase {
requestId: number;
}
// NS: urn:x-cast:com.google.cast.receiver
export type SenderMessage =
| (ReqBase & { type: "LAUNCH"; appId: string })
| (ReqBase & { type: "STOP"; sessionId: string })
| (ReqBase & { type: "GET_STATUS" })
| (ReqBase & { type: "GET_APP_AVAILABILITY"; appId: string[] })
| (ReqBase & { type: "SET_VOLUME"; volume: Partial<Volume> });
export type ReceiverMessage =
| (ReqBase & { type: "RECEIVER_STATUS"; status: ReceiverStatus })
| (ReqBase & { type: "LAUNCH_ERROR"; reason: string });
interface MediaReqBase extends ReqBase {
mediaSessionId: number;
customData?: unknown;
}
// NS: urn:x-cast:com.google.cast.media
export type SenderMediaMessage =
| (MediaReqBase & { type: "PLAY" })
| (MediaReqBase & { type: "PAUSE" })
| (MediaReqBase & { type: "MEDIA_GET_STATUS" })
| (MediaReqBase & { type: "STOP" })
| (MediaReqBase & { type: "MEDIA_SET_VOLUME"; volume: Partial<Volume> })
| (MediaReqBase & { type: "SET_PLAYBACK_RATE"; playbackRate: number })
| (ReqBase & {
type: "LOAD";
activeTrackIds: Nullable<number[]>;
atvCredentials?: string;
atvCredentialsType?: string;
autoplay: Nullable<boolean>;
currentTime: Nullable<number>;
customData?: unknown;
media: MediaInfo;
sessionId: Nullable<string>;
})
| (MediaReqBase & {
type: "SEEK";
resumeState: Nullable<ResumeState>;
currentTime: Nullable<number>;
})
| (MediaReqBase & {
type: "EDIT_TRACKS_INFO";
activeTrackIds: Nullable<number[]>;
textTrackStyle: Nullable<string>;
})
// QueueLoadRequest
| (ReqBase & {
type: "QUEUE_LOAD";
items: QueueItem[];
startIndex: number;
repeatMode: string;
sessionId: Nullable<string>;
})
// QueueInsertItemsRequest
| (MediaReqBase & {
type: "QUEUE_INSERT";
items: QueueItem[];
insertBefore: Nullable<number>;
sessionId: Nullable<string>;
})
// QueueUpdateItemsRequest
| (MediaReqBase & {
type: "QUEUE_UPDATE";
items: QueueItem[];
sessionId: Nullable<string>;
})
// QueueJumpRequest
| (MediaReqBase & {
type: "QUEUE_UPDATE";
jump: Nullable<number>;
currentItemId: Nullable<number>;
sessionId: Nullable<string>;
})
// QueueRemoveItemsRequest
| (MediaReqBase & {
type: "QUEUE_REMOVE";
itemIds: number[];
sessionId: Nullable<string>;
})
// QueueReorderItemsRequest
| (MediaReqBase & {
type: "QUEUE_REORDER";
itemIds: number[];
insertBefore: Nullable<number>;
sessionId: Nullable<string>;
})
// QueueSetPropertiesRequest
| (MediaReqBase & {
type: "QUEUE_UPDATE";
repeatMode: Nullable<string>;
sessionId: Nullable<string>;
});
export type ReceiverMediaMessage =
| (MediaReqBase & { type: "MEDIA_STATUS"; status: MediaStatus[] })
| (MediaReqBase & { type: "INVALID_PLAYER_STATE" })
| (MediaReqBase & { type: "LOAD_FAILED" })
| (MediaReqBase & { type: "LOAD_CANCELLED" })
| (MediaReqBase & { type: "INVALID_REQUEST" });