Cast API overhaul (#173)

Re-write the shim<->bridge messaging interface and session creation/update handling for better accuracy.
This commit is contained in:
Matt Hensman
2021-05-03 14:37:54 +01:00
committed by GitHub
parent ccac662e74
commit 101c25e26d
25 changed files with 1079 additions and 1346 deletions

View File

@@ -55,5 +55,8 @@
, "@typescript-eslint/no-unused-vars": "off"
, "@typescript-eslint/ban-types": "off"
, "@typescript-eslint/ban-ts-comment": "off"
, "@typescript-eslint/no-this-alias": [ "error", {
"allowedNames": [ "this_" ]
}]
}
}

View File

@@ -0,0 +1,260 @@
"use strict";
import { Channel, Client } from "castv2";
import { sendMessage } from "../../lib/nativeMessaging";
import { ReceiverDevice } from "../../types";
import { ReceiverApplication, ReceiverMessage, SenderMessage } from "./types";
export const NS_CONNECTION = "urn:x-cast:com.google.cast.tp.connection";
export const NS_HEARTBEAT = "urn:x-cast:com.google.cast.tp.heartbeat";
export const NS_RECEIVER = "urn:x-cast:com.google.cast.receiver";
const HEARTBEAT_INTERVAL = 5000;
class CastClient {
protected client = new Client();
protected connectionChannel?: Channel;
protected heartbeatChannel?: Channel;
protected heartbeatIntervalId?: NodeJS.Timeout;
constructor(protected sourceId = "sender-0"
, protected destinationId = "receiver-0") {}
/**
* Create a channel on the client connection with a given
* namespace.
*/
createChannel(namespace: string
, sourceId = this.sourceId
, destinationId = this.destinationId) {
return this.client.createChannel(sourceId, destinationId, namespace, "JSON");
}
connect(host: string, port: number, onHeartbeat?: () => void) {
return new Promise<void>((resolve, reject) => {
// Handle errors
this.client.on("error", reject);
this.client.on("close", () => {
if (this.heartbeatChannel && this.heartbeatIntervalId) {
clearInterval(this.heartbeatIntervalId);
}
});
this.client.connect({ host, port }, () => {
this.connectionChannel = this.createChannel(NS_CONNECTION);
this.heartbeatChannel = this.createChannel(NS_HEARTBEAT);
this.connectionChannel.send({ type: "CONNECT" });
this.heartbeatChannel.send({ type: "PING" });
this.heartbeatIntervalId = setInterval(() => {
this.heartbeatChannel?.send({ type: "PING" });
if (onHeartbeat) {
onHeartbeat();
}
}, HEARTBEAT_INTERVAL);
resolve();
});
});
}
}
type OnSessionCreatedCallback = (sessionId: string) => void;
export default class Session extends CastClient {
// Assigned by the receiver once the session is established
public sessionId?: string;
// Platform messaging
private receiverChannel?: Channel;
private receiverRequestId = 0;
// Receiver app messaging
private transportId?: string;
private transportConnection?: Channel;
private transportHeartbeat?: Channel;
// Channels created by `sendCastSessionMessage` messages
private namespaceChannelMap = new Map<string, Channel>();
/**
* Request ID used to correlate the launch request with the
* RECEIVER_STATUS message associated with session creation.
*/
private launchRequestId?: number;
private onSessionCreated?: OnSessionCreatedCallback;
private establishAppConnection(transportId: string) {
this.transportConnection = this.createChannel(
NS_CONNECTION, this.sourceId, transportId);
this.transportHeartbeat = this.createChannel(
NS_HEARTBEAT, this.sourceId, transportId);
this.transportConnection.send({ type: "CONNECT" });
}
/**
* Handle incoming receiver messages.
*/
private onReceiverMessage = (message: ReceiverMessage) => {
switch (message.type) {
case "RECEIVER_STATUS": {
const { status } = message;
const application = status.applications?.find(
app => app.appId === this.appId);
/**
* If application isn't set, still waiting on the launch
* request response.
*/
if (!this.sessionId) {
// Launch message response only
if (message.requestId !== this.launchRequestId) {
break;
}
if (application) {
this.sessionId = application.sessionId;
this.transportId = application.transportId;
this.establishAppConnection(this.transportId);
this.onSessionCreated?.(this.sessionId);
const { friendlyName } = this.receiverDevice;
sendMessage({
subject: "shim:castSessionCreated"
, data: {
sessionId: this.sessionId
, statusText: application.statusText
, namespaces: application.namespaces
, volume: status.volume
, appId: application.appId
, displayName: application.displayName
, receiverFriendlyName: friendlyName
, transportId: this.sessionId
// TODO: Fix this
, senderApps: []
, appImages: []
}
});
}
break;
}
// Handle session stop
if (!application) {
this.client.close();
break;
}
sendMessage({
subject: "shim:castSessionUpdated"
, data: {
sessionId: this.sessionId
, statusText: application.statusText
, namespaces: application.namespaces
, volume: message.status.volume
}
});
break;
}
case "LAUNCH_ERROR": {
console.error(`err: LAUNCH_ERROR, ${message.reason}`);
this.client.close();
break;
}
}
}
sendMessage(namespace: string, message: unknown) {
let channel = this.namespaceChannelMap.get(namespace);
if (!channel) {
channel = this.createChannel(
namespace, this.sourceId, this.transportId);
channel.on("message", messageData => {
if (!this.sessionId) {
return;
}
messageData = JSON.stringify(messageData);
sendMessage({
subject: "shim:receivedCastSessionMessage"
, data: {
sessionId: this.sessionId
, namespace
, messageData
}
});
});
this.namespaceChannelMap.set(namespace, channel);
}
channel.send(message);
}
sendReceiverMessage(message: DistributiveOmit<SenderMessage, "requestId">) {
if (!this.receiverChannel) {
this.receiverChannel = this.createChannel(NS_RECEIVER);
this.receiverChannel.on("message", this.onReceiverMessage);
}
const requestId = this.receiverRequestId++;
this.receiverChannel?.send({ ...message, requestId });
return requestId;
}
constructor(public appId: string
, public receiverDevice: ReceiverDevice) {
super();
this.client.on("close", () => {
if (this.sessionId) {
sendMessage({
subject: "shim:castSessionStopped"
, data: { sessionId: this.sessionId }
});
}
});
}
async connect(host: string
, port: number
, onSessionCreated?: OnSessionCreatedCallback) {
if (onSessionCreated) {
this.onSessionCreated = onSessionCreated;
}
await super.connect(host, port, () => {
// Include transport heartbeat with platform heartbeat
if (this.transportHeartbeat) {
this.transportHeartbeat.send({ type: "PING" });
}
});
this.launchRequestId = this.sendReceiverMessage({
type: "LAUNCH"
, appId: this.appId
});
}
}

View File

@@ -0,0 +1,114 @@
"use strict";
import { sendMessage } from "../../lib/nativeMessaging";
import { Message } from "../../messaging";
import Session from "./Session";
const sessions = new Map<string, Session>();
export function handleCastMessage(message: Message) {
switch (message.subject) {
case "bridge:createCastSession": {
const { appId, receiverDevice } = message.data;
// Connect and store with returned ID
const session = new Session(appId, receiverDevice);
session.connect(
receiverDevice.host, receiverDevice.port, sessionId => {
sessions.set(sessionId, session);
});
break;
}
case "bridge:sendCastReceiverMessage": {
const { sessionId, messageData, messageId } = message.data;
const session = sessions.get(sessionId);
if (!session) {
sendMessage({
subject: "shim:impl_sendCastMessage"
, data: {
error: "Session does not exist"
, sessionId, messageId
}
});
break;
}
try {
session.sendReceiverMessage(messageData);
} catch (err) {
sendMessage({
subject: "shim:impl_sendCastMessage"
, data: {
error: `Failed to send message (${err})`
, sessionId, messageId
}
});
break;
}
// Success
sendMessage({
subject: "shim:impl_sendCastMessage"
, data: { sessionId, messageId }
});
break;
}
case "bridge:sendCastSessionMessage": {
const { namespace, sessionId, messageId } = message.data;
const session = sessions.get(sessionId);
if (!session) {
sendMessage({
subject: "shim:impl_sendCastMessage"
, data: {
error: "Session does not exist"
, sessionId, messageId
}
});
break;
}
try {
// Handle string messages
let { messageData } = message.data;
if (typeof messageData === "string") {
messageData = JSON.parse(messageData);
}
session.sendMessage(namespace, messageData);
} catch (err) {
sendMessage({
subject: "shim:impl_sendCastMessage"
, data: {
error: `Failed to send message (${err})`
, sessionId, messageId
}
});
break;
}
// Success
sendMessage({
subject: "shim:impl_sendCastMessage"
, data: { sessionId, messageId }
});
break;
}
case "bridge:stopCastApp": {
break;
}
}
}

View File

