mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-09 17:19:59 +00:00
Rename shim -> cast
This commit is contained in:
297
ext/src/cast/api/Session.ts
Normal file
297
ext/src/cast/api/Session.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
104
ext/src/cast/api/dataClasses.ts
Normal file
104
ext/src/cast/api/dataClasses.ts
Normal 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
75
ext/src/cast/api/enums.ts
Normal 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
440
ext/src/cast/api/index.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
438
ext/src/cast/api/media/Media.ts
Normal file
438
ext/src/cast/api/media/Media.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
380
ext/src/cast/api/media/dataClasses.ts
Normal file
380
ext/src/cast/api/media/dataClasses.ts
Normal 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) {}
|
||||
}
|
||||
139
ext/src/cast/api/media/enums.ts
Normal file
139
ext/src/cast/api/media/enums.ts
Normal 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"
|
||||
}
|
||||
22
ext/src/cast/api/media/index.ts
Normal file
22
ext/src/cast/api/media/index.ts
Normal 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
171
ext/src/cast/api/types.ts
Normal 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
42
ext/src/cast/content.ts
Normal 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)
|
||||
});
|
||||
21
ext/src/cast/contentBridge.ts
Normal file
21
ext/src/cast/contentBridge.ts
Normal 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();
|
||||
});
|
||||
79
ext/src/cast/eventMessageChannel.ts
Normal file
79
ext/src/cast/eventMessageChannel.ts
Normal 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
108
ext/src/cast/export.ts
Normal 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;
|
||||
86
ext/src/cast/framework/GoogleCastLauncher.ts
Normal file
86
ext/src/cast/framework/GoogleCastLauncher.ts
Normal 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");
|
||||
});
|
||||
}
|
||||
}
|
||||
11
ext/src/cast/framework/classes/ActiveInputStateEventData.ts
Normal file
11
ext/src/cast/framework/classes/ActiveInputStateEventData.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
21
ext/src/cast/framework/classes/ApplicationMetadata.ts
Normal file
21
ext/src/cast/framework/classes/ApplicationMetadata.ts
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
11
ext/src/cast/framework/classes/ApplicationStatusEventData.ts
Normal file
11
ext/src/cast/framework/classes/ApplicationStatusEventData.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
38
ext/src/cast/framework/classes/CastContext.ts
Normal file
38
ext/src/cast/framework/classes/CastContext.ts
Normal 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();
|
||||
25
ext/src/cast/framework/classes/CastOptions.ts
Normal file
25
ext/src/cast/framework/classes/CastOptions.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
107
ext/src/cast/framework/classes/CastSession.ts
Normal file
107
ext/src/cast/framework/classes/CastSession.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
11
ext/src/cast/framework/classes/CastStateEventData.ts
Normal file
11
ext/src/cast/framework/classes/CastStateEventData.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
5
ext/src/cast/framework/classes/EventData.ts
Normal file
5
ext/src/cast/framework/classes/EventData.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
"use strict";
|
||||
|
||||
export default class EventData {
|
||||
constructor(public type: string) {}
|
||||
}
|
||||
13
ext/src/cast/framework/classes/MediaSessionEventData.ts
Normal file
13
ext/src/cast/framework/classes/MediaSessionEventData.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
33
ext/src/cast/framework/classes/RemotePlayer.ts
Normal file
33
ext/src/cast/framework/classes/RemotePlayer.ts
Normal 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;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
"use strict";
|
||||
|
||||
export default class RemotePlayerChangedEvent {
|
||||
constructor(public type: string, public field: string, public value: any) {}
|
||||
}
|
||||
50
ext/src/cast/framework/classes/RemotePlayerController.ts
Normal file
50
ext/src/cast/framework/classes/RemotePlayerController.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
16
ext/src/cast/framework/classes/SessionStateEventData.ts
Normal file
16
ext/src/cast/framework/classes/SessionStateEventData.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
9
ext/src/cast/framework/classes/VolumeEventData.ts
Normal file
9
ext/src/cast/framework/classes/VolumeEventData.ts
Normal 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) {}
|
||||
}
|
||||
64
ext/src/cast/framework/enums.ts
Normal file
64
ext/src/cast/framework/enums.ts
Normal 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"
|
||||
}
|
||||
91
ext/src/cast/framework/index.ts
Normal file
91
ext/src/cast/framework/index.ts
Normal 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
73
ext/src/cast/index.ts
Normal 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
12
ext/src/cast/types.ts
Normal 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;
|
||||
Reference in New Issue
Block a user