From b672b8d7223e14be74ef65f3535bb5cfec403052 Mon Sep 17 00:00:00 2001 From: hensm Date: Sun, 17 Apr 2022 07:49:01 +0100 Subject: [PATCH] Add better support for handling device capabilities and receiver objects --- app/src/bridge/components/cast/remote.ts | 16 ++-- app/src/bridge/components/discovery.ts | 13 ++-- app/src/bridge/types.ts | 15 +++- ext/src/background/menus.ts | 2 +- ext/src/background/receiverDevices.ts | 14 +++- .../receiverSelector/ReceiverSelector.ts | 18 ++--- .../ReceiverSelectorManager.ts | 6 +- ext/src/background/receiverSelector/index.ts | 4 +- ext/src/cast/api/index.ts | 75 +++++++++++++++---- ext/src/lib/loadSender.ts | 6 +- ext/src/messaging.ts | 4 +- ext/src/types.ts | 15 +++- ext/src/ui/popup/index.tsx | 48 ++++++------ 13 files changed, 159 insertions(+), 77 deletions(-) diff --git a/app/src/bridge/components/cast/remote.ts b/app/src/bridge/components/cast/remote.ts index bf35860..d50bbe1 100644 --- a/app/src/bridge/components/cast/remote.ts +++ b/app/src/bridge/components/cast/remote.ts @@ -27,13 +27,15 @@ export default class Remote extends CastClient { constructor(private host: string, private options?: CastRemoteOptions) { super(); - super.connect(host, { - onReceiverMessage: message => { - this.onReceiverMessage(message); - } - }).then(() => { - this.sendReceiverMessage({ type: "GET_STATUS" }); - }); + super + .connect(host, { + onReceiverMessage: message => { + this.onReceiverMessage(message); + } + }) + .then(() => { + this.sendReceiverMessage({ type: "GET_STATUS" }); + }); } disconnect() { diff --git a/app/src/bridge/components/discovery.ts b/app/src/bridge/components/discovery.ts index 4de2676..b69c274 100644 --- a/app/src/bridge/components/discovery.ts +++ b/app/src/bridge/components/discovery.ts @@ -16,6 +16,8 @@ interface CastRecord { md: string; // Friendly name (user-visible) fn: string; + // Capabilities + ca: string; // Version (?) ve: string; // Icon path (?) @@ -23,7 +25,6 @@ interface CastRecord { cd: string; rm: string; - ca: string; st: string; bs: string; nf: string; @@ -71,16 +72,18 @@ browser.on("serviceUp", service => { const record = service.txtRecord as CastRecord; const device: ReceiverDevice = { + id: record.id, + friendlyName: record.fn, + modelName: record.md, + capabilities: parseInt(record.ca), host: service.addresses[0], - port: service.port, - id: service.name, - friendlyName: record.fn + port: service.port }; sendMessage({ subject: "main:receiverDeviceUp", data: { - deviceId: service.name, + deviceId: device.id, deviceInfo: device } }); diff --git a/app/src/bridge/types.ts b/app/src/bridge/types.ts index 188e240..3d44871 100644 --- a/app/src/bridge/types.ts +++ b/app/src/bridge/types.ts @@ -7,10 +7,21 @@ import { Volume } from "./components/cast/types"; +export enum ReceiverDeviceCapabilities { + NONE = 0, + VIDEO_OUT = 1, + VIDEO_IN = 2, + AUDIO_OUT = 4, + AUDIO_IN = 8, + MULTIZONE_GROUP = 32 +} + export interface ReceiverDevice { - host: string; - friendlyName: string; id: string; + friendlyName: string; + modelName: string; + capabilities: ReceiverDeviceCapabilities; + host: string; port: number; status?: ReceiverStatus; } diff --git a/ext/src/background/menus.ts b/ext/src/background/menus.ts index 0169627..5281818 100644 --- a/ext/src/background/menus.ts +++ b/ext/src/background/menus.ts @@ -137,7 +137,7 @@ browser.menus.onClicked.addListener(async (info, tab) => { if (selection.mediaType === ReceiverSelectorMediaType.App) { await browser.tabs.executeScript(tab.id, { code: stringify` - window.receiver = ${selection.receiver}; + window.receiver = ${selection.receiverDevice}; window.mediaUrl = ${info.srcUrl}; window.targetElementId = ${info.targetElementId}; `, diff --git a/ext/src/background/receiverDevices.ts b/ext/src/background/receiverDevices.ts index e5c1c6d..6182bde 100644 --- a/ext/src/background/receiverDevices.ts +++ b/ext/src/background/receiverDevices.ts @@ -5,7 +5,7 @@ import logger from "../lib/logger"; import { TypedEventTarget } from "../lib/TypedEventTarget"; import { Message, Port } from "../messaging"; -import { ReceiverDevice } from "../types"; +import { ReceiverDevice, ReceiverDeviceCapabilities } from "../types"; import { ReceiverStatus } from "../cast/api/types"; interface EventMap { @@ -84,6 +84,16 @@ export default new (class extends TypedEventTarget { case "main:receiverDeviceUp": { const { deviceId, deviceInfo } = message.data; + // TODO: Add proper support for Chromecast Audio devices + if ( + !( + deviceInfo.capabilities & + ReceiverDeviceCapabilities.VIDEO_OUT + ) + ) { + break; + } + this.receiverDevices.set(deviceId, deviceInfo); this.dispatchEvent( new CustomEvent("receiverDeviceUp", { @@ -112,9 +122,7 @@ export default new (class extends TypedEventTarget { case "main:receiverDeviceStatusUpdated": { const { deviceId, status } = message.data; const receiverDevice = this.receiverDevices.get(deviceId); - if (!receiverDevice) { - logger.error(`Receiver ID \`${deviceId}\` not found!`); break; } diff --git a/ext/src/background/receiverSelector/ReceiverSelector.ts b/ext/src/background/receiverSelector/ReceiverSelector.ts index 180ef44..22c33ba 100644 --- a/ext/src/background/receiverSelector/ReceiverSelector.ts +++ b/ext/src/background/receiverSelector/ReceiverSelector.ts @@ -35,7 +35,7 @@ export default class ReceiverSelector extends TypedEventTarget { logger.info("Stopping receiver app...", ev.detail); - receiverDevices.stopReceiverApp(ev.detail.receiver.id); + receiverDevices.stopReceiverApp(ev.detail.receiverDevice.id); resolve({ actionType: ReceiverSelectionActionType.Stop, - receiver: ev.detail.receiver + receiverDevice: ev.detail.receiverDevice }); removeListeners(); }) diff --git a/ext/src/background/receiverSelector/index.ts b/ext/src/background/receiverSelector/index.ts index 8103b4f..441377c 100644 --- a/ext/src/background/receiverSelector/index.ts +++ b/ext/src/background/receiverSelector/index.ts @@ -17,13 +17,13 @@ export enum ReceiverSelectionActionType { export interface ReceiverSelectionCast { actionType: ReceiverSelectionActionType.Cast; - receiver: ReceiverDevice; + receiverDevice: ReceiverDevice; mediaType: ReceiverSelectorMediaType; filePath?: string; } export interface ReceiverSelectionStop { actionType: ReceiverSelectionActionType.Stop; - receiver: ReceiverDevice; + receiverDevice: ReceiverDevice; } export type ReceiverSelection = ReceiverSelectionCast | ReceiverSelectionStop; diff --git a/ext/src/cast/api/index.ts b/ext/src/cast/api/index.ts index 9cae89b..6bade9d 100644 --- a/ext/src/cast/api/index.ts +++ b/ext/src/cast/api/index.ts @@ -2,7 +2,10 @@ import logger from "../../lib/logger"; -import { ReceiverDevice } from "../../types"; +import { + ReceiverDevice, + ReceiverDeviceCapabilities as ReceiverDeviceCapabilities +} from "../../types"; import { ErrorCallback, SuccessCallback } from "../types"; import { onMessage, sendMessageResponse } from "../eventMessageChannel"; @@ -92,17 +95,40 @@ export const timeout = new Timeout(); // chrome.cast.media namespace export * as media from "./media"; +/** + * Create `chrome.cast.Receiver` object from receiver device info. + */ +function createReceiver(device: ReceiverDevice) { + // Convert capabilities bitflag to string array + const capabilities: Capability[] = []; + if (device.capabilities & ReceiverDeviceCapabilities.VIDEO_OUT) { + capabilities.push(Capability.VIDEO_OUT); + } else if (device.capabilities & ReceiverDeviceCapabilities.VIDEO_IN) { + capabilities.push(Capability.VIDEO_IN); + } else if (device.capabilities & ReceiverDeviceCapabilities.AUDIO_OUT) { + capabilities.push(Capability.AUDIO_OUT); + } else if (device.capabilities & ReceiverDeviceCapabilities.AUDIO_IN) { + capabilities.push(Capability.AUDIO_IN); + } else if ( + device.capabilities & ReceiverDeviceCapabilities.MULTIZONE_GROUP + ) { + capabilities.push(Capability.MULTIZONE_GROUP); + } + + const receiver = new Receiver(device.id, device.friendlyName, capabilities); + + // Currently only supports CAST receivers + receiver.receiverType = ReceiverType.CAST; + + return receiver; +} + function sendSessionRequest( sessionRequest: SessionRequest, receiverDevice: ReceiverDevice ) { for (const listener of receiverActionListeners) { - const receiver = new Receiver( - receiverDevice.id, - receiverDevice.friendlyName - ); - - listener(receiver, ReceiverAction.CAST); + listener(createReceiver(receiverDevice), ReceiverAction.CAST); } sendMessageResponse({ @@ -258,13 +284,28 @@ onMessage(message => { const status = message.data; // TODO: Implement persistent per-origin receiver IDs - const receiver = new Receiver( + const receiver1 = new Receiver( status.receiverId, // label status.receiverFriendlyName, // friendlyName [Capability.VIDEO_OUT, Capability.AUDIO_OUT], // capabilities status.volume // volume ); + const receiverDevice = receiverDevices.get(status.receiverId); + if (!receiverDevice) { + logger.error( + `Could not find receiver device "${status.receiverFriendlyName}" (${status.receiverId})` + ); + break; + } + + const receiver = createReceiver(receiverDevice); + receiver.volume = status.volume; + receiver.displayStatus = new ReceiverDisplayStatus( + status.statusText, + status.appImages + ); + const session = new Session( status.sessionId, // sessionId status.appId, // appId @@ -401,7 +442,7 @@ onMessage(message => { logger.info("Selected receiver"); if (sessionRequest) { - sendSessionRequest(sessionRequest, message.data.receiver); + sendSessionRequest(sessionRequest, message.data.receiverDevice); sessionRequest = null; } @@ -409,7 +450,7 @@ onMessage(message => { } case "cast:selectReceiver/stopped": { - const { receiver } = message.data; + const { receiverDevice } = message.data; logger.info("Stopped receiver"); @@ -417,12 +458,11 @@ onMessage(message => { sessionRequest = null; for (const listener of receiverActionListeners) { - const castReceiver = new Receiver( - receiver.id, - receiver.friendlyName + listener( + // TODO: Use existing receiver object? + createReceiver(receiverDevice), + ReceiverAction.STOP ); - - listener(castReceiver, ReceiverAction.STOP); } } @@ -451,7 +491,10 @@ onMessage(message => { break; } - sendSessionRequest(apiConfig.sessionRequest, message.data.receiver); + sendSessionRequest( + apiConfig.sessionRequest, + message.data.receiverDevice + ); break; } diff --git a/ext/src/lib/loadSender.ts b/ext/src/lib/loadSender.ts index e6f5bcb..6bc7180 100644 --- a/ext/src/lib/loadSender.ts +++ b/ext/src/lib/loadSender.ts @@ -42,7 +42,7 @@ export default async function loadSender(opts: LoadSenderOptions) { instance.contentPort.postMessage({ subject: "cast:launchApp", - data: { receiver: opts.selection.receiver } + data: { receiverDevice: opts.selection.receiverDevice } }); break; @@ -53,7 +53,7 @@ export default async function loadSender(opts: LoadSenderOptions) { await browser.tabs.executeScript(opts.tabId, { code: stringify` window.selectedMedia = ${opts.selection.mediaType}; - window.selectedReceiver = ${opts.selection.receiver}; + window.selectedReceiver = ${opts.selection.receiverDevice}; `, frameId: opts.frameId }); @@ -72,7 +72,7 @@ export default async function loadSender(opts: LoadSenderOptions) { init({ mediaUrl: fileUrl.href, - receiver: opts.selection.receiver + receiver: opts.selection.receiverDevice }); break; diff --git a/ext/src/messaging.ts b/ext/src/messaging.ts index c640016..5febff8 100644 --- a/ext/src/messaging.ts +++ b/ext/src/messaging.ts @@ -47,7 +47,7 @@ type ExtMessageDefinitions = { }; }; "popup:update": { - receivers: ReceiverDevice[]; + receiverDevices: ReceiverDevice[]; defaultMediaType?: ReceiverSelectorMediaType; availableMediaTypes?: ReceiverSelectorMediaType; }; @@ -68,7 +68,7 @@ type ExtMessageDefinitions = { "cast:receiverDeviceUp": { receiverDevice: ReceiverDevice }; "cast:receiverDeviceDown": { receiverDeviceId: ReceiverDevice["id"] }; - "cast:launchApp": { receiver: ReceiverDevice }; + "cast:launchApp": { receiverDevice: ReceiverDevice }; }; /** diff --git a/ext/src/types.ts b/ext/src/types.ts index 4e2b85c..7117678 100644 --- a/ext/src/types.ts +++ b/ext/src/types.ts @@ -2,10 +2,21 @@ import { ReceiverStatus } from "./cast/api/types"; +export enum ReceiverDeviceCapabilities { + NONE = 0, + VIDEO_OUT = 1, + VIDEO_IN = 2, + AUDIO_OUT = 4, + AUDIO_IN = 8, + MULTIZONE_GROUP = 32 +} + export interface ReceiverDevice { - host: string; - friendlyName: string; id: string; + friendlyName: string; + modelName: string; + capabilities: ReceiverDeviceCapabilities; + host: string; port: number; status?: ReceiverStatus; } diff --git a/ext/src/ui/popup/index.tsx b/ext/src/ui/popup/index.tsx index 6ff6d39..56eb79f 100755 --- a/ext/src/ui/popup/index.tsx +++ b/ext/src/ui/popup/index.tsx @@ -32,7 +32,7 @@ browser.runtime.getPlatformInfo().then(platformInfo => { interface PopupAppProps {} interface PopupAppState { - receivers: ReceiverDevice[]; + receiverDevices: ReceiverDevice[]; mediaType: ReceiverSelectorMediaType; availableMediaTypes: ReceiverSelectorMediaType; isLoading: boolean; @@ -58,7 +58,7 @@ class PopupApp extends Component { super(props); this.state = { - receivers: [], + receiverDevices: [], mediaType: ReceiverSelectorMediaType.App, availableMediaTypes: ReceiverSelectorMediaType.App, isLoading: false, @@ -111,10 +111,13 @@ class PopupApp extends Component { } case "popup:update": { - const { receivers, availableMediaTypes, defaultMediaType } = - message.data; + const { + receiverDevices: receivers, + availableMediaTypes, + defaultMediaType + } = message.data; - this.setState({ receivers }); + this.setState({ receiverDevices: receivers }); if ( availableMediaTypes !== undefined && @@ -332,10 +335,11 @@ class PopupApp extends Component {
    - {this.state.receivers && this.state.receivers.length ? ( - this.state.receivers.map((receiver, i) => ( + {this.state.receiverDevices && + this.state.receiverDevices.length ? ( + this.state.receiverDevices.map((receiver, i) => ( { } } - private onCast(receiver: ReceiverDevice) { + private onCast(receiverDevice: ReceiverDevice) { this.setState({ isLoading: true }); @@ -376,20 +380,20 @@ class PopupApp extends Component { this.port?.postMessage({ subject: "receiverSelector:selected", data: { + receiverDevice, actionType: ReceiverSelectionActionType.Cast, - receiver, mediaType: this.state.mediaType, filePath: this.state.filePath } }); } - private onStop(receiver: ReceiverDevice) { + private onStop(receiverDevice: ReceiverDevice) { this.port?.postMessage({ subject: "receiverSelector:stop", data: { - actionType: ReceiverSelectionActionType.Stop, - receiver + receiverDevice, + actionType: ReceiverSelectionActionType.Stop } }); } @@ -427,11 +431,11 @@ class PopupApp extends Component { } interface ReceiverEntryProps { - receiver: ReceiverDevice; + receiverDevice: ReceiverDevice; isLoading: boolean; canCast: boolean; - onCast(receiver: ReceiverDevice): void; - onStop(receiver: ReceiverDevice): void; + onCast(receiverDevice: ReceiverDevice): void; + onStop(receiverDevice: ReceiverDevice): void; } interface ReceiverEntryState { @@ -472,18 +476,18 @@ class ReceiverEntry extends Component { } public render() { - const { status } = this.props.receiver; + const { status } = this.props.receiverDevice; const application = status?.applications?.[0]; return (
  • - {this.props.receiver.friendlyName} + {this.props.receiverDevice.friendlyName}
    {application && !application.isIdleScreen ? application.statusText - : `${this.props.receiver.host}:${this.props.receiver.port}`} + : `${this.props.receiverDevice.host}:${this.props.receiverDevice.port}`}