mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-08 08:39:59 +00:00
Cast API overhaul (#173)
Re-write the shim<->bridge messaging interface and session creation/update handling for better accuracy.
This commit is contained in:
@@ -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_" ]
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
260
app/src/bridge/components/cast/Session.ts
Normal file
260
app/src/bridge/components/cast/Session.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
114
app/src/bridge/components/cast/index.ts
Normal file
114
app/src/bridge/components/cast/index.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 & {
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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}`);
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
5
app/src/global.d.ts
vendored
@@ -1 +1,6 @@
|
||||
declare type Nullable<T> = T | null;
|
||||
|
||||
declare type DistributiveOmit<T, K extends keyof any> =
|
||||
T extends any
|
||||
? Omit<T, K>
|
||||
: never;
|
||||
|
||||
@@ -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 }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
7
ext/src/global.d.ts
vendored
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -162,10 +162,10 @@ function receiverListener(availability: string) {
|
||||
|
||||
if (availability === cast.ReceiverAvailability.AVAILABLE) {
|
||||
wasSessionRequested = true;
|
||||
cast._requestSession(
|
||||
/*cast._requestSession(
|
||||
selectedReceiver
|
||||
, onRequestSessionSuccess
|
||||
, onRequestSessionError);
|
||||
, onRequestSessionError);*/
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
]);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 & {
|
||||
|
||||
@@ -14,9 +14,6 @@ if (!_window.chrome) {
|
||||
}
|
||||
|
||||
|
||||
// Remove private APIs
|
||||
delete (cast as any)._requestSession;
|
||||
|
||||
// Create page-accessible API object
|
||||
_window.chrome.cast = cast;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user