@@ -2,8 +2,29 @@
export interface Image {
url: string;
height?: number;
width?: number;
height: Nullable<number>;
width: Nullable<number>;
}
enum Capability {
VIDEO_OUT = "video_out"
, AUDIO_OUT = "audio_out"
, VIDEO_IN = "video_in"
, AUDIO_IN = "audio_in"
, MULTIZONE_GROUP = "multizone_group"
}
enum ReceiverType {
CAST = "cast"
, DIAL = "dial"
, HANGOUT = "hangout"
, CUSTOM = "custom"
}
export interface SenderApplication {
packageId: Nullable<string>;
platform: string;
url: Nullable<string>;
}
enum VolumeControlType {
@@ -257,13 +278,13 @@ interface QueueItem {
startTime: number;
}
export interface MediaStatus {
mediaSessionId: number;
media?: MediaInformation;
playbackRate: number;
playerState: PlayerState;
idleReason?: IdleReason;
items?: QueueItem[]
currentTime: number;
supportedMediaCommands: number;
repeatMode: RepeatMode;
@@ -271,25 +292,41 @@ export interface MediaStatus {
customData: unknown;
}
interface ReceiverDisplayStatus {
showStop: Nullable<boolean>;
statusText: string;
appImages: Image[];
}
export interface Receiver {
displayStatus: Nullable<ReceiverDisplayStatus>;
isActiveInput: Nullable<boolean>;
receiverType: ReceiverType;
label: string;
friendlyName: string;
capabilities: Capability[];
volume: Nullable<Volume>;
}
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
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
applications?: ReceiverApplication[];
isActiveInput?: boolean;
isStandBy?: boolean;
volume: Volume;
}
@@ -306,13 +343,12 @@ export type SenderMessage =
| ReqBase & { type: "SET_VOLUME", volume: Volume };
export type ReceiverMessage =
ReqBase & {
type: "RECEIVER_STATUS"
, status: ReceiverStatus
};
ReqBase & { type: "RECEIVER_STATUS", status: ReceiverStatus }
| ReqBase & { type: "LAUNCH_ERROR", reason: string }
interface MediaReqBase extends ReqBase {
mediaSessionId: number;
customData?: unknown;
}
@@ -324,11 +360,16 @@ export type SenderMediaMessage =
| MediaReqBase & { type: "STOP" }
| MediaReqBase & { type: "MEDIA_SET_VOLUME", volume: Volume }
| MediaReqBase & { type: "SET_PLAYBACK_RATE" , playbackRate: number }
| MediaReqBase & {
| ReqBase & {
type: "LOAD"
, media: MediaInformation
, activeTrackIds: Nullable<number[]>
, atvCredentials?: string
, atvCredentialsType?: string
, autoplay: Nullable<boolean>
, currentTime: Nullable<number>
, customData?: unknown
, media: MediaInformation
, sessionId: Nullable<string>
}
| MediaReqBase & {
type: "SEEK"
@@ -366,7 +407,7 @@ export type SenderMediaMessage =
type: "QUEUE_UPDATE"
, jump: Nullable<number>
, currentItemId: Nullable<number>
, sessionId: Nullable<number>
, sessionId: Nullable<string>
}
// QueueRemoveItemsRequest
| MediaReqBase & {

View File

@@ -1,78 +0,0 @@
"use strict";
import castv2 from "castv2";
import { ReceiverMediaMessage } from "./types";
import { Message } from "../../messaging";
import { sendMessage } from "../../lib/nativeMessaging";
import Session from "./Session";
const NS_MEDIA = "urn:x-cast:com.google.cast.media";
export default class Media {
private channel: castv2.Channel;
constructor(
private referenceId: string
, private session: Session) {
// Ensure channel exists
this.session.createChannel(NS_MEDIA);
const channel = this.session.channelMap.get(NS_MEDIA);
if (!channel) {
throw new Error("Media message cannel not found");
}
this.channel = channel;
this.channel.on("message", this.onMediaMessage);
}
private onMediaMessage = (message: ReceiverMediaMessage) => {
switch (message.type) {
case "MEDIA_STATUS": {
// TODO: Fix for multiple media statuses
const status = message.status[0];
this.sendMessage({
subject: "shim:media/updateStatus"
, data: { status }
});
break;
}
}
}
public messageHandler(message: Message) {
switch (message.subject) {
case "bridge:media/sendMediaMessage": {
let error = false;
try {
this.channel.send(message.data.message);
} catch (err) {
error = true;
}
this.sendMessage({
subject: "shim:media/sendMediaMessageResponse"
, data: {
messageId: message.data.messageId
, error
}
});
break;
}
}
}
private sendMessage(message: Message) {
(message.data as any)._id = this.referenceId;
sendMessage(message);
}
}

View File

@@ -1,255 +0,0 @@
"use strict";
import { Channel, Client } from "castv2";
import { Message } from "../../messaging";
import { sendMessage } from "../../lib/nativeMessaging";
import { ReceiverApplication, ReceiverMessage, SenderMessage } from "./types";
export const NS_CONNECTION = "urn:x-cast:com.google.cast.tp.connection";
export const NS_HEARTBEAT = "urn:x-cast:com.google.cast.tp.heartbeat";
export const NS_RECEIVER = "urn:x-cast:com.google.cast.receiver";
const HEARTBEAT_INTERVAL = 5000;
export default class Session {
private isSessionCreated = false;
private client: Client;
private clientId = `client-${Math.floor(Math.random() * 10e5)}`;
private transportId?: string;
public channelMap = new Map<string, Channel>();
private platformConnection?: Channel;
private platformHeartbeat?: Channel;
private platformReceiver?: Channel;
private platformHeartbeatIntervalId?: NodeJS.Timeout;
private transportConnection?: Channel;
private transportHeartbeat?: Channel;
private app?: ReceiverApplication;
constructor(
public host: string
, public port: number
, private appId: string
, private referenceId: string) {
const client = new Client();
client.on("error", err => {
console.error(`castv2 error: ${err}`);
});
client.on("close", () => {
// TODO: Don't send new data
if (this.platformHeartbeatIntervalId) {
clearInterval(this.platformHeartbeatIntervalId);
}
});
client.connect({ host, port }, this.onConnect.bind(this));
this.client = client;
}
public createChannel(namespace: string) {
if (!this.channelMap.has(namespace)) {
this.channelMap.set(namespace, this.client.createChannel(
this.clientId!, this.transportId!
, namespace, "JSON"));
}
}
private establishSession(app: ReceiverApplication) {
this.transportId = app.transportId;
// Mesage channel to app
this.transportConnection = this.client.createChannel(
this.clientId, this.transportId, NS_CONNECTION, "JSON");
this.transportHeartbeat = this.client.createChannel(
this.clientId, this.transportId, NS_HEARTBEAT, "JSON");
this.transportConnection.send({
type: "CONNECT"
});
}
private onConnect() {
const sourceId = "sender-0";
const destinationId = "receiver-0";
this.platformConnection = this.client.createChannel(
sourceId, destinationId, NS_CONNECTION, "JSON");
this.platformHeartbeat = this.client.createChannel(
sourceId, destinationId, NS_HEARTBEAT, "JSON");
this.platformReceiver = this.client.createChannel(
sourceId, destinationId, NS_RECEIVER, "JSON");
this.platformConnection.send({ type: "CONNECT" });
this.platformHeartbeat.send({ type: "PING" });
this.platformHeartbeatIntervalId = setInterval(() => {
this.platformHeartbeat?.send({ type: "PING" });
if (this.transportHeartbeat) {
this.transportHeartbeat.send({ type: "PING" });
}
}, HEARTBEAT_INTERVAL);
this.platformReceiver.send({
type: "LAUNCH"
, appId: this.appId
, requestId: 0
});
this.platformReceiver.on("message", (message: ReceiverMessage) => {
switch (message.type) {
case "RECEIVER_STATUS": {
const { status } = message;
if (status.applications) {
// TODO: Fix for multiple applications?
const app = status.applications[0];
if (app.appId !== this.appId) {
this.sendMessage({
subject: "shim:session/stopped"
});
this.client.close();
return;
}
if (!this.isSessionCreated) {
this.isSessionCreated = true;
this.establishSession(app);
}
}
this.sendMessage({
subject: "shim:session/updateStatus"
, data: { status: message.status }
});
break;
}
default: {
console.error(message);
}
}
});
}
public messageHandler(message: Message) {
switch (message.subject) {
case "bridge:session/close": {
this.close();
break;
}
case "bridge:session/impl_addMessageListener": {
this._impl_addMessageListener(message.data.namespace);
break;
}
case "bridge:session/impl_sendMessage": {
this._impl_sendMessage(
message.data.namespace
, message.data.message
, message.data.messageId);
break;
}
case "bridge:session/impl_sendReceiverMessage": {
const { message: receiverMessage
, messageId: receiverMessageId } = message.data;
this.impl_sendReceiverMessage(
receiverMessage, receiverMessageId);
break;
}
}
}
public close() {
this.platformConnection?.send({ type: "CLOSE" });
this.transportConnection?.send({ type: "CLOSE" });
}
public stop() {
this.platformConnection?.send({ type: "STOP" });
}
private sendMessage(message: Message) {
(message.data as any)._id = this.referenceId;
sendMessage(message);
}
private _impl_addMessageListener(namespace: string) {
// TODO: Limit to one listener per namespace
this.createChannel(namespace);
this.channelMap.get(namespace)?.on("message", (message: any) => {
this.sendMessage({
subject: "shim:session/impl_addMessageListener"
, data: {
namespace
, message: JSON.stringify(message)
}
});
});
}
private _impl_sendMessage(
namespace: string
, message: object | string
, messageId: string) {
let wasError = false;
try {
// Decode string messages
if (typeof message === "string") {
message = JSON.parse(message);
}
this.createChannel(namespace);
this.channelMap.get(namespace)?.send(message);
} catch (err) {
wasError = true;
}
this.sendMessage({
subject: "shim:session/impl_sendMessage"
, data: { messageId, wasError }
});
}
private impl_sendReceiverMessage(
message: SenderMessage
, messageId: string) {
let wasError = false;
try {
this.platformReceiver?.send(message);
} catch (err) {
wasError = true;
}
// Handle stop message
if (message.type === "STOP") {
this.client.close();
}
this.sendMessage({
subject: "shim:session/impl_sendReceiverMessage"
, data: { messageId, wasError }
});
}
}

View File

@@ -1,82 +0,0 @@
"use strict";
import castv2 from "castv2";
import Session, { NS_CONNECTION, NS_RECEIVER } from "./Session";
import Media from "./Media";
import { ReceiverDevice } from "../../types";
// Existing counterpart Media/Session objects
const existingSessions: Map<string, Session> = new Map();
const existingMedia: Map<string, Media> = new Map();
export function handleSessionMessage(message: any) {
if (!message.data._id) {
console.error("Session message missing _id");
return;
}
const sessionId = message.data._id;
if (existingSessions.has(sessionId)) {
// Forward message to instance message handler
existingSessions.get(sessionId)?.messageHandler(message);
} else {
if (message.subject === "bridge:session/initialize") {
existingSessions.set(sessionId, new Session(
message.data.address
, message.data.port
, message.data.appId
, sessionId));
}
}
}
export function handleMediaMessage(message: any) {
if (!message.data._id) {
console.error("Media message missing _id");
return;
}
const mediaId = message.data._id;
if (existingMedia.has(mediaId)) {
// Forward message to instance message handler
existingMedia.get(mediaId)?.messageHandler(message);
} else {
if (message.subject === "bridge:media/initialize") {
// Get Session object media belongs to
const parentSession = existingSessions.get(
message.data._internalSessionId);
if (parentSession) {
// Create Media
existingMedia.set(mediaId, new Media(
mediaId
, parentSession));
}
}
}
}
export function stopReceiverApp(host: string, port: number) {
const client = new castv2.Client();
client.connect({ host, port }, () => {
const sourceId = "sender-0";
const destinationId = "receiver-0";
const clientConnection = client.createChannel(
sourceId, destinationId, NS_CONNECTION, "JSON");
const clientReceiver = client.createChannel(
sourceId, destinationId, NS_RECEIVER, "JSON");
clientConnection.send({ type: "CONNECT" });
clientReceiver.send({ type: "STOP", requestId: 1 });
});
client.on("error", err => {
console.error(`castv2 error (stopReceiverApp): ${err}`);
});
}

View File

@@ -8,10 +8,9 @@ import mdns from "mdns";
import { sendMessage } from "../lib/nativeMessaging";
import { ReceiverStatus } from "./chromecast/types";
import { NS_CONNECTION
, NS_HEARTBEAT
, NS_RECEIVER } from "./chromecast/Session";
import { ReceiverStatus } from "./cast/types";
import { NS_CONNECTION, NS_HEARTBEAT, NS_RECEIVER }
from "./cast/Session";
interface CastTxtRecord {
@@ -152,8 +151,10 @@ export function stopDiscovery() {
* Closes status listener connection.
*/
public deregister(): void {
if (this.clientReceiver) {
this.clientReceiver.send({ type: "CLOSE" });
try {
this.clientReceiver?.send({ type: "CLOSE" });
} catch (err) {
// Supress
}
this.client.close();

View File

@@ -3,8 +3,7 @@
import { decodeTransform, encodeTransform } from "./lib/nativeMessaging";
import { Message } from "./messaging";
import { handleSessionMessage, handleMediaMessage, stopReceiverApp }
from "./components/chromecast";
import { handleCastMessage } from "./components/cast";
import { startDiscovery, stopDiscovery } from "./components/discovery";
import { startMediaServer, stopMediaServer } from "./components/mediaServer";
import { startReceiverSelector, stopReceiverSelector }
@@ -28,16 +27,6 @@ process.on("SIGTERM", () => {
* for managing existing ones.
*/
decodeTransform.on("data", (message: Message) => {
if (message.subject.startsWith("bridge:session/")) {
handleSessionMessage(message);
return;
}
if (message.subject.startsWith("bridge:media/")) {
handleMediaMessage(message);
return;
}
switch (message.subject) {
case "bridge:getInfo":
case "bridge:/getInfo": {
@@ -50,12 +39,6 @@ decodeTransform.on("data", (message: Message) => {
break;
}
case "bridge:stopReceiverApp": {
const { receiverDevice } = message.data;
stopReceiverApp(receiverDevice.host, receiverDevice.port);
break;
}
// Receiver selector
case "bridge:openReceiverSelector": {
startReceiverSelector(message.data); break;
@@ -74,5 +57,9 @@ decodeTransform.on("data", (message: Message) => {
stopMediaServer();
break;
}
default: {
handleCastMessage(message);
}
}
});

View File

@@ -1,84 +1,70 @@
"use strict";
import { MediaStatus, ReceiverStatus, ReceiverApplication, SenderMessage }
from "./components/chromecast/types";
import { Image
, ReceiverStatus
, SenderApplication
, SenderMessage
, Volume } from "./components/cast/types";
import { ReceiverDevice
, ReceiverSelectionCast
, ReceiverSelectionStop } from "./types";
interface CastSessionUpdated {
sessionId: string
, statusText: string
, namespaces: Array<{ name: string }>
, volume: Volume
}
interface CastSessionCreated extends CastSessionUpdated {
appId: string
, appImages: Image[]
, displayName: string
, receiverFriendlyName: string
, senderApps: SenderApplication[]
, transportId: string
}
type MessageDefinitions = {
// Session messages
"shim:session/connected": { application: ReceiverApplication }
, "shim:session/updateStatus": { status: ReceiverStatus }
, "shim:session/stopped": {}
, "shim:session/impl_addMessageListener": {
namespace: string
, message: string
}
, "shim:session/impl_sendMessage": {
messageId: string
, wasError: boolean
}
, "shim:session/impl_sendReceiverMessage": {
messageId: string
, wasError: boolean
}
// Bridge session messages
, "bridge:session/initialize": {
address: string
, port: number
, appId: string
, sessionId: string
, _id: string
}
, "bridge:session/close": {}
, "bridge:session/impl_leave": {
id: string
, _id: string
}
, "bridge:session/impl_sendMessage": {
namespace: string
, message: any
, messageId: string
, _id: string
}
, "bridge:session/impl_sendReceiverMessage": {
message: SenderMessage
, messageId: string
, _id: string
}
, "bridge:session/impl_addMessageListener": {
namespace: string;
_id: string;
}
// Media messages
, "shim:media/updateStatus": {
status: MediaStatus
}
, "shim:media/sendMediaMessageResponse": {
messageId: string
, error: boolean
}
// Bridge media messages
, "bridge:media/initialize": {
"shim:castSessionCreated": CastSessionCreated
, "shim:castSessionUpdated": CastSessionUpdated
, "shim:castSessionStopped": {
sessionId: string
, mediaSessionId: number
, _internalSessionId: string
, _id: string
}
, "bridge:media/sendMediaMessage": {
message: any
, "shim:receivedCastSessionMessage": {
sessionId: string
, namespace: string
, messageData: string
}
, "shim:impl_sendCastMessage": {
sessionId: string
, messageId: string
, _id: string
, error?: string
}
// Bridge messages
, "bridge:createCastSession": {
appId: string
, receiverDevice: ReceiverDevice
}
, "bridge:sendCastReceiverMessage": {
sessionId: string
, messageData: SenderMessage
, messageId: string
}
, "bridge:sendCastSessionMessage": {
sessionId: string
, namespace: string
, messageData: object | string
, messageId: string
}
, "bridge:stopCastApp": { receiverDevice: ReceiverDevice }
// Bridge messages
, "main:receiverSelector/selected": ReceiverSelectionCast
, "main:receiverSelector/stopped": ReceiverSelectionStop
, "main:receiverSelector/cancelled": {}
@@ -98,9 +84,6 @@ type MessageDefinitions = {
, "bridge:openReceiverSelector": string
, "bridge:closeReceiverSelector": {}
, "bridge:stopReceiverApp": { receiverDevice: ReceiverDevice }
, "bridge:startMediaServer": {
filePath: string
, port: number

View File

@@ -1,6 +1,6 @@
"use strict";
import { ReceiverStatus } from "./components/chromecast/types";
import { ReceiverStatus } from "./components/cast/types";
export enum ReceiverSelectorMediaType {

5
app/src/global.d.ts vendored
View File

@@ -1 +1,6 @@
declare type Nullable<T> = T | null;
declare type DistributiveOmit<T, K extends keyof any> =
T extends any
? Omit<T, K>
: never;

View File

@@ -41,7 +41,7 @@ export default new class ShimManager {
for (const shim of this.activeShims) {
shim.contentPort.postMessage({
subject: "shim:serviceUp"
, data: { id: ev.detail.receiverDevice.id }
, data: { receiverDevice: ev.detail.receiverDevice }
});
}
});
@@ -50,7 +50,7 @@ export default new class ShimManager {
for (const shim of this.activeShims) {
shim.contentPort.postMessage({
subject: "shim:serviceDown"
, data: { id: ev.detail.receiverDeviceId }
, data: { receiverDeviceId: ev.detail.receiverDeviceId }
});
}
});
@@ -173,7 +173,7 @@ export default new class ShimManager {
for (const receiverDevice of receiverDevices.getDevices()) {
shim.contentPort.postMessage({
subject: "shim:serviceUp"
, data: { id: receiverDevice.id }
, data: { receiverDevice }
});
}

View File

@@ -76,7 +76,7 @@ export default new class extends TypedEventTarget<EventMap> {
const receiverDevice = this.receiverDevices.get(receiverDeviceId);
if (receiverDevice) {
this.bridgePort.postMessage({
subject: "bridge:stopReceiverApp"
subject: "bridge:stopCastApp"
, data: { receiverDevice }
});
}

7
ext/src/global.d.ts vendored
View File

@@ -3,8 +3,15 @@ declare const MIRRORING_APP_ID: string;
declare const APPLICATION_NAME: string;
declare const APPLICATION_VERSION: string;
declare type Nullable<T> = T | null;
declare type DistributiveOmit<T, K extends keyof any> =
T extends any
? Omit<T, K>
: never;
declare interface Object {
// eslint-disable-next-line @typescript-eslint/ban-types
wrappedJSObject: Object;

View File

@@ -5,19 +5,18 @@ import Messenger from "./lib/Messenger";
import { TypedPort } from "./lib/TypedPort";
import { BridgeInfo } from "./lib/bridge";
import { ReceiverDevice } from "./types";
import { ReceiverSelectorMediaType } from "./background/receiverSelector";
import { ReceiverSelection
, ReceiverSelectionCast
, ReceiverSelectionStop }
from "./background/receiverSelector/ReceiverSelector";
import { Volume } from "./shim/cast/dataClasses";
import { MediaStatus
, SenderMessage
, ReceiverApplication
, ReceiverStatus } from "./shim/cast/types";
import { CastSessionCreated
, CastSessionUpdated
, ReceiverStatus
, SenderMessage } from "./shim/cast/types";
import { ReceiverDevice } from "./types";
/**
@@ -60,8 +59,8 @@ type ExtMessageDefinitions = {
, "main:sessionCreated": {}
, "shim:initialized": BridgeInfo
, "shim:serviceUp": { id: ReceiverDevice["id"] }
, "shim:serviceDown": { id: ReceiverDevice["id"] }
, "shim:serviceUp": { receiverDevice: ReceiverDevice }
, "shim:serviceDown": { receiverDeviceId: ReceiverDevice["id"] }
, "shim:launchApp": { receiver: ReceiverDevice }
}
@@ -72,74 +71,42 @@ type ExtMessageDefinitions = {
* app/bridge/messaging.ts > MessagesBase
*/
type AppMessageDefinitions = {
// Session messages
"shim:session/stopped": {}
, "shim:session/connected": { application: ReceiverApplication }
, "shim:session/updateStatus": { status: ReceiverStatus }
, "shim:session/impl_addMessageListener": {
namespace: string
, message: string
}
, "shim:session/impl_sendMessage": {
messageId: string
, wasError: boolean
}
, "shim:session/impl_sendReceiverMessage": {
messageId: string
, wasError: boolean
}
// Bridge session messages
, "bridge:session/initialize": {
address: string
, port: number
, appId: string
, sessionId: string
, _id: string
}
, "bridge:session/close": {}
, "bridge:session/impl_leave": {
id: string
, _id: string
}
, "bridge:session/impl_sendMessage": {
namespace: string
, message: any
, messageId: string
, _id: string
}
, "bridge:session/impl_sendReceiverMessage": {
message: SenderMessage
, messageId: string
, _id: string
}
, "bridge:session/impl_addMessageListener": {
namespace: string;
_id: string;
}
// Media messages
, "shim:media/updateStatus": {
status: MediaStatus
}
, "shim:media/sendMediaMessageResponse": {
messageId: string
, error: boolean
}
// Bridge media messages
, "bridge:media/initialize": {
"shim:castSessionCreated": CastSessionCreated
, "shim:castSessionUpdated": CastSessionUpdated
, "shim:castSessionStopped": {
sessionId: string
, mediaSessionId: number
, _internalSessionId: string
, _id: string
}
, "bridge:media/sendMediaMessage": {
message: any
, "shim:receivedCastSessionMessage": {
sessionId: string
, namespace: string
, messageData: string
}
, "shim:impl_sendCastMessage": {
sessionId: string
, messageId: string
, _id: string
, error?: string
}
, "bridge:createCastSession": {
appId: string
, receiverDevice: ReceiverDevice
}
, "bridge:sendCastReceiverMessage": {
sessionId: string
, messageData: SenderMessage
, messageId: string
}
, "bridge:sendCastSessionMessage": {
sessionId: string
, namespace: string
, messageData: object | string
, messageId: string
}
, "bridge:stopCastApp": { receiverDevice: ReceiverDevice }
// Bridge messages
, "main:receiverSelector/selected": ReceiverSelectionCast
, "main:receiverSelector/stopped": ReceiverSelectionStop
@@ -160,9 +127,6 @@ type AppMessageDefinitions = {
, "bridge:openReceiverSelector": string
, "bridge:closeReceiverSelector": {}
, "bridge:stopReceiverApp": { receiverDevice: ReceiverDevice }
, "bridge:startMediaServer": {
filePath: string
, port: number

View File

@@ -64,10 +64,10 @@ function getSession(opts: InitOptions): Promise<cast.Session> {
function receiverListener(availability: string) {
if (availability === cast.ReceiverAvailability.AVAILABLE) {
if (opts.receiver) {
cast._requestSession(
/*cast._requestSession(
opts.receiver
, onRequestSessionSuccess
, onRequestSessionError);
, onRequestSessionError);*/
} else {
cast.requestSession(
onRequestSessionSuccess

View File

@@ -162,10 +162,10 @@ function receiverListener(availability: string) {
if (availability === cast.ReceiverAvailability.AVAILABLE) {
wasSessionRequested = true;
cast._requestSession(
/*cast._requestSession(
selectedReceiver
, onRequestSessionSuccess
, onRequestSessionError);
, onRequestSessionError);*/
}
}

View File

@@ -4,8 +4,7 @@ import { v4 as uuid } from "uuid";
import logger from "../../lib/logger";
import { onMessage
, sendMessageResponse } from "../eventMessageChannel";
import { sendMessageResponse } from "../eventMessageChannel";
import { ErrorCallback
, LoadSuccessCallback
@@ -14,168 +13,158 @@ import { ErrorCallback
, SuccessCallback
, UpdateListener } from "../types";
import { SenderMediaMessage, SenderMessage } from "./types";
import { MediaStatus
, ReceiverMediaMessage
, SenderMediaMessage
, SenderMessage } from "./types";
import { Error as _Error
, Image, Receiver
, SenderApplication, Volume } from "./dataClasses";
import { ErrorCode, SessionStatus } from "./enums";
import { Media
, LoadRequest
, QueueLoadRequest } from "./media";
import { Image, Receiver, SenderApplication } from "./dataClasses";
import { SessionStatus } from "./enums";
import { Media, LoadRequest, QueueLoadRequest, QueueItem } from "./media";
type SenderMessageData<T = SenderMessage> =
T extends any
? Omit<T, "requestId">
: never;
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;
}
}
}
media.items = newItems;
}
}
type SessionSuccessCallback = (session: Session) => void;
export default class Session {
#id = uuid();
#isConnected = false;
#successCallback?: SessionSuccessCallback;
#messageListeners = new Map<string, Set<MessageListener>>();
#updateListeners = new Set<UpdateListener>();
#loadMediaSuccessCallback?: (media: Media) => void;
#loadMediaErrorCallback?: ErrorCallback;
#loadMediaRequest?: LoadRequest;
#sendMessageCallbacks =
_messageListeners = new Map<string, Set<MessageListener>>();
_updateListeners = new Set<UpdateListener>();
_sendMessageCallbacks =
new Map<string, [ SuccessCallback?, ErrorCallback? ]>();
#sendReceiverMessageCallbacks =
new Map<string, (wasError: boolean) => void>();
#listener = onMessage(message => {
// Filter other session messages
if ((message as any).data._id !== this.#id) {
return;
}
switch (message.subject) {
case "shim:session/stopped": {
// Disconnect from extension messages
this.#listener.disconnect();
this.status = SessionStatus.STOPPED;
for (const listener of this.#updateListeners) {
listener(false);
}
break;
}
case "shim:session/updateStatus": {
const { status } = message.data;
// First status message indicates session creation
if (!this.#isConnected && status.applications) {
this.#isConnected = true;
this.status = SessionStatus.CONNECTED;
// Update app props
const app = status.applications[0];
this.sessionId = app.sessionId;
this.namespaces = app.namespaces;
this.displayName = app.displayName;
this.statusText = app.statusText;
if (this.#successCallback) {
this.#successCallback(this);
}
return;
}
this.receiver.volume = status.volume;
for (const listener of this.#updateListeners) {
listener(true);
}
break;
}
case "shim:session/impl_addMessageListener": {
const { namespace, message: newMessage } = message.data;
const messageListeners = this.#messageListeners.get(namespace);
if (messageListeners) {
for (const listener of messageListeners) {
listener(namespace, newMessage);
}
}
break;
}
case "shim:session/impl_sendMessage": {
const { messageId, wasError } = message.data;
const [ successCallback, errorCallback ] =
this.#sendMessageCallbacks.get(messageId) ?? [];
if (wasError && errorCallback) {
errorCallback(new _Error(ErrorCode.SESSION_ERROR));
} else if (successCallback) {
successCallback();
}
this.#sendMessageCallbacks.delete(messageId);
break;
}
case "shim:session/impl_sendReceiverMessage": {
const { messageId, wasError } = message.data;
const callback =
this.#sendReceiverMessageCallbacks.get(messageId);
if (callback) {
callback(wasError);
}
break;
}
}
});
/**
* Sends a message to the bridge that is forwarded to the
* receiver device. Promise resolves once the message is sent
* or an error occurs.
*
*/
#sendReceiverMessage = (message: SenderMessageData) => {
const messageId = uuid();
sendMessageResponse({
subject: "bridge:session/impl_sendReceiverMessage"
, data: {
message: { requestId: 0, ...message }
, messageId
, _id: this.#id
}
});
#mediaMessageListener = (namespace: string, messageString: string) => {
if (namespace !== NS_MEDIA) return;
return new Promise<void>((resolve, reject) => {
this.#sendReceiverMessageCallbacks.set(messageId
, (wasError: boolean) => {
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);
if (wasError) {
reject(new _Error(ErrorCode.SESSION_ERROR));
return;
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);
resolve();
});
});
}
private _sendMediaMessage(message: SenderMediaMessage) {
this.sendMessage("urn:x-cast:com.google.cast.media", message);
#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[] = [];
@@ -187,51 +176,37 @@ export default class Session {
, public appId: string
, public displayName: string
, public appImages: Image[]
, public receiver: Receiver
, _successCallback: SessionSuccessCallback) {
, public receiver: Receiver) {
this.#successCallback = _successCallback;
this.transportId = sessionId || "";
if (receiver) {
sendMessageResponse({
subject: "bridge:session/initialize"
, data: {
address: (receiver as any)._address
, port: (receiver as any)._port
, appId
, sessionId
, _id: this.#id
}
});
}
this.addMessageListener(NS_MEDIA, this.#mediaMessageListener);
}
addMediaListener(_mediaListener: MediaListener) {
logger.info("STUB :: Session#addMediaListener");
}
removeMediaListener(_mediaListener: MediaListener): void {
logger.info("STUB :: Session#removeMediaListener");
}
addMessageListener(namespace: string
, listener: MessageListener) {
if (!this.#messageListeners.has(namespace)) {
this.#messageListeners.set(namespace, new Set());
addMessageListener(namespace: string, listener: MessageListener) {
if (!this._messageListeners.has(namespace)) {
this._messageListeners.set(namespace, new Set());
}
this.#messageListeners.get(namespace)?.add(listener);
sendMessageResponse({
subject: "bridge:session/impl_addMessageListener"
, data: {
namespace
, _id: this.#id
}
});
this._messageListeners.get(namespace)?.add(listener);
}
removeMessageListener(namespace: string, listener: MessageListener): void {
this._messageListeners.get(namespace)?.delete(listener);
}
addUpdateListener(listener: UpdateListener) {
this.#updateListeners.add(listener);
this._updateListeners.add(listener);
}
removeUpdateListener(listener: UpdateListener): void {
this._updateListeners.delete(listener);
}
leave(_successCallback?: SuccessCallback
@@ -244,48 +219,13 @@ export default class Session {
, successCallback?: LoadSuccessCallback
, errorCallback?: ErrorCallback): void {
this._sendMediaMessage(loadRequest);
this.#loadMediaSuccessCallback = successCallback;
this.#loadMediaErrorCallback = errorCallback;
this.#loadMediaRequest = loadRequest;
let hasResponded = false;
this.addMessageListener(
"urn:x-cast:com.google.cast.media"
, (_namespace, data) => {
if (hasResponded) {
return;
}
const message = JSON.parse(data);
if (message.status && message.status.length > 0) {
const sessionId = this.#id;
if (!sessionId) {
return;
}
hasResponded = true;
const media = new Media(
this.sessionId
, message.status[0].mediaSessionId
, sessionId);
media.media = loadRequest.media;
this.media = [ media ];
media.play();
if (successCallback) {
successCallback(media);
}
} else {
if (errorCallback) {
errorCallback(new _Error(ErrorCode.SESSION_ERROR));
}
}
});
loadRequest.sessionId = this.sessionId;
this.#sendMediaMessage(loadRequest)
.catch(errorCallback);
}
queueLoad(_queueLoadRequest: QueueLoadRequest
@@ -295,34 +235,24 @@ export default class Session {
logger.info("STUB :: Session#queueLoad");
}
removeMediaListener(_mediaListener: MediaListener): void {
logger.info("STUB :: Session#removeMediaListener");
}
removeMessageListener(namespace: string, listener: MessageListener): void {
this.#messageListeners.get(namespace)?.delete(listener);
}
removeUpdateListener(_namespace: string, listener: UpdateListener): void {
this.#updateListeners.delete(listener);
}
sendMessage(namespace: string
, message: {} | string
, message: object | string
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback): void {
const messageId = uuid();
sendMessageResponse({
subject: "bridge:session/impl_sendMessage"
subject: "bridge:sendCastSessionMessage"
, data: {
namespace
, message
sessionId: this.sessionId
, namespace
, messageData: message
, messageId
, _id: this.#id
}
});
this.#sendMessageCallbacks.set(messageId, [
this._sendMessageCallbacks.set(messageId, [
successCallback
, errorCallback
]);

View File

@@ -55,7 +55,7 @@ export class Image {
export class Receiver {
displayStatus: Nullable<ReceiverDisplayStatus> = null;
isActiveInput: Nullable<boolean> = null;
receiverType: string = ReceiverType.CAST;
receiverType = ReceiverType.CAST;
constructor(
public label: string

View File

@@ -3,60 +3,23 @@
import logger from "../../lib/logger";
import { ReceiverDevice } 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";
import { ApiConfig
, CredentialsData
, DialRequest
, Error as Error_
, Image as Image_
, Receiver as Receiver
, ReceiverDisplayStatus
, SenderApplication
, SessionRequest
, Timeout
, Volume } from "./dataClasses";
import { AutoJoinPolicy
, Capability
, DefaultActionPolicy
, DialAppState
, ErrorCode
, ReceiverAction
, ReceiverAvailability
, ReceiverType
, SenderPlatform
, SessionStatus
, VolumeControlType } from "./enums";
export * as media from "./media";
export {
// Enums
AutoJoinPolicy, Capability, DefaultActionPolicy, DialAppState, ErrorCode
, ReceiverAction, ReceiverAvailability, ReceiverType, SenderPlatform
, SessionStatus, VolumeControlType
// Classes
, ApiConfig, CredentialsData, DialRequest, ReceiverDisplayStatus
, SenderApplication, Session, SessionRequest, Timeout, Volume
, Error_ as Error
, Image_ as Image
, Receiver as Receiver
};
export let isAvailable = false;
export const timeout = new Timeout();
export const VERSION = [ 1, 2 ];
type ReceiverActionListener = (
receiver: Receiver
, receiverAction: string) => void;
receiver: Receiver
, receiverAction: string) => void;
type RequestSessionSuccessCallback = (session: Session) => void;
@@ -64,41 +27,48 @@ type SuccessCallback = () => void;
type ErrorCallback = (err: Error_) => void;
let apiConfig: ApiConfig;
let apiConfig: Nullable<ApiConfig>;
let sessionRequest: Nullable<SessionRequest>;
const receiverList: Array<{ id: string }> = [];
const sessionList: Session[] = [];
let requestSessionSuccessCallback: Nullable<
RequestSessionSuccessCallback>;
let requestSessionErrorCallback: Nullable<ErrorCallback>;
const receiverActionListeners = new Set<ReceiverActionListener>();
let sessionRequestInProgress = false;
let sessionSuccessCallback: RequestSessionSuccessCallback;
let sessionErrorCallback: ErrorCallback;
const receiverDevices = new Map<string, ReceiverDevice>();
const sessions = new Map<string, Session>();
export function addReceiverActionListener(
listener: ReceiverActionListener): void {
export { AutoJoinPolicy, Capability, DefaultActionPolicy, DialAppState
, ErrorCode, ReceiverAction, ReceiverAvailability, ReceiverType
, SenderPlatform, SessionStatus, VolumeControlType };
receiverActionListeners.add(listener);
}
export { ApiConfig, CredentialsData, DialRequest, Error_ as Error, Image
, Receiver, ReceiverDisplayStatus, SenderApplication, SessionRequest
, Timeout, Volume, Session };
export function initialize(
newApiConfig: ApiConfig
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback): void {
export const VERSION = [ 1, 2 ];
export let isAvailable = false;
export const timeout = new Timeout();
// chrome.cast.media namespace
export * as media from "./media";
export function initialize(newApiConfig: ApiConfig
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
logger.info("cast.initialize");
// Already initialized
if (apiConfig) {
if (errorCallback) {
errorCallback(new Error_(ErrorCode.INVALID_PARAMETER));
}
errorCallback?.(new Error_(ErrorCode.INVALID_PARAMETER));
return;
}
apiConfig = newApiConfig;
sendMessageResponse({
@@ -106,127 +76,61 @@ export function initialize(
, data: { appId: apiConfig.sessionRequest.appId }
});
if (successCallback) {
successCallback();
}
successCallback?.();
apiConfig.receiverListener(receiverList.length
apiConfig.receiverListener(receiverDevices.size
? ReceiverAvailability.AVAILABLE
: ReceiverAvailability.UNAVAILABLE);
}
export function logMessage(message: string): void {
// eslint-disable-next-line no-console
console.log("CAST MSG:", message);
}
export function precache(_data: string): void {
logger.info("STUB :: cast.precache");
}
export function removeReceiverActionListener(
listener: ReceiverActionListener): void {
receiverActionListeners.delete(listener);
}
export function requestSession(
successCallback: RequestSessionSuccessCallback
, errorCallback: ErrorCallback
, _sessionRequest: SessionRequest = apiConfig.sessionRequest): void {
export function requestSession(successCallback: RequestSessionSuccessCallback
, errorCallback: ErrorCallback
, newSessionRequest?: SessionRequest) {
logger.info("cast.requestSession");
// Called before initialization
// Not yet initialized
if (!apiConfig) {
if (errorCallback) {
errorCallback(new Error_(ErrorCode.API_NOT_INITIALIZED));
}
errorCallback?.(new Error_(ErrorCode.API_NOT_INITIALIZED));
return;
}
// Already requesting session
if (sessionRequestInProgress) {
if (errorCallback) {
errorCallback(new Error_(ErrorCode.INVALID_PARAMETER
, "Session request already in progress."));
}
if (sessionRequest) {
errorCallback?.(new Error_(
ErrorCode.INVALID_PARAMETER
, "Session request already in progress."));
return;
}
// No available receivers
if (!receiverList.length) {
if (errorCallback) {
errorCallback(new Error_(ErrorCode.RECEIVER_UNAVAILABLE));
}
// No receivers available
if (!receiverDevices.size) {
errorCallback?.(new Error_(ErrorCode.RECEIVER_UNAVAILABLE));
return;
}
sessionRequestInProgress = true;
/**
* Store session request for use in return message from
* receiver selection.
*/
sessionRequest = newSessionRequest ?? apiConfig.sessionRequest;
sessionSuccessCallback = successCallback;
sessionErrorCallback = errorCallback;
requestSessionSuccessCallback = successCallback;
requestSessionErrorCallback = errorCallback;
// Open destination chooser
// Open receiver selector UI
sendMessageResponse({
subject: "main:selectReceiver"
});
}
export function _requestSession(
receiver: ReceiverDevice
, successCallback?: RequestSessionSuccessCallback
, errorCallback?: ErrorCallback): void {
logger.info("cast._requestSession");
if (!apiConfig) {
if (errorCallback) {
errorCallback(new Error_(ErrorCode.API_NOT_INITIALIZED));
}
return;
}
if (sessionRequestInProgress) {
if (errorCallback) {
errorCallback(new Error_(ErrorCode.INVALID_PARAMETER
, "Session request already in progress."));
}
return;
}
if (!receiverList.length) {
if (errorCallback) {
errorCallback(new Error_(ErrorCode.RECEIVER_UNAVAILABLE));
}
return;
}
sessionRequestInProgress = true;
createSession(receiver).then(session => {
sessionRequestInProgress = false;
if (successCallback) {
successCallback(session);
}
});
}
export function requestSessionById(_sessionId: string): void {
logger.info("STUB :: cast.requestSessionById");
}
export function setCustomReceivers(
_receivers: Receiver[]
, _successCallback?: SuccessCallback
, _errorCallback?: ErrorCallback): void {
export function setCustomReceivers(_receivers: Receiver[]
, _successCallback?: SuccessCallback
, _errorCallback?: ErrorCallback): void {
logger.info("STUB :: cast.setCustomReceivers");
}
@@ -239,55 +143,26 @@ export function setReceiverDisplayStatus(_sessionId: string): void {
}
export function unescape(escaped: string): string {
return decodeURI(escaped);
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");
}
function createSession(receiver: ReceiverDevice): Promise<Session> {
const selectedReceiver = new Receiver(
receiver.id
, receiver.friendlyName);
(selectedReceiver as any)._address = receiver.host;
(selectedReceiver as any)._port = receiver.port;
async function createSessionObject(): Promise<Session> {
return new Promise((resolve, _reject) => {
const session = new Session(
sessionList.length.toString() // sessionId
, apiConfig.sessionRequest.appId // appId
, receiver.friendlyName // displayName
, [] // appImages
, selectedReceiver // receiver
, session => {
sendMessageResponse({
subject: "main:sessionCreated"
});
resolve(session);
});
});
}
// If an existing session is active, stop it and start new one
// TODO: Fix whatever broken behaviour this is
if (sessionList.length) {
const lastSession = sessionList[sessionList.length - 1];
if (lastSession.status !== SessionStatus.STOPPED) {
return new Promise((resolve, _reject) => {
lastSession.stop(() => {
resolve(createSessionObject());
});
});
}
}
return createSessionObject();
}
onMessage(async message => {
onMessage(message => {
switch (message.subject) {
case "shim:initialized": {
isAvailable = true;
@@ -295,37 +170,136 @@ onMessage(async message => {
}
/**
* Cast destination found (serviceUp). Set the API availability
* property and call the page event function (__onGCastApiAvailable).
* Once the bridge detects a session creation, session info
* and data needed to create cast API objects is sent.
*/
case "shim:serviceUp": {
const receiver = message.data;
case "shim:castSessionCreated": {
// Notify background to close UI
sendMessageResponse({
subject: "main:sessionCreated"
});
if (receiverList.find(r => r.id === receiver.id)) {
break;
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 "shim:castSessionUpdated": {
const status = message.data;
const session = sessions.get(status.sessionId);
if (!session) {
logger.error(`Session not found (${status.sessionId})`);
return;
}
receiverList.push(receiver);
session.statusText = status.statusText;
session.namespaces = status.namespaces;
session.receiver.volume = status.volume;
if (apiConfig) {
// Notify listeners of new cast destination
apiConfig.receiverListener(ReceiverAvailability.AVAILABLE);
if (requestSessionSuccessCallback) {
requestSessionSuccessCallback(session);
requestSessionSuccessCallback = null;
requestSessionErrorCallback = null;
}
break;
}
case "shim:castSessionStopped": {
const { sessionId } = message.data;
const session = sessions.get(sessionId);
if (session) {
session.status = SessionStatus.STOPPED;
for (const listener of session?._updateListeners) {
listener(false);
}
}
break;
}
case "shim:receivedCastSessionMessage": {
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 "shim:impl_sendCastMessage": {
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 "shim: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;
}
/**
* Cast destination lost (serviceDown). Remove from the receiver list
* and update availability state.
*/
case "shim:serviceDown": {
const receiverIndex = receiverList.findIndex(
receiver => receiver.id === message.data.id);
const { receiverDeviceId } = message.data;
receiverList.splice(receiverIndex, 1);
receiverDevices.delete(receiverDeviceId);
if (receiverList.length === 0) {
if (receiverDevices.size === 0) {
if (apiConfig) {
apiConfig.receiverListener(
ReceiverAvailability.UNAVAILABLE);
@@ -338,42 +312,44 @@ onMessage(async message => {
case "shim:selectReceiver/selected": {
logger.info("Selected receiver");
if (!sessionRequestInProgress) {
if (!sessionRequest) {
break;
}
const { receiver } = message.data;
const { receiver: receiverDevice } = message.data;
for (const listener of receiverActionListeners) {
logger.info("Calling receiver action listener", receiver);
const receiver = new Receiver(
receiverDevice.id
, receiverDevice.friendlyName);
const castReceiver = new Receiver(
receiver.id, receiver.friendlyName);
listener(castReceiver, ReceiverAction.CAST);
listener(receiver, ReceiverAction.CAST);
}
const session = await createSession(receiver);
sessionRequestInProgress = false;
if (sessionSuccessCallback) {
sessionSuccessCallback(session);
}
sendMessageResponse({
subject: "bridge:createCastSession"
, data: {
appId: sessionRequest.appId
, receiverDevice: receiverDevice
}
});
break;
}
case "shim:selectReceiver/stopped": {
const { receiver } = message.data;
logger.info("Stopped receiver");
if (sessionRequestInProgress) {
sessionRequestInProgress = false;
if (sessionRequest) {
sessionRequest = null;
for (const listener of receiverActionListeners) {
const castReceiver = new Receiver(
message.data.receiver.id
, message.data.receiver.friendlyName);
receiver.id
, receiver.friendlyName);
logger.info("Calling receiver action listener (STOP)"
, message.data.receiver);
listener(castReceiver, ReceiverAction.STOP);
}
}
@@ -385,25 +361,15 @@ onMessage(async message => {
* Popup closed before session established.
*/
case "shim:selectReceiver/cancelled": {
if (sessionRequestInProgress) {
sessionRequestInProgress = false;
if (sessionRequest) {
sessionRequest = null;
if (sessionErrorCallback) {
sessionErrorCallback(new Error_(ErrorCode.CANCEL));
}
requestSessionErrorCallback?.(
new Error_(ErrorCode.CANCEL));
}
break;
}
case "shim:launchApp": {
const receiver: ReceiverDevice = message.data.receiver;
_requestSession(receiver
, session => {
apiConfig.sessionListener(session);
});
break;
}
}
});

View File

@@ -15,8 +15,6 @@ import { BreakStatus, EditTracksInfoRequest, GetStatusRequest, LiveSeekableRange
import { PlayerState, RepeatMode } from "./enums";
import { ErrorCode } from "../enums";
import { onMessage, sendMessageResponse } from "../../eventMessageChannel";
import { ErrorCallback
, SuccessCallback
, UpdateListener } from "../../types";
@@ -25,114 +23,54 @@ import { SenderMediaMessage } from "../types";
export default class Media {
#id = uuid();
#isActive = true;
/**
* Timestamp of last status update
*/
#lastUpdateTime = 0;
#updateListeners = new Set<UpdateListener>();
#sendMediaMessageCallbacks =
new Map<string, [ SuccessCallback?, ErrorCallback? ]>();
#listener = onMessage(message => {
if ((message as any).data._id !== this.#id) {
return;
}
switch (message.subject) {
case "shim:media/updateStatus": {
const { status } = message.data;
// Store current update time
this.#lastUpdateTime = Date.now();
this.currentTime = status.currentTime;
this.mediaSessionId = status.mediaSessionId;
this.playbackRate = status.playbackRate;
this.playerState = status.playerState;
this.repeatMode = status.repeatMode;
this.volume = status.volume;
if (status.customData) {
this.customData = status.customData;
}
if (status.media) {
this.media = status.media as MediaInfo;
}
// Call update listeners
for (const listener of this.#updateListeners) {
listener(this.#isActive);
}
break;
}
case "shim:media/sendMediaMessageResponse": {
const { messageId, error } = message.data;
const [ successCallback, errorCallback ] =
this.#sendMediaMessageCallbacks
.get(messageId) ?? [];
if (error && errorCallback) {
errorCallback(new _Error(ErrorCode.SESSION_ERROR));
} else if (successCallback) {
successCallback();
}
break;
}
}
});
// Timestamp of last status update
_lastUpdateTime = 0;
_updateListeners = new Set<UpdateListener>();
activeTrackIds: Nullable<number[]> = null;
breakStatus?: BreakStatus;
currentItemId: Nullable<number> = null;
currentTime = 0;
customData: any = null;
idleReason: Nullable<string> = null;
items: Nullable<QueueItem[]> = null;
liveSeekableRange?: LiveSeekableRange;
loadingItemId: Nullable<number> = null;
media: Nullable<MediaInfo> = null;
playbackRate = 1;
playerState: string = PlayerState.IDLE;
preloadedItemId: Nullable<number> = null;
queueData?: QueueData;
repeatMode: string = RepeatMode.OFF;
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
, _internalSessionId: string) {
sendMessageResponse({
subject: "bridge:media/initialize"
, data: {
sessionId
, mediaSessionId
, _internalSessionId
, _id: this.#id
}
});
, public _sendMediaMessage: (message: DistributiveOmit<
SenderMediaMessage, "requestId">) => Promise<void>) {
}
addUpdateListener(listener: UpdateListener) {
this.#updateListeners.add(listener);
this._updateListeners.add(listener);
}
removeUpdateListener(listener: UpdateListener) {
this._updateListeners.delete(listener);
}
editTracksInfo(editTracksInfoRequest: EditTracksInfoRequest
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
this.#sendMediaMessage(
{ type: "EDIT_TRACKS_INFO", ...editTracksInfoRequest })
this._sendMediaMessage(
{ ...editTracksInfoRequest
, type: "EDIT_TRACKS_INFO"
, mediaSessionId: this.mediaSessionId })
.then(successCallback)
.catch(errorCallback);
}
@@ -153,9 +91,9 @@ export default class Media {
* rate.
*/
getEstimatedTime(): number {
if (this.playerState === PlayerState.PLAYING) {
if (this.playerState === PlayerState.PLAYING && this._lastUpdateTime) {
let estimatedTime = this.currentTime +
((Date.now() - this.#lastUpdateTime) / 1000);
(((Date.now() - this._lastUpdateTime) / 1000));
// Enforce valid range
if (estimatedTime < 0) {
@@ -179,8 +117,10 @@ export default class Media {
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
this.#sendMediaMessage(
{ type: "MEDIA_GET_STATUS", ...getStatusRequest })
this._sendMediaMessage(
{ ...getStatusRequest
, type: "MEDIA_GET_STATUS"
, mediaSessionId: this.mediaSessionId })
.then(successCallback)
.catch(errorCallback);
}
@@ -189,8 +129,10 @@ export default class Media {
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
this.#sendMediaMessage(
{ type: "PAUSE", ...pauseRequest })
this._sendMediaMessage(
{ ...pauseRequest
, type: "PAUSE"
, mediaSessionId: this.mediaSessionId })
.then(successCallback)
.catch(errorCallback);
}
@@ -199,8 +141,10 @@ export default class Media {
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
this.#sendMediaMessage(
{ type: "PLAY", ...playRequest })
this._sendMediaMessage(
{ ...playRequest
, type: "PLAY"
, mediaSessionId: this.mediaSessionId })
.then(successCallback)
.catch(errorCallback);
}
@@ -209,11 +153,11 @@ export default class Media {
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
this.#sendMediaMessage(
{
...new QueueInsertItemsRequest([ item ])
, type: "QUEUE_INSERT"
})
this._sendMediaMessage(
{ ...new QueueInsertItemsRequest([ item ])
, type: "QUEUE_INSERT"
, sessionId: this.sessionId
, mediaSessionId: this.mediaSessionId })
.then(successCallback)
.catch(errorCallback);
}
@@ -222,11 +166,11 @@ export default class Media {
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
this.#sendMediaMessage(
{
...queueInsertItemsRequest
, type: "QUEUE_INSERT"
})
this._sendMediaMessage(
{ ...queueInsertItemsRequest
, type: "QUEUE_INSERT"
, sessionId: this.sessionId
, mediaSessionId: this.mediaSessionId })
.then(successCallback)
.catch(errorCallback);
@@ -240,8 +184,11 @@ export default class Media {
const jumpRequest = new QueueJumpRequest();
jumpRequest.currentItemId = itemId;
this.#sendMediaMessage(
{ ...jumpRequest, type: "QUEUE_UPDATE" })
this._sendMediaMessage(
{ ...jumpRequest
, type: "QUEUE_UPDATE"
, sessionId: this.sessionId
, mediaSessionId: this.mediaSessionId })
.then(successCallback)
.catch(errorCallback);
}
@@ -280,8 +227,11 @@ export default class Media {
reorderItemsRequest.insertBefore = existingItem.itemId;
}
this.#sendMediaMessage(
{ ...reorderItemsRequest, type: "QUEUE_REORDER" })
this._sendMediaMessage(
{ ...reorderItemsRequest
, type: "QUEUE_REORDER"
, sessionId: this.sessionId
, mediaSessionId: this.mediaSessionId })
.then(successCallback)
.catch(errorCallback);
}
@@ -293,8 +243,11 @@ export default class Media {
const jumpRequest = new QueueJumpRequest();
jumpRequest.jump = 1;
this.#sendMediaMessage(
{ ...jumpRequest, type: "QUEUE_UPDATE" })
this._sendMediaMessage(
{ ...jumpRequest
, type: "QUEUE_UPDATE"
, sessionId: this.sessionId
, mediaSessionId: this.mediaSessionId })
.then(successCallback)
.catch(errorCallback);
}
@@ -305,8 +258,11 @@ export default class Media {
const jumpRequest = new QueueJumpRequest();
jumpRequest.jump = -1;
this.#sendMediaMessage(
{ ...jumpRequest, type: "QUEUE_UPDATE" })
this._sendMediaMessage(
{ ...jumpRequest
, type: "QUEUE_UPDATE"
, sessionId: this.sessionId
, mediaSessionId: this.mediaSessionId })
.then(successCallback)
.catch(errorCallback);
}
@@ -327,8 +283,12 @@ export default class Media {
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
this.#sendMediaMessage(
{ ...queueRemoveItemsRequest, type: "QUEUE_REMOVE" })
this._sendMediaMessage(
{ ...queueRemoveItemsRequest
, mediaSessionId: this.mediaSessionId
, type: "QUEUE_REMOVE"
, sessionId: this.sessionId })
.then(successCallback)
.catch(errorCallback);
}
@@ -337,8 +297,12 @@ export default class Media {
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
this.#sendMediaMessage(
{ ...queueReorderItemsRequest, type: "QUEUE_REORDER" })
this._sendMediaMessage(
{ ...queueReorderItemsRequest
, mediaSessionId: this.mediaSessionId
, type: "QUEUE_REORDER"
, sessionId: this.sessionId })
.then(successCallback)
.catch(errorCallback);
}
@@ -350,8 +314,11 @@ export default class Media {
const setPropertiesRequest = new QueueSetPropertiesRequest();
setPropertiesRequest.repeatMode = repeatMode;
this.#sendMediaMessage(
{ ...setPropertiesRequest, type: "QUEUE_UPDATE" })
this._sendMediaMessage(
{ ...setPropertiesRequest
, type: "QUEUE_UPDATE"
, sessionId: this.sessionId
, mediaSessionId: this.mediaSessionId })
.then(successCallback)
.catch(errorCallback);
}
@@ -360,22 +327,23 @@ export default class Media {
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
this.#sendMediaMessage(
{ ...queueUpdateItemsRequest, type: "QUEUE_UPDATE" })
this._sendMediaMessage(
{ ...queueUpdateItemsRequest
, type: "QUEUE_UPDATE"
, sessionId: this.sessionId
, mediaSessionId: this.mediaSessionId })
.then(successCallback)
.catch(errorCallback);
}
removeUpdateListener(listener: UpdateListener) {
this.#updateListeners.delete(listener);
}
seek(seekRequest: SeekRequest
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
this.#sendMediaMessage(
{ type: "SEEK", ...seekRequest })
this._sendMediaMessage(
{ ...seekRequest
, type: "SEEK"
, mediaSessionId: this.mediaSessionId })
.then(successCallback)
.catch(errorCallback);
}
@@ -384,8 +352,10 @@ export default class Media {
, successCallback?: SuccessCallback
, errorCallback?: ErrorCallback) {
this.#sendMediaMessage(
{ type: "MEDIA_SET_VOLUME", ...volumeRequest })
this._sendMediaMessage(
{ ...volumeRequest
, type: "MEDIA_SET_VOLUME"
, mediaSessionId: this.mediaSessionId })
.then(successCallback)
.catch(errorCallback);
}
@@ -398,13 +368,11 @@ export default class Media {
stopRequest = new StopRequest();
}
this.#sendMediaMessage({
type: "STOP"
, ...stopRequest
this._sendMediaMessage({
...stopRequest
, type: "STOP"
, mediaSessionId: this.mediaSessionId
}).then(() => {
this.#isActive = false;
this.#listener.disconnect();
if (successCallback) {
successCallback();
}
@@ -414,49 +382,4 @@ export default class Media {
supportsCommand(command: string): boolean {
return this.supportedMediaCommands.includes(command);
}
#sendMediaMessage = async (
// Allow messages without requestId
message: Omit<SenderMediaMessage, "requestId">
& { requestId?: Nullable<number> }) => {
if (!this.media) {
return;
}
// TODO: Handle this and other errors better
if (!this.#isActive) {
throw new _Error(ErrorCode.SESSION_ERROR
, "INVALID_MEDIA_SESSION_ID"
, {
type: "INVALID_REQUEST"
, reason: "INVALID_MEDIA_SESSION_ID"
});
return;
}
return new Promise<void>((resolve, reject) => {
const messageId = uuid();
this.#sendMediaMessageCallbacks.set(messageId, [
resolve, reject
]);
sendMessageResponse({
subject: "bridge:media/sendMediaMessage"
, data: {
message: {
// Default properties
requestId: 0
, mediaSessionId: this.mediaSessionId
, ...message
}
, messageId
, _id: this.#id
}
});
});
}
}

View File

@@ -22,7 +22,6 @@ export class AudiobookChapterMediaMetadata {
type = MetadataType.AUDIOBOOK_CHAPTER;
}
export class AudiobookContainerMetadata {
authors?: string[];
narrators?: string[];
@@ -30,7 +29,6 @@ export class AudiobookContainerMetadata {
releaseDate?: string;
}
export class Break {
duration?: number;
isEmbedded?: boolean;
@@ -42,7 +40,6 @@ export class Break {
, public position: number) {}
}
export class BreakClip {
clickThroughUrl?: string;
contentId?: string;
@@ -59,7 +56,6 @@ export class BreakClip {
constructor(public id: string) {}
}
export class BreakStatus {
breakClipId?: string;
breakId?: string;
@@ -68,7 +64,6 @@ export class BreakStatus {
whenSkippable?: number;
}
export class ContainerMetadata {
containerDuration?: number;
containerImages?: Image[];
@@ -80,7 +75,6 @@ export class ContainerMetadata {
ContainerType.GENERIC_CONTAINER) {}
}
export class EditTracksInfoRequest {
requestId = 0;
@@ -90,7 +84,6 @@ export class EditTracksInfoRequest {
}
}
export class GenericMediaMetadata {
images?: Image[];
metadataType = MetadataType.GENERIC;
@@ -101,12 +94,10 @@ export class GenericMediaMetadata {
type = MetadataType.GENERIC;
}
export class GetStatusRequest {
customData: any = null;
}
export class LiveSeekableRange {
constructor(
public start?: number
@@ -115,7 +106,6 @@ export class LiveSeekableRange {
, public isLiveDone?: boolean) {}
}
export class LoadRequest {
activeTrackIds: Nullable<number[]> = null;
atvCredentials?: string;
@@ -180,7 +170,6 @@ export class MediaMetadata {
}
}
export class MovieMediaMetadata {
images?: Image[];
metadataType = MetadataType.MOVIE;
@@ -192,7 +181,6 @@ export class MovieMediaMetadata {
type = MetadataType.MOVIE;
}
export class MusicTrackMediaMetadata {
albumArtist?: string;
albumName?: string;
@@ -210,12 +198,10 @@ export class MusicTrackMediaMetadata {
type = MetadataType.MUSIC_TRACK;
}
export class PauseRequest {
customData: any = null;
}
export class PhotoMediaMetadata {
artist?: string;
creationDateTime?: string;
@@ -230,12 +216,10 @@ export class PhotoMediaMetadata {
width?: number;
}
export class PlayRequest {
customData: any = null;
}
export class QueueData {
shuffle = false;
@@ -249,7 +233,6 @@ export class QueueData {
, public startTime?: number) {}
}
export class QueueInsertItemsRequest {
customData: any = null;
insertBefore: Nullable<number> = null;
@@ -261,7 +244,6 @@ export class QueueInsertItemsRequest {
public items: QueueItem[]) {}
}
export class QueueItem {
activeTrackIds: Nullable<number[]> = null;
autoplay = true;
@@ -277,70 +259,47 @@ export class QueueItem {
}
}
export class QueueJumpRequest {
type = "QUEUE_UPDATE";
jump: Nullable<number> = null;
currentItemId: Nullable<number> = null;
sessionId: Nullable<number> = null;
requestId: Nullable<number> = null;
type = "QUEUE_UPDATE";
}
export class QueueLoadRequest {
type = "QUEUE_LOAD";
customData: any = null;
repeatMode: string = RepeatMode.OFF;
requestId: Nullable<number> = null;
sessionId: Nullable<string> = null;
startIndex = 0;
type = "QUEUE_LOAD";
constructor(
public items: QueueItem[]) {}
constructor(public items: QueueItem[]) {}
}
export class QueueRemoveItemsRequest {
customData: any = null;
requestId: Nullable<number> = null;
sessionId: Nullable<string> = null;
type = "QUEUE_REMOVE";
customData: any = null;
constructor(
public itemIds: number[]) {}
constructor(public itemIds: number[]) {}
}
export class QueueReorderItemsRequest {
customData: any = null;
insertBefore: Nullable<number> = null;
requestId: Nullable<number> = null;
sessionId: Nullable<string> = null;
type = "QUEUE_REORDER";
constructor(
public itemIds: number[]) {}
constructor(public itemIds: number[]) {}
}
export class QueueSetPropertiesRequest {
type = "QUEUE_UPDATE";
customData: any = null;
repeatMode: Nullable<string> = null;
requestId: Nullable<number> = null;
sessionId: Nullable<string> = null;
type = "QUEUE_UPDATE";
}
export class QueueUpdateItemsRequest {
customData: any = null;
requestId: Nullable<number> = null;
sessionId: Nullable<string> = null;
type = "QUEUE_UPDATE";
customData: any = null;
constructor(
public items: QueueItem[]) {}
constructor(public items: QueueItem[]) {}
}
@@ -350,12 +309,10 @@ export class SeekRequest {
resumeState: Nullable<ResumeState> = null;
}
export class StopRequest {
customData: any = null;
}
export class TextTrackStyle {
backgroundColor: Nullable<string> = null;
customData: any = null;
@@ -371,7 +328,6 @@ export class TextTrackStyle {
windowType: Nullable<string> = null;
}
export class Track {
customData: any = null;
language: Nullable<string> = null;
@@ -385,7 +341,6 @@ export class Track {
, public type: TrackType) {}
}
export class TvShowMediaMetadata {
episode?: number;
episodeNumber?: number;
@@ -401,7 +356,6 @@ export class TvShowMediaMetadata {
type = MetadataType.TV_SHOW;
}
export class UserActionState {
customData: any = null;
@@ -409,13 +363,11 @@ export class UserActionState {
public userAction: UserAction) {}
}
export class VastAdsRequest {
adsResponse?: string;
adTagUrl?: string;
}
export class VideoInformation {
constructor(
public width: number
@@ -423,7 +375,6 @@ export class VideoInformation {
, public hdrType: HdrType) {}
}
export class VolumeRequest {
customData: any = null;

View File

@@ -5,7 +5,7 @@
* app/src/bridge/components/chromecast/types.ts
*/
import { Volume } from "./dataClasses";
import { SenderApplication, Volume, Image } from "./dataClasses";
import { MediaInfo, QueueItem } from "./media/dataClasses";
import { IdleReason
, PlayerState
@@ -19,6 +19,7 @@ export interface MediaStatus {
playbackRate: number;
playerState: PlayerState;
idleReason?: IdleReason;
items?: QueueItem[];
currentTime: number;
supportedMediaCommands: number;
repeatMode: RepeatMode;
@@ -48,6 +49,23 @@ export interface ReceiverStatus {
}
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;
}
@@ -61,13 +79,12 @@ export type SenderMessage =
| ReqBase & { type: "SET_VOLUME", volume: Partial<Volume> };
export type ReceiverMessage =
ReqBase & {
type: "RECEIVER_STATUS"
, status: ReceiverStatus
};
ReqBase & { type: "RECEIVER_STATUS", status: ReceiverStatus }
| ReqBase & { type: "LAUNCH_ERROR", reason: string }
interface MediaReqBase extends ReqBase {
mediaSessionId: number;
customData?: unknown;
}
@@ -79,16 +96,15 @@ export type SenderMediaMessage =
| MediaReqBase & { type: "STOP" }
| MediaReqBase & { type: "MEDIA_SET_VOLUME", volume: Partial<Volume> }
| MediaReqBase & { type: "SET_PLAYBACK_RATE" , playbackRate: number }
| MediaReqBase & {
| ReqBase & {
type: "LOAD"
, activeTrackIds: Nullable<number[]>
, atvCredentials?: string
, atvCredentialsType?: string
, autoplay: Nullable<boolean>
, currentTime: Nullable<number>
, customData: any
, customData?: unknown
, media: MediaInfo
, requestId: number
, sessionId: Nullable<string>
}
| MediaReqBase & {
@@ -127,7 +143,7 @@ export type SenderMediaMessage =
type: "QUEUE_UPDATE"
, jump: Nullable<number>
, currentItemId: Nullable<number>
, sessionId: Nullable<number>
, sessionId: Nullable<string>
}
// QueueRemoveItemsRequest
| MediaReqBase & {

View File

@@ -14,9 +14,6 @@ if (!_window.chrome) {
}
// Remove private APIs
delete (cast as any)._requestSession;
// Create page-accessible API object
_window.chrome.cast = cast;