Rename shim -> cast

This commit is contained in:
hensm
2022-03-15 07:00:45 +00:00
parent d7592d5806
commit 9af43c2910
51 changed files with 182 additions and 177 deletions

297
ext/src/cast/api/Session.ts Normal file
View File

@@ -0,0 +1,297 @@
"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 { Image, Receiver, SenderApplication } from "./dataClasses";
import { SessionStatus } from "./enums";
import { Media, LoadRequest, QueueLoadRequest, QueueItem } from "./media";
const NS_MEDIA = "urn:x-cast:com.google.cast.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 props
for (const prop in status) {
if (prop !== "items" && status.hasOwnProperty(prop)) {
(media as any)[prop] = (status as any)[prop];
}
}
// 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 {
#id = uuid();
#isConnected = false;
#loadMediaSuccessCallback?: (media: Media) => void;
#loadMediaErrorCallback?: ErrorCallback;
#loadMediaRequest?: LoadRequest;
_messageListeners = new Map<string, Set<MessageListener>>();
_updateListeners = new Set<UpdateListener>();
_sendMessageCallbacks = new Map<
string,
[SuccessCallback?, ErrorCallback?]
>();
/**
*
*/
#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
);
console.info(media);
// 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.
* urn:x-cast:com.google.cast.media
*/
#sendMediaMessage = (
message: DistributiveOmit<SenderMediaMessage, "requestId">
) => {
return new Promise<void>((resolve, reject) => {
this.sendMessage(
"urn:x-cast:com.google.cast.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]);
});
};
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);
}
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;
this.#loadMediaRequest = loadRequest;
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);
}
}

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.VIDEO_OUT, Capability.AUDIO_OUT],
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/api/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"
}

440
ext/src/cast/api/index.ts Normal file
View File

@@ -0,0 +1,440 @@
"use strict";
import logger from "../../lib/logger";
import { ReceiverDevice } 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 "./dataClasses";
import Session from "./Session";
type ReceiverActionListener = (
receiver: Receiver,
receiverAction: string
) => void;
type RequestSessionSuccessCallback = (session: Session) => void;
let apiConfig: Nullable<ApiConfig>;
let sessionRequest: Nullable<SessionRequest>;
let requestSessionSuccessCallback: Nullable<RequestSessionSuccessCallback>;
let requestSessionErrorCallback: Nullable<ErrorCallback>;
const receiverActionListeners = new Set<ReceiverActionListener>();
const receiverDevices = new Map<string, ReceiverDevice>();
const sessions = new Map<string, Session>();
export {
AutoJoinPolicy,
Capability,
DefaultActionPolicy,
DialAppState,
ErrorCode,
ReceiverAction,
ReceiverAvailability,
ReceiverType,
SenderPlatform,
SessionStatus,
VolumeControlType
};
export {
ApiConfig,
CredentialsData,
DialRequest,
Error_ as Error,
Image,
Receiver,
ReceiverDisplayStatus,
SenderApplication,
SessionRequest,
Timeout,
Volume,
Session
};
export const VERSION = [1, 2];
export let isAvailable = false;
export const timeout = new Timeout();
// chrome.cast.media namespace
export * as media from "./media";
function sendSessionRequest(
sessionRequest: SessionRequest,
receiverDevice: ReceiverDevice
) {
for (const listener of receiverActionListeners) {
const receiver = new Receiver(
receiverDevice.id,
receiverDevice.friendlyName
);
listener(receiver, ReceiverAction.CAST);
}
sendMessageResponse({
subject: "bridge:createCastSession",
data: {
appId: sessionRequest.appId,
receiverDevice: receiverDevice
}
});
}
export function initialize(
newApiConfig: ApiConfig,
successCallback?: SuccessCallback,
errorCallback?: ErrorCallback
) {
logger.info("cast.initialize");
// Already initialized
if (apiConfig) {
errorCallback?.(new Error_(ErrorCode.INVALID_PARAMETER));
return;
}
apiConfig = newApiConfig;
sendMessageResponse({
subject: "main:castReady",
data: { appId: apiConfig.sessionRequest.appId }
});
successCallback?.();
apiConfig.receiverListener(
receiverDevices.size
? ReceiverAvailability.AVAILABLE
: ReceiverAvailability.UNAVAILABLE
);
}
export function requestSession(
successCallback: RequestSessionSuccessCallback,
errorCallback: ErrorCallback,
newSessionRequest?: SessionRequest,
receiverDevice?: ReceiverDevice
) {
logger.info("cast.requestSession");
// Not yet initialized
if (!apiConfig) {
errorCallback?.(new Error_(ErrorCode.API_NOT_INITIALIZED));
return;
}
// Already requesting session
if (sessionRequest) {
errorCallback?.(
new Error_(
ErrorCode.INVALID_PARAMETER,
"Session request already in progress."
)
);
return;
}
// No receivers available
if (!receiverDevices.size) {
errorCallback?.(new Error_(ErrorCode.RECEIVER_UNAVAILABLE));
return;
}
/**
* Store session request for use in return message from
* receiver selection.
*/
sessionRequest = newSessionRequest ?? apiConfig.sessionRequest;
requestSessionSuccessCallback = successCallback;
requestSessionErrorCallback = errorCallback;
/**
* If a receiver was provided, skip the receiver selector
* process.
*/
if (receiverDevice) {
if (receiverDevice?.id && receiverDevices.has(receiverDevice.id)) {
sendSessionRequest(sessionRequest, receiverDevice);
}
} else {
// Open receiver selector UI
sendMessageResponse({
subject: "main:selectReceiver"
});
}
}
export function requestSessionById(_sessionId: string): void {
logger.info("STUB :: cast.requestSessionById");
}
export function setCustomReceivers(
_receivers: Receiver[],
_successCallback?: SuccessCallback,
_errorCallback?: ErrorCallback
): void {
logger.info("STUB :: cast.setCustomReceivers");
}
export function setPageContext(_win: Window): void {
logger.info("STUB :: cast.setPageContext");
}
export function setReceiverDisplayStatus(_sessionId: string): void {
logger.info("STUB :: cast.setReceiverDisplayStatus");
}
export function unescape(escaped: string): string {
return window.decodeURI(escaped);
}
export function addReceiverActionListener(listener: ReceiverActionListener) {
receiverActionListeners.add(listener);
}
export function removeReceiverActionListener(listener: ReceiverActionListener) {
receiverActionListeners.delete(listener);
}
export function logMessage(message: string) {
logger.info("cast.logMessage", message);
}
export function precache(_data: string) {
logger.info("STUB :: cast.precache");
}
onMessage(message => {
switch (message.subject) {
case "cast:initialized": {
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:sessionCreated"
});
const status = message.data;
// TODO: Implement persistent per-origin receiver IDs
const receiver = new Receiver(
status.receiverFriendlyName, // label
status.receiverFriendlyName, // friendlyName
[Capability.VIDEO_OUT, Capability.AUDIO_OUT], // capabilities
status.volume // volume
);
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;
sessions.set(session.sessionId, session);
}
// eslint-disable-next-line no-fallthrough
case "cast:sessionUpdated": {
const status = message.data;
const session = 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 (requestSessionSuccessCallback) {
requestSessionSuccessCallback(session);
requestSessionSuccessCallback = null;
requestSessionErrorCallback = null;
}
break;
}
case "cast:sessionStopped": {
const { sessionId } = message.data;
const session = 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 = 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 = 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:serviceUp": {
const { receiverDevice } = message.data;
if (receiverDevices.has(receiverDevice.id)) {
break;
}
receiverDevices.set(receiverDevice.id, receiverDevice);
if (apiConfig) {
// Notify listeners of new cast destination
apiConfig.receiverListener(ReceiverAvailability.AVAILABLE);
}
break;
}
case "cast:serviceDown": {
const { receiverDeviceId } = message.data;
receiverDevices.delete(receiverDeviceId);
if (receiverDevices.size === 0) {
if (apiConfig) {
apiConfig.receiverListener(
ReceiverAvailability.UNAVAILABLE
);
}
}
break;
}
case "cast:selectReceiver/selected": {
logger.info("Selected receiver");
if (!sessionRequest) {
break;
}
sendSessionRequest(sessionRequest, message.data.receiver);
break;
}
case "cast:selectReceiver/stopped": {
const { receiver } = message.data;
logger.info("Stopped receiver");
if (sessionRequest) {
sessionRequest = null;
for (const listener of receiverActionListeners) {
const castReceiver = new Receiver(
receiver.id,
receiver.friendlyName
);
listener(castReceiver, ReceiverAction.STOP);
}
}
break;
}
/**
* Popup closed before session established.
*/
case "cast:selectReceiver/cancelled": {
if (sessionRequest) {
sessionRequest = null;
requestSessionErrorCallback?.(new Error_(ErrorCode.CANCEL));
}
break;
}
}
});

View File

@@ -0,0 +1,438 @@
"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<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 "../dataClasses";
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,22 @@
"use strict";
import Media from "./Media";
export { Media };
export * from "./dataClasses";
export * from "./enums";
export const timeout = {
editTracksInfo: 0,
getStatus: 0,
load: 0,
pause: 0,
play: 0,
queue: 0,
seek: 0,
setVolume: 0,
stop: 0
};
export const DEFAULT_MEDIA_RECEIVER_APP_ID = "CC1AD845";

171
ext/src/cast/api/types.ts Normal file
View File

@@ -0,0 +1,171 @@
"use strict";
/**
* Keep in sync with bridge types at:
* app/src/bridge/components/cast/types.ts
*/
import { SenderApplication, Volume, Image } from "./dataClasses";
import { MediaInfo, QueueItem } from "./media/dataClasses";
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 CastSessionUpdated {
sessionId: string;
statusText: string;
namespaces: Array<{ name: string }>;
volume: Volume;
}
export interface CastSessionCreated extends CastSessionUpdated {
appId: string;
appImages: Image[];
displayName: 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" });

42
ext/src/cast/content.ts Normal file
View File

@@ -0,0 +1,42 @@
"use strict";
import { CAST_LOADER_SCRIPT_URL, CAST_SCRIPT_URLS } from "../lib/endpoints";
const _window = window.wrappedJSObject as any;
_window.chrome = cloneInto({}, window);
/**
* YouTube won't load the cast SDK unless it thinks the
* presentation API exists.
*/
if (window.location.host === "www.youtube.com") {
_window.navigator.presentation = cloneInto({}, window);
}
/**
* Replace the src property setter on <script> elements to
* intercept the new value.
*
* If it matches one of Chrome's cast extension sender script
* URLs, replace it with the standard API URL, the request for
* which is handled in the main script.
*/
const desc = Reflect.getOwnPropertyDescriptor(
HTMLScriptElement.prototype.wrappedJSObject,
"src"
);
Reflect.defineProperty(HTMLScriptElement.prototype.wrappedJSObject, "src", {
configurable: true,
enumerable: true,
get: desc?.get,
set: exportFunction(function setFunc(this: HTMLScriptElement, value) {
if (CAST_SCRIPT_URLS.includes(value)) {
return desc?.set?.call(this, CAST_LOADER_SCRIPT_URL);
}
return desc?.set?.call(this, value);
}, window)
});

View File

@@ -0,0 +1,21 @@
"use strict";
import { onMessageResponse, sendMessage } from "./eventMessageChannel";
import messaging, { Message } from "../messaging";
// Message port to background script
export const backgroundPort = messaging.connect({ name: "cast" });
const forwardToCast = (message: Message) => sendMessage(message);
const forwardToMain = (message: Message) => backgroundPort.postMessage(message);
// Add message listeners
backgroundPort.onMessage.addListener(forwardToCast);
const listener = onMessageResponse(forwardToMain);
// Remove listeners
backgroundPort.onDisconnect.addListener(() => {
backgroundPort.onMessage.removeListener(forwardToCast);
listener.disconnect();
});

View File

@@ -0,0 +1,79 @@
"use strict";
import { Message } from "../messaging";
type ListenerFunc = (message: Message) => void;
export interface ListenerObject {
disconnect(): void;
}
export function onMessage(listener: ListenerFunc): ListenerObject {
function on__castMessage(ev: CustomEvent) {
listener(JSON.parse(ev.detail));
/**
* TODO:
* Figure out a way to handle and stop propagation of this
* event to hide it from page scripts.
* Currently the event handler is set after the page loads the
* cast API, allowing pages set handlers before this script,
* intercept the event, and cancel it.
*/
ev.stopPropagation();
}
// @ts-ignore
document.addEventListener("__castMessage", on__castMessage, true);
return {
disconnect() {
// @ts-ignore
document.removeEventListener(
"__castMessage",
on__castMessage,
true
);
}
};
}
export function sendMessageResponse(message: Message) {
const event = new CustomEvent("__castMessageResponse", {
detail: JSON.stringify(message)
});
document.dispatchEvent(event);
}
export function onMessageResponse(listener: ListenerFunc): ListenerObject {
function on__castMessageResponse(ev: CustomEvent) {
listener(JSON.parse(ev.detail));
}
// @ts-ignore
document.addEventListener(
"__castMessageResponse",
on__castMessageResponse,
true
);
return {
disconnect() {
// @ts-ignore
document.removeEventListener(
"__castMessageResponse",
on__castMessageResponse,
true
);
}
};
}
export function sendMessage(message: Message) {
const event = new CustomEvent("__castMessage", {
detail: JSON.stringify(message)
});
document.dispatchEvent(event);
}

108
ext/src/cast/export.ts Normal file
View File

@@ -0,0 +1,108 @@
"use strict";
import * as cast from "./api";
import { Message } from "../messaging";
import { BridgeInfo } from "../lib/bridge";
import { TypedMessagePort } from "../lib/TypedMessagePort";
import {
onMessage,
onMessageResponse,
sendMessage
} from "./eventMessageChannel";
let initializedBridgeInfo: BridgeInfo;
let initializedBackgroundPort: MessagePort;
/**
* To support exporting an API from a module, we need to
* retain the event-based message passing despite not
* actually crossing any context boundaries. The cast instance
* listens for and emits these messages, and changing that
* behavior is too messy.
*/
export function ensureInit(): Promise<TypedMessagePort<Message>> {
return new Promise(async (resolve, reject) => {
// If already initialized, just return existing bridge info
if (initializedBridgeInfo) {
if (initializedBridgeInfo.isVersionCompatible) {
resolve(initializedBackgroundPort);
} else {
reject();
}
return;
}
const channel = new MessageChannel();
initializedBackgroundPort = channel.port1;
/**
* If the module is imported into a background script
* context, the location will be the internal extension URL,
* whereas in a content script, it will be the content page
* URL.
*/
if (window.location.protocol === "moz-extension:") {
const { default: CastManager } = await import(
"../background/CastManager"
);
// port2 will post bridge messages to port 1
await CastManager.init();
await CastManager.createInstance(channel.port2);
// bridge -> cast instance
channel.port1.onmessage = ev => {
const message = ev.data as Message;
// Send message to cast instance
sendMessage(message);
handleIncomingMessageToCast(message);
};
// cast instance -> bridge
onMessageResponse(message => {
channel.port1.postMessage(message);
});
} else {
/**
* Import reference to message port created by contentBridge.
* Creation of the port triggers side-effects in the
* background script.
*/
const { backgroundPort } = await import("./contentBridge");
// backgroundPort -> channel.port2
backgroundPort.onMessage.addListener((message: Message) => {
channel.port2.postMessage(message);
});
// channel.port2 -> backgroundPort
channel.port2.onmessage = ev => {
const message = ev.data as Message;
backgroundPort.postMessage(message);
};
// Handle cast messages
onMessage(handleIncomingMessageToCast);
}
function handleIncomingMessageToCast(message: Message) {
switch (message.subject) {
case "cast:initialized": {
initializedBridgeInfo = message.data;
if (initializedBridgeInfo.isVersionCompatible) {
resolve(initializedBackgroundPort);
} else {
reject();
}
}
}
}
});
}
export default cast;

View File

@@ -0,0 +1,86 @@
"use strict";
import logger from "../../lib/logger";
/**
* Custom element for a cast button used by sites that injects
* a cast icon and manages visibility state and event handling.
*/
export default class GoogleCastLauncher extends HTMLElement {
constructor() {
super();
this.style.display = "none";
const style = document.createElement("style");
style.textContent = `
.cast_caf_state_c {
fill: var(--connected-color, #4285f4);
}
.cast_caf_state_d {
fill: var(--disconnected-color, #7d7d7d);
}
.cast_caf_state_h {
opacity: 0;
}
`;
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
const icon = document.createElementNS(SVG_NAMESPACE, "svg");
const iconArch0 = document.createElementNS(SVG_NAMESPACE, "path");
const iconArch1 = document.createElementNS(SVG_NAMESPACE, "path");
const iconArch2 = document.createElementNS(SVG_NAMESPACE, "path");
const iconBox = document.createElementNS(SVG_NAMESPACE, "path");
const iconBoxFill = document.createElementNS(SVG_NAMESPACE, "path");
// Set SVG attributes
icon.setAttribute("x", "0");
icon.setAttribute("y", "0");
icon.setAttribute("width", "100%");
icon.setAttribute("height", "100%");
icon.setAttribute("viewBox", "0 0 24 24");
iconArch0.classList.add("cast_caf_state_d");
iconArch0.setAttribute("id", "cast_caf_icon_arch0");
iconArch0.setAttribute("d", "M1 18v3h3c0-1.7-1.34-3-3-3z");
iconArch1.classList.add("cast_caf_state_d");
iconArch1.setAttribute("id", "cast_caf_icon_arch1");
iconArch1.setAttribute(
"d",
"M1 14v2c2.76 0 5 2.2 5 5h2c0-3.87-3.13-7-7-7z"
);
iconArch2.classList.add("cast_caf_state_d");
iconArch2.setAttribute("id", "cast_caf_icon_arch2");
iconArch2.setAttribute(
"d",
"M1 10v2c4.97 0 9 4 9 9h2c0-6.08-4.93-11-11-11z"
);
iconBox.classList.add("cast_caf_state_d");
iconBox.setAttribute("id", "cast_caf_icon_box");
iconBox.setAttribute(
"d",
"M21 3H3c-1.1 0-2 .9-2 2v3h2V5h18v14h-7v2h7c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"
);
iconBoxFill.classList.add("cast_caf_state_h");
iconBoxFill.setAttribute("id", "cast_caf_icon_boxfill");
iconBoxFill.setAttribute(
"d",
"M5 7v1.63C8 8.6 13.37 14 13.37 17H19V7z"
);
// Add icon paths to SVG
icon.append(iconArch0, iconArch1, iconArch2, iconBox, iconBoxFill);
const shadow = this.attachShadow({ mode: "open" });
shadow.append(icon, style);
this.addEventListener("click", () => {
logger.info("<google-cast-launcher> onClick");
});
}
}

View File

@@ -0,0 +1,11 @@
"use strict";
import EventData from "./EventData";
import { SessionEventType } from "../enums";
export default class ActiveInputStateEventData extends EventData {
constructor(public activeInputState: number) {
super(SessionEventType.ACTIVE_INPUT_STATE_CHANGED);
}
}

View File

@@ -0,0 +1,21 @@
"use strict";
import * as cast from "../../api";
export default class ApplicationMetadata {
public applicationId: string;
public images: cast.Image[];
public name: string;
public namespaces: string[];
constructor(sessionObj: cast.Session) {
this.applicationId = sessionObj.appId;
this.images = sessionObj.appImages;
this.name = sessionObj.displayName;
// Convert [{ name: <ns> }, ...] to [ <ns>, ... ]
this.namespaces = sessionObj.namespaces.map(
namespaceObj => namespaceObj.name
);
}
}

View File

@@ -0,0 +1,12 @@
"use strict";
import ApplicationMetadata from "./ApplicationMetadata";
import EventData from "./EventData";
import { SessionEventType } from "../enums";
export default class ApplicationMetadataEventData extends EventData {
constructor(public metadata: ApplicationMetadata) {
super(SessionEventType.APPLICATION_METADATA_CHANGED);
}
}

View File

@@ -0,0 +1,11 @@
"use strict";
import EventData from "./EventData";
import { SessionEventType } from "../enums";
export default class ApplicationStatusEventData extends EventData {
constructor(public status: string) {
super(SessionEventType.APPLICATION_STATUS_CHANGED);
}
}

View File

@@ -0,0 +1,38 @@
"use strict";
import logger from "../../../lib/logger";
import CastOptions from "./CastOptions";
import CastSession from "./CastSession";
export default class CastContext extends EventTarget {
public endCurrentSession(_stopCasting: boolean): void {
logger.info("STUB :: CastContext#endCurrentSession");
}
// @ts-ignore
public getCastState(): string {
logger.info("STUB :: CastContext#getCastState");
}
// @ts-ignore
public getCurrentSession(): CastSession {
logger.info("STUB :: CastContext#getCurrentSession");
}
// @ts-ignore
public getSessionState(): string {
logger.info("STUB :: CastContext#getSessionState");
}
// @ts-ignore
public requestSession(): Promise<string> {
logger.info("STUB :: CastContext#requestSession");
}
public setOptions(_options: CastOptions): void {
logger.info("STUB :: CastContext#setOptions");
}
}
export const instance = new CastContext();

View File

@@ -0,0 +1,25 @@
"use strict";
import * as cast from "../../api";
export default class CastOptions {
public autoJoinPolicy: string = cast.AutoJoinPolicy.TAB_AND_ORIGIN_SCOPED;
public language: string | null = null;
public receiverApplicationId: string | null = null;
public resumeSavedSession = true;
constructor(options: CastOptions = {} as CastOptions) {
if (options.autoJoinPolicy) {
this.autoJoinPolicy = options.autoJoinPolicy;
}
if (options.language) {
this.language = options.language;
}
if (options.receiverApplicationId) {
this.receiverApplicationId = options.receiverApplicationId;
}
if (options.resumeSavedSession) {
this.resumeSavedSession = options.resumeSavedSession;
}
}
}

View File

@@ -0,0 +1,107 @@
"use strict";
import logger from "../../../lib/logger";
import * as cast from "../../api";
import ApplicationMetadata from "./ApplicationMetadata";
type MessageListener = (namespace: string, message: string) => void;
export default class CastSession extends EventTarget {
constructor(_sessionObj: cast.Session, _state: string) {
super();
logger.info("STUB :: CastSession#constructor");
}
public addMessageListener(
_namespace: string,
_listener: MessageListener
): void {
logger.info("STUB :: CastSession#addMessageListener");
}
public endSession(_stopCasting: boolean): void {
logger.info("STUB :: CastSession#endSession");
}
// @ts-ignore
public getActiveInputState(): number {
logger.info("STUB :: CastSession#getActiveInputState");
}
// @ts-ignore
public getApplicationMetadata(): ApplicationMetadata {
logger.info("STUB :: CastSession#getApplicationMetadata");
}
// @ts-ignore
public getApplicationStatus(): string {
logger.info("STUB :: CastSession#getApplicationStatus");
}
// @ts-ignore
public getCastDevice(): cast.Receiver {
logger.info("STUB :: CastSession#getCastDevice");
}
// @ts-ignore
public getMediaSession(): cast.media.Media {
logger.info("STUB :: CastSession#getMediaSession");
}
// @ts-ignore
public getSessionId(): string {
logger.info("STUB :: CastSession#getSessionId");
}
// @ts-ignore
public getSessionObj(): cast.Session {
logger.info("STUB :: CastSession#getSessionObj");
}
// @ts-ignore
public getSessionState(): string {
logger.info("STUB :: CastSession#getSessionState");
}
// @ts-ignore
public getVolume(): number {
logger.info("STUB :: CastSession#getVolume");
}
// @ts-ignore
public isMute(): boolean {
logger.info("STUB :: CastSession#isMute");
}
// @ts-ignore
public loadMedia(_loadRequest: cast.media.LoadRequest): Promise<string> {
logger.info("STUB :: CastSession#loadMedia");
}
public removeMessageListener(
_namespace: string,
_listener: MessageListener
): void {
logger.info("STUB :: CastSession#removeMessageListener");
}
public sendMessage(
_namespace: string,
// @ts-ignore
_data: any
): Promise<void> {
logger.info("STUB :: CastSession#sendMessage");
}
// @ts-ignore
public setMute(_isMute: boolean): Promise<void> {
logger.info("STUB :: CastSession#setMute");
}
// @ts-ignore
public setVolume(_volume: number): Promise<void> {
logger.info("STUB :: CastSession#setVolume");
}
}

View File

@@ -0,0 +1,11 @@
"use strict";
import EventData from "./EventData";
import { CastContextEventType } from "../enums";
export default class CastStateEventData extends EventData {
constructor(public castState: string) {
super(CastContextEventType.CAST_STATE_CHANGED);
}
}

View File

@@ -0,0 +1,5 @@
"use strict";
export default class EventData {
constructor(public type: string) {}
}

View File

@@ -0,0 +1,13 @@
"use strict";
import * as cast from "../../api";
import EventData from "./EventData";
import { SessionEventType } from "../enums";
export default class MediaSessionEventData extends EventData {
constructor(public mediaSession: cast.media.Media) {
super(SessionEventType.MEDIA_SESSION);
}
}

View File

@@ -0,0 +1,33 @@
"use strict";
import * as cast from "../../api";
import RemotePlayerController from "./RemotePlayerController";
interface SavedPlayerState {
mediaInfo: string;
currentTime: number;
isPaused: boolean;
}
export default class RemotePlayer {
public canControlVolume = false;
public canPause = false;
public canSeek = false;
public controller: RemotePlayerController | null = null;
public currentTime = 0;
public displayName = "";
public displayStatus = "";
public duration = 0;
public imageUrl: string | null = null;
public isConnected = false;
public isMediaLoaded = false;
public isMuted = false;
public isPaused = false;
public mediaInfo: cast.media.MediaInfo | null = null;
public playerState: string | null = null;
public savedPlayerState: SavedPlayerState | null = null;
public statusText = "";
public title = "";
public volumeLevel = 1;
}

View File

@@ -0,0 +1,5 @@
"use strict";
export default class RemotePlayerChangedEvent {
constructor(public type: string, public field: string, public value: any) {}
}

View File

@@ -0,0 +1,50 @@
"use strict";
import logger from "../../../lib/logger";
import RemotePlayer from "./RemotePlayer";
export default class RemotePlayerController extends EventTarget {
constructor(_player: RemotePlayer) {
super();
logger.info("STUB :: RemotePlayerController#constructor");
}
public getFormattedTime(timeInSec: number): string {
const hours = Math.floor(timeInSec / 3600) % 24;
const minutes = Math.floor(timeInSec / 60) % 60;
const seconds = timeInSec % 60;
return [hours, minutes, seconds]
.map(c => c.toString().padStart(2, "0"))
.join(":");
}
public getSeekPosition(currentTime: number, duration: number) {
return (currentTime / duration) * 100;
}
public getSeekTime(currentPosition: number, duration: number) {
return (duration / 100) * currentPosition;
}
public muteOrUnmute(): void {
logger.info("STUB :: RemotePlayerController#muteOrUnmute");
}
public playOrPause(): void {
logger.info("STUB :: RemotePlayerController#playOrPause");
}
public seek(): void {
logger.info("STUB :: RemotePlayerController#seek");
}
public setVolumeLevel(): void {
logger.info("STUB :: RemotePlayerController#setVolumeLevel");
}
public stop(): void {
logger.info("STUB :: RemotePlayerController#stop");
}
}

View File

@@ -0,0 +1,16 @@
"use strict";
import CastSession from "./CastSession";
import EventData from "./EventData";
import { SessionEventType } from "../enums";
export default class SessionStateEventData extends EventData {
constructor(
public session: CastSession,
public sessionState: string,
public errorCode: string | null = null
) {
super(SessionEventType.APPLICATION_STATUS_CHANGED);
}
}

View File

@@ -0,0 +1,9 @@
"use strict";
import { SessionEventType } from "../enums";
export default class VolumeEventData {
public type = SessionEventType.VOLUME_CHANGED;
constructor(public volume: number, public isMute: boolean) {}
}

View File

@@ -0,0 +1,64 @@
"use strict";
export enum ActiveInputState {
ACTIVE_INPUT_STATE_UNKNOWN = -1,
ACTIVE_INPUT_STATE_NO = 0,
ACTIVE_INPUT_YES = 1
}
export enum CastContextEventType {
CAST_STATE_CHANGED = "caststatechanged",
SESSION_STATE_CHANGED = "sessionstatechanged"
}
export enum CastState {
NO_DEVICES_AVAILABLE = "NO_DEVICES_AVAILABLE",
NOT_CONNECTED = "NOT_CONNECTED",
CONNECTING = "CONNECTING",
CONNECTED = "CONNECTED"
}
export enum LoggerLevel {
DEBUG = 0,
INFO = 800,
WARNING = 900,
ERROR = 1000,
NONE = 1500
}
export enum RemotePlayerEventType {
ANY_CHANGE = "anyChanged",
IS_CONNECTED_CHANGE = "isConnectedChanged",
IS_MEDIA_LOADED_CHANGED = "isMediaLoadedChanged",
DURATION_CHANGED = "durationChanged",
CURRENT_TIME_CHANGED = "currentTimeChanged",
IS_PAUSED_CHANGED = "isPausedChanged",
VOLUME_LEVEL_CHANGED = "volumeLevelChanged",
CAN_CONTROL_VOLUME_CHANGED = "canControlVolumeChanged",
IS_MUTED_CHANGED = "isMutedChanged",
CAN_PAUSE_CHANGED = "canPauseChanged",
CAN_SEEK_CHANGED = "canSeekChanged",
DISPLAY_NAME_CHANGED = "displayNameChanged",
STATUS_TEXT_CHANGED = "statusTextChanged",
MEDIA_INFO_CHANGED = "mediaInfoChanged",
IMAGE_URL_CHANGED = "imageUrlChanged",
PLAYER_STATE_CHANGED = "playerStateChanged"
}
export enum SessionEventType {
APPLICATION_STATUS_CHANGED = "applicationstatuschanged",
APPLICATION_METADATA_CHANGED = "applicationmetadatachanged",
ACTIVE_INPUT_STATE_CHANGED = "activeinputstatechanged",
VOLUME_CHANGED = "volumechanged",
MEDIA_SESSION = "mediasession"
}
export enum SessionState {
NO_SESSION = "NO_SESSION",
SESSION_STARTING = "SESSION_STARTING",
SESSION_STARTED = "SESSION_STARTED",
SESSION_START_FAILED = "SESSION_START_FAILED",
SESSION_ENDING = "SESSION_ENDING",
SESSION_ENDED = "SESSION_ENDED",
SESSION_RESUMED = "SESSION_RESUMED"
}

View File

@@ -0,0 +1,91 @@
"use strict";
import logger from "../../lib/logger";
import ActiveInputStateEventData from "./classes/ActiveInputStateEventData";
import ApplicationMetadata from "./classes/ApplicationMetadata";
import ApplicationMetadataEventData from "./classes/ApplicationMetadataEventData";
import ApplicationStatusEventData from "./classes/ApplicationStatusEventData";
import CastContext, { instance } from "./classes/CastContext";
import CastOptions from "./classes/CastOptions";
import CastSession from "./classes/CastSession";
import CastStateEventData from "./classes/CastStateEventData";
import EventData from "./classes/EventData";
import MediaSessionEventData from "./classes/MediaSessionEventData";
import RemotePlayer from "./classes/RemotePlayer";
import RemotePlayerChangedEvent from "./classes/RemotePlayerChangedEvent";
import RemotePlayerController from "./classes/RemotePlayerController";
import SessionStateEventData from "./classes/SessionStateEventData";
import VolumeEventData from "./classes/VolumeEventData";
import {
ActiveInputState,
CastContextEventType,
CastState,
LoggerLevel,
RemotePlayerEventType,
SessionEventType,
SessionState
} from "./enums";
import GoogleCastLauncher from "./GoogleCastLauncher";
export default {
// Enums
ActiveInputState,
CastContextEventType,
CastState,
LoggerLevel,
RemotePlayerEventType,
SessionEventType,
SessionState,
// Classes
ActiveInputStateEventData,
ApplicationMetadata,
ApplicationMetadataEventData,
ApplicationStatusEventData,
CastOptions,
CastSession,
CastStateEventData,
EventData,
MediaSessionEventData,
RemotePlayer,
RemotePlayerChangedEvent,
RemotePlayerController,
SessionStateEventData,
VolumeEventData,
/**
* CastContext class with an extra getInstance method used to
* instantiate and fetch a singleton instance.
*/
CastContext: {
...CastContext,
getInstance() {
return instance;
}
},
VERSION: "1.0.07",
setLoggerLevel(_level: number) {
logger.info("STUB :: cast.framework.setLoggerLevel");
}
};
/**
* The Framework API defines a <google-cast-launcher> element
* and a <button is="google-cast-button"> element extension,
* both of which produce the same result.
*
* Chrome allowed custom elements to extend <button> elements
* via Element#createShadowRoot, but the standard
* Element#attachShadow method supported in Firefox specifies a
* limited whitelist of elements that are extendable.
*
* It's not officially advertised in the cast docs, so it
* shouldn't be much of a compatibility issue to ignore it.
*/
customElements.define("google-cast-launcher", GoogleCastLauncher);

73
ext/src/cast/index.ts Normal file
View File

@@ -0,0 +1,73 @@
"use strict";
import * as cast from "./api";
import { CAST_FRAMEWORK_SCRIPT_URL } from "../lib/endpoints";
import { loadScript } from "../lib/utils";
import { onMessage } from "./eventMessageChannel";
const _window = window as any;
if (!_window.chrome) {
_window.chrome = {};
}
// Create page-accessible API object
_window.chrome.cast = cast;
let bridgeInfo: any;
let isFramework = false;
// Call page's API loaded function if defined
function callPageReadyFunction() {
const readyFunction = _window.__onGCastApiAvailable;
if (readyFunction && typeof readyFunction === "function") {
readyFunction(bridgeInfo && bridgeInfo.isVersionCompatible);
}
}
/**
* If loaded within a page via a <script> element,
* document.currentScript should exist and we can check its
* [src] query string for the loadCastFramework param.
*/
if (document.currentScript) {
const currentScript = document.currentScript as HTMLScriptElement;
const currentScriptUrl = new URL(currentScript.src);
const currentScriptParams = new URLSearchParams(currentScriptUrl.search);
// Load Framework API if requested
if (currentScriptParams.get("loadCastFramework") === "1") {
if (!_window.cast) {
_window.cast = {};
}
isFramework = true;
const script = loadScript(CAST_FRAMEWORK_SCRIPT_URL);
script.addEventListener("load", () => {
callPageReadyFunction();
});
/*
// TODO: Finish cast.framework and replace Google's implementation
import("./framework").then(framework => {
_window.cast.framework = framework.default;
});
*/
}
}
onMessage(message => {
switch (message.subject) {
case "cast:initialized": {
bridgeInfo = message.data;
if (!isFramework) {
callPageReadyFunction();
}
break;
}
}
});

12
ext/src/cast/types.ts Normal file
View File

@@ -0,0 +1,12 @@
"use strict";
import { Error as Error_ } from "./api/dataClasses";
import { Media } from "./api/media";
export type SuccessCallback = () => void;
export type ErrorCallback = (err: Error_) => void;
export type MediaListener = (media: Media) => void;
export type MessageListener = (namespace: string, message: string) => void;
export type UpdateListener = (isAlive: boolean) => void;
export type LoadSuccessCallback = (media: Media) => void;