mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-08 16:49:58 +00:00
272 lines
8.4 KiB
TypeScript
272 lines
8.4 KiB
TypeScript
import bridge, { BridgeInfo } from "../lib/bridge";
|
|
import logger from "../lib/logger";
|
|
import { TypedEventTarget } from "../lib/TypedEventTarget";
|
|
|
|
import type { Message, Port } from "../messaging";
|
|
import type { ReceiverDevice } from "../types";
|
|
|
|
import type {
|
|
MediaStatus,
|
|
ReceiverStatus,
|
|
SenderMediaMessage,
|
|
SenderMessage
|
|
} from "../cast/sdk/types";
|
|
import { PlayerState } from "../cast/sdk/media/enums";
|
|
|
|
import { ActionState, updateActionState } from "./action";
|
|
|
|
interface EventMap {
|
|
deviceUp: { deviceInfo: ReceiverDevice };
|
|
deviceDown: { deviceId: string };
|
|
deviceUpdated: { deviceId: string; status: ReceiverStatus };
|
|
deviceMediaUpdated: { deviceId: string; status: MediaStatus };
|
|
|
|
applicationFound: { deviceId: string; appId: string };
|
|
applicationClosed: { deviceId: string; appId: string; sessionId: string };
|
|
}
|
|
|
|
export default new (class extends TypedEventTarget<EventMap> {
|
|
/**
|
|
* Map of receiver device IDs to devices. Updated as receiverDevice
|
|
* messages are received from the bridge.
|
|
*/
|
|
private receiverDevices = new Map<string, ReceiverDevice>();
|
|
|
|
private bridgePort?: Port;
|
|
private bridgeInfo?: BridgeInfo;
|
|
async init() {
|
|
if (!this.bridgePort) {
|
|
await this.refresh();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initializes (or re-initializes) a bridge connection to start
|
|
* dispatching events.
|
|
*/
|
|
async refresh() {
|
|
this.bridgePort?.disconnect();
|
|
|
|
try {
|
|
this.bridgeInfo = await bridge.getInfo();
|
|
// eslint-disable-next-line no-empty
|
|
} catch {}
|
|
|
|
if (this.bridgeInfo?.isVersionCompatible) {
|
|
this.bridgePort = await bridge.connect();
|
|
this.bridgePort.onMessage.addListener(this.onBridgeMessage);
|
|
this.bridgePort.onDisconnect.addListener(this.onBridgeDisconnect);
|
|
|
|
this.bridgePort.postMessage({
|
|
subject: "bridge:startDiscovery",
|
|
data: {
|
|
// Also send back status messages
|
|
shouldWatchStatus: true
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
getBridgeInfo() {
|
|
return this.bridgeInfo;
|
|
}
|
|
|
|
/** Gets a list of receiver devices. */
|
|
getDevices() {
|
|
return Array.from(this.receiverDevices.values());
|
|
}
|
|
/** Gets a device by ID. */
|
|
getDeviceById(deviceId: string) {
|
|
return this.receiverDevices.get(deviceId);
|
|
}
|
|
|
|
/** Sends an NS_RECEIVER message to a given device. */
|
|
sendReceiverMessage(deviceId: string, message: SenderMessage) {
|
|
if (!this.bridgePort) {
|
|
logger.error(
|
|
"Failed to send receiver message (no bridge connection)"
|
|
);
|
|
return;
|
|
}
|
|
|
|
const device = this.receiverDevices.get(deviceId);
|
|
if (!device) {
|
|
logger.error(
|
|
"Failed to send receiver message (could not find device)"
|
|
);
|
|
return;
|
|
}
|
|
|
|
this.bridgePort?.postMessage({
|
|
subject: "bridge:sendReceiverMessage",
|
|
data: { deviceId, message }
|
|
});
|
|
}
|
|
|
|
/** Sends an NS_MEDIA message to a given device. */
|
|
sendMediaMessage(deviceId: string, message: SenderMediaMessage) {
|
|
if (!this.bridgePort) {
|
|
logger.error("Failed to send media message (no bridge connection)");
|
|
return;
|
|
}
|
|
|
|
const device = this.receiverDevices.get(deviceId);
|
|
if (!device) {
|
|
logger.error(
|
|
"Failed to send media message (could not find device)"
|
|
);
|
|
return;
|
|
}
|
|
|
|
this.bridgePort?.postMessage({
|
|
subject: "bridge:sendMediaMessage",
|
|
data: { deviceId, message }
|
|
});
|
|
}
|
|
|
|
private onBridgeMessage = (message: Message) => {
|
|
switch (message.subject) {
|
|
case "main:deviceUp": {
|
|
const { deviceId, deviceInfo } = message.data;
|
|
|
|
this.receiverDevices.set(deviceId, deviceInfo);
|
|
|
|
// Sort devices by friendly name
|
|
this.receiverDevices = new Map(
|
|
[...this.receiverDevices].sort(([, deviceA], [, deviceB]) =>
|
|
deviceA.friendlyName.localeCompare(deviceB.friendlyName)
|
|
)
|
|
);
|
|
|
|
this.dispatchEvent(
|
|
new CustomEvent("deviceUp", {
|
|
detail: { deviceInfo }
|
|
})
|
|
);
|
|
|
|
break;
|
|
}
|
|
|
|
case "main:deviceDown": {
|
|
const { deviceId } = message.data;
|
|
|
|
if (this.receiverDevices.has(deviceId)) {
|
|
this.receiverDevices.delete(deviceId);
|
|
}
|
|
this.dispatchEvent(
|
|
new CustomEvent("deviceDown", {
|
|
detail: { deviceId: deviceId }
|
|
})
|
|
);
|
|
|
|
break;
|
|
}
|
|
|
|
case "main:receiverDeviceStatusUpdated": {
|
|
const { deviceId, status } = message.data;
|
|
const device = this.receiverDevices.get(deviceId);
|
|
if (!device) break;
|
|
|
|
const oldApplication = device.status?.applications?.[0];
|
|
|
|
// Clear media status when app status changes
|
|
const application = status.applications?.[0];
|
|
if (!application || application.isIdleScreen) {
|
|
delete device.mediaStatus;
|
|
|
|
// Send application closed event
|
|
if (oldApplication && !oldApplication.isIdleScreen) {
|
|
this.dispatchEvent(
|
|
new CustomEvent("applicationClosed", {
|
|
detail: {
|
|
deviceId,
|
|
appId: oldApplication.appId,
|
|
sessionId: oldApplication.transportId
|
|
}
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
device.status = status;
|
|
|
|
this.dispatchEvent(
|
|
new CustomEvent("deviceUpdated", {
|
|
detail: {
|
|
deviceId,
|
|
status: device.status
|
|
}
|
|
})
|
|
);
|
|
|
|
// Send new application found event
|
|
if (
|
|
!oldApplication &&
|
|
application &&
|
|
!application.isIdleScreen
|
|
) {
|
|
this.dispatchEvent(
|
|
new CustomEvent("applicationFound", {
|
|
detail: { deviceId, appId: application.appId }
|
|
})
|
|
);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case "main:receiverDeviceMediaStatusUpdated": {
|
|
const { deviceId, status } = message.data;
|
|
const device = this.receiverDevices.get(deviceId);
|
|
if (!device) break;
|
|
|
|
if (device.mediaStatus) {
|
|
device.mediaStatus = { ...device.mediaStatus, ...status };
|
|
if (status.playerState === PlayerState.IDLE) {
|
|
delete device.mediaStatus.media;
|
|
}
|
|
} else {
|
|
device.mediaStatus = status;
|
|
}
|
|
|
|
this.dispatchEvent(
|
|
new CustomEvent("deviceMediaUpdated", {
|
|
detail: {
|
|
deviceId,
|
|
status: device.mediaStatus
|
|
}
|
|
})
|
|
);
|
|
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
private onBridgeDisconnect = () => {
|
|
const deviceIds = [...this.receiverDevices.keys()];
|
|
|
|
delete this.bridgeInfo;
|
|
this.receiverDevices.clear();
|
|
|
|
// Notify listeners of device availablility
|
|
for (const deviceId of deviceIds) {
|
|
const event = new CustomEvent("deviceDown", {
|
|
detail: { deviceId }
|
|
});
|
|
|
|
this.dispatchEvent(event);
|
|
}
|
|
|
|
/**
|
|
* Reconnect 10 seconds after disconnect if not already
|
|
* reconnected (like immediately after a refresh).
|
|
*/
|
|
window.setTimeout(() => {
|
|
if (!this.bridgeInfo) {
|
|
this.refresh();
|
|
}
|
|
}, 10000);
|
|
};
|
|
})();
|