Cast API overhaul (#173)

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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