Files
fx_cast/app/src/bridge/components/cast/client.ts
2022-09-02 06:30:19 +01:00

116 lines
3.5 KiB
TypeScript

import { Channel, Client } from "castv2";
import type { 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 DEFAULT_PORT = 8009;
const HEARTBEAT_INTERVAL_MS = 5000;
interface CastClientConnectOptions {
port?: number;
onReceiverMessage?: (message: ReceiverMessage) => void;
onHeartbeat?: () => void;
}
export default class CastClient {
protected client = new Client();
protected connectionChannel?: Channel;
protected heartbeatChannel?: Channel;
protected heartbeatIntervalId?: NodeJS.Timeout;
// Platform messaging
private receiverChannel?: Channel;
private receiverRequestId = Math.floor(Math.random() * 1e6);
constructor(
protected sourceId = "sender-0",
protected destinationId = "receiver-0"
) {}
/**
* Create a channel on the client connection with a given
* namespace.
*/
protected createChannel(
namespace: string,
sourceId = this.sourceId,
destinationId = this.destinationId
) {
return this.client.createChannel(
sourceId,
destinationId,
namespace,
"JSON"
);
}
/**
* Sends a message on the receiver channel with the correct
* request ID.
*/
sendReceiverMessage(message: DistributiveOmit<SenderMessage, "requestId">) {
if (!this.receiverChannel) return;
const requestId = this.receiverRequestId++;
this.receiverChannel.send({ ...message, requestId });
return requestId;
}
/**
* Connects to a cast receiver at a given host, returning a
* promise that resolves once the client is connected.
*/
connect(host: string, options?: CastClientConnectOptions) {
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: options?.port ?? DEFAULT_PORT
},
// On connection callback
() => {
this.connectionChannel = this.createChannel(NS_CONNECTION);
this.heartbeatChannel = this.createChannel(NS_HEARTBEAT);
// Handle receiver messages
this.receiverChannel = this.createChannel(NS_RECEIVER);
this.receiverChannel.on("message", message => {
options?.onReceiverMessage?.(message);
});
this.connectionChannel.send({ type: "CONNECT" });
this.heartbeatChannel.send({ type: "PING" });
this.heartbeatIntervalId = setInterval(() => {
this.heartbeatChannel?.send({ type: "PING" });
options?.onHeartbeat?.();
}, HEARTBEAT_INTERVAL_MS);
resolve();
}
);
});
}
disconnect() {
if (this.heartbeatIntervalId) {
clearInterval(this.heartbeatIntervalId);
}
this.connectionChannel?.send({ type: "CLOSE" });
this.client.close();
}
}