From 83c81219d7a91e97331775ae3340e67122e7726e Mon Sep 17 00:00:00 2001 From: hensm Date: Mon, 29 Aug 2022 09:43:10 +0100 Subject: [PATCH] Pass Receiver objects instead of ReceiverDevice objects to cast API --- app/src/bridge/components/cast/Session.ts | 4 +- app/src/bridge/messaging.ts | 4 +- ext/src/background/ReceiverSelector.ts | 25 ++- ext/src/background/castManager.ts | 111 +++++++++---- ext/src/background/deviceManager.ts | 36 ++-- ext/src/cast/sdk/index.ts | 194 +++++----------------- ext/src/cast/utils.ts | 19 ++- ext/src/messaging.ts | 26 +-- 8 files changed, 185 insertions(+), 234 deletions(-) diff --git a/app/src/bridge/components/cast/Session.ts b/app/src/bridge/components/cast/Session.ts index 48d673d..de514b7 100644 --- a/app/src/bridge/components/cast/Session.ts +++ b/app/src/bridge/components/cast/Session.ts @@ -73,7 +73,7 @@ export default class Session extends CastClient { this.onSessionCreated?.(this.sessionId); messaging.sendMessage({ - subject: "cast:sessionCreated", + subject: "main:castSessionCreated", data: { sessionId: this.sessionId, statusText: application.statusText, @@ -103,7 +103,7 @@ export default class Session extends CastClient { } messaging.sendMessage({ - subject: "cast:sessionUpdated", + subject: "main:castSessionUpdated", data: { sessionId: this.sessionId, statusText: application.statusText, diff --git a/app/src/bridge/messaging.ts b/app/src/bridge/messaging.ts index 917f1eb..cac69cd 100644 --- a/app/src/bridge/messaging.ts +++ b/app/src/bridge/messaging.ts @@ -110,8 +110,8 @@ type MessageDefinitions = { * updates. Updated details is a mutable subset of session details * otherwise fixed on creation. */ - "cast:sessionCreated": CastSessionCreatedDetails; - "cast:sessionUpdated": CastSessionUpdatedDetails; + "main:castSessionCreated": CastSessionCreatedDetails; + "main:castSessionUpdated": CastSessionUpdatedDetails; /** * Sent to cast API instances whenever a session is stopped. */ diff --git a/ext/src/background/ReceiverSelector.ts b/ext/src/background/ReceiverSelector.ts index 773774b..f26e302 100644 --- a/ext/src/background/ReceiverSelector.ts +++ b/ext/src/background/ReceiverSelector.ts @@ -16,8 +16,10 @@ import deviceManager from "./deviceManager"; import castManager from "./castManager"; import { BaseConfig, baseConfigStorage, getAppTag } from "../cast/googleApi"; -import type { SessionRequest } from "../cast/sdk/classes"; import type { SenderMediaMessage, SenderMessage } from "../cast/sdk/types"; +import type { SessionRequest } from "../cast/sdk/classes"; +import { ReceiverAction } from "../cast/sdk/enums"; +import { createReceiver } from "../cast/utils"; const POPUP_URL = browser.runtime.getURL("ui/popup/index.html"); @@ -330,7 +332,10 @@ export default class ReceiverSelector extends TypedEventTarget; - if (castInstance?.appId) { + if (castInstance?.apiConfig?.sessionRequest.appId) { if (!baseConfig) { try { baseConfig = (await baseConfigStorage.get("baseConfig")) @@ -389,7 +394,7 @@ export default class ReceiverSelector extends TypedEventTarget { + // Pass receiver availability updates to cast API. + const updateReceiverAvailability = () => { + const isAvailable = deviceManager.getDevices().length > 0; + for (const instance of this.activeInstances) { instance.contentPort.postMessage({ - subject: "cast:receiverDeviceUp", - data: { receiverDevice: ev.detail.deviceInfo } + subject: "cast:updateReceiverAvailability", + data: { isAvailable } }); } - }); - deviceManager.addEventListener("deviceDown", ev => { - for (const instance of this.activeInstances) { - instance.contentPort.postMessage({ - subject: "cast:receiverDeviceDown", - data: { receiverDeviceId: ev.detail.deviceId } - }); - } - }); + }; + + deviceManager.addEventListener("deviceUp", updateReceiverAvailability); + deviceManager.addEventListener( + "deviceDown", + updateReceiverAvailability + ); } /** @@ -197,7 +204,7 @@ export default new (class { ) { // Intercept messages to store relevant info switch (message.subject) { - case "cast:sessionCreated": { + case "main:castSessionCreated": { // Close after session is created const selector = ReceiverSelector.sharedInstance; if ( @@ -211,12 +218,38 @@ export default new (class { selector.close(); } + const { receiverId: deviceId } = message.data; + instance.session = { - deviceId: message.data.receiverId, + deviceId, sessionId: message.data.sessionId }; + + const device = deviceManager.getDeviceById(deviceId); + if (!device) { + logger.error( + "[on main:castSessionCreated]: Could not find device with ID:", + deviceId + ); + break; + } + + instance.contentPort.postMessage({ + subject: "cast:sessionCreated", + data: { + ...message.data, + receiver: createReceiver(device) + } + }); + break; } + + case "main:castSessionUpdated": + instance.contentPort.postMessage({ + subject: "cast:sessionUpdated", + data: message.data + }); } instance.contentPort.postMessage(message); @@ -239,14 +272,14 @@ export default new (class { switch (message.subject) { // Cast API has been initialized case "main:initializeCast": { - instance.appId = message.data.appId; + instance.apiConfig = message.data.apiConfig; - for (const receiverDevice of deviceManager.getDevices()) { - instance.contentPort.postMessage({ - subject: "cast:receiverDeviceUp", - data: { receiverDevice } - }); - } + instance.contentPort.postMessage({ + subject: "cast:updateReceiverAvailability", + data: { + isAvailable: deviceManager.getDevices().length > 0 + } + }); break; } @@ -262,11 +295,13 @@ export default new (class { ); } + const { sessionRequest } = message.data; + try { const selection = await ReceiverSelector.getSelection( instance.contentTabId, instance.contentFrameId, - { sessionRequest: message.data.sessionRequest } + { sessionRequest } ); // Handle cancellation @@ -297,9 +332,12 @@ export default new (class { break; } - instance.contentPort.postMessage({ - subject: "cast:selectReceiver/selected", - data: selection + instance.bridgePort.postMessage({ + subject: "bridge:createCastSession", + data: { + appId: sessionRequest.appId, + receiverDevice: selection.receiverDevice + } }); } catch (err) { // TODO: Report errors properly @@ -336,9 +374,24 @@ export default new (class { ); } + if (!instance.apiConfig?.sessionRequest.appId) { + throw logger.error("Invalid session request"); + } + instance.contentPort.postMessage({ - subject: "cast:launchApp", - data: { receiverDevice: opts.selection.receiverDevice } + subject: "cast:sendReceiverAction", + data: { + receiver: createReceiver(opts.selection.receiverDevice), + action: ReceiverAction.CAST + } + }); + + instance.bridgePort.postMessage({ + subject: "bridge:createCastSession", + data: { + appId: instance.apiConfig?.sessionRequest.appId, + receiverDevice: opts.selection.receiverDevice + } }); break; diff --git a/ext/src/background/deviceManager.ts b/ext/src/background/deviceManager.ts index c0037cc..600b52d 100644 --- a/ext/src/background/deviceManager.ts +++ b/ext/src/background/deviceManager.ts @@ -30,8 +30,8 @@ interface EventMap { export default new (class extends TypedEventTarget { /** - * Map of receiver device IDs to devices. Updated as - * receiverDevice messages are received from the bridge. + * Map of receiver device IDs to devices. Updated as receiverDevice + * messages are received from the bridge. */ private receiverDevices = new Map(); @@ -43,8 +43,8 @@ export default new (class extends TypedEventTarget { } /** - * Initialize (or re-initialize) a bridge connection to - * start dispatching events. + * Initializes (or re-initializes) a bridge connection to start + * dispatching events. */ async refresh() { this.bridgePort?.disconnect(); @@ -63,33 +63,16 @@ export default new (class extends TypedEventTarget { }); } - /** - * Get a list of receiver devices - */ + /** Gets a list of receiver devices. */ getDevices() { return Array.from(this.receiverDevices.values()); } - - /** - * Stops a receiver app running on a given device. - */ - stopReceiverApp(receiverDeviceId: string) { - if (!this.bridgePort) { - logger.error( - "Failed to stop receiver device, no bridge connection" - ); - return; - } - - const receiverDevice = this.receiverDevices.get(receiverDeviceId); - if (receiverDevice) { - this.bridgePort.postMessage({ - subject: "bridge:stopCastSession", - data: { receiverDevice } - }); - } + /** 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( @@ -112,6 +95,7 @@ export default new (class extends TypedEventTarget { }); } + /** 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)"); diff --git a/ext/src/cast/sdk/index.ts b/ext/src/cast/sdk/index.ts index f4f7e9f..2df125c 100644 --- a/ext/src/cast/sdk/index.ts +++ b/ext/src/cast/sdk/index.ts @@ -5,7 +5,6 @@ import logger from "../../lib/logger"; import type { Message } from "../../messaging"; import eventMessaging from "../eventMessaging"; -import type { ReceiverDevice } from "../../types"; import type { ErrorCallback, SuccessCallback } from "../types"; import { @@ -39,7 +38,6 @@ import { import Session from "./Session"; import media from "./media"; -import { convertCapabilitiesFlags } from "../utils"; type ReceiverActionListener = ( receiver: Receiver, @@ -48,35 +46,21 @@ type ReceiverActionListener = ( type RequestSessionSuccessCallback = (session: Session) => void; -/** - * Create `chrome.cast.Receiver` object from receiver device info. - */ -function createReceiver(device: ReceiverDevice) { - const receiver = new Receiver( - device.id, - device.friendlyName, - convertCapabilitiesFlags(device.capabilities) - ); - - // Currently only supports CAST receivers - receiver.receiverType = ReceiverType.CAST; - - return receiver; -} - /** Cast SDK root class */ export default class { - #receiverDevices = new Map(); - #apiConfig?: ApiConfig; #sessionRequest?: SessionRequest; #requestSessionSuccessCallback?: RequestSessionSuccessCallback; #requestSessionErrorCallback?: ErrorCallback; + #initializeSuccessCallback?: SuccessCallback; + #sessions = new Map(); #receiverActionListeners = new Set(); + #receiverAvailability = ReceiverAvailability.UNAVAILABLE; + // Enums AutoJoinPolicy = AutoJoinPolicy; Capability = Capability; @@ -114,29 +98,15 @@ export default class { eventMessaging.page.addListener(this.#onMessage.bind(this)); } - #sendSessionRequest( - sessionRequest: SessionRequest, - receiverDevice: ReceiverDevice - ) { - for (const listener of this.#receiverActionListeners) { - listener(createReceiver(receiverDevice), ReceiverAction.CAST); - } - - eventMessaging.page.sendMessage({ - subject: "bridge:createCastSession", - data: { - appId: sessionRequest.appId, - receiverDevice: receiverDevice - } - }); - } - #onMessage(message: Message) { switch (message.subject) { - case "cast:initialized": { + case "cast:initialized": + this.#initializeSuccessCallback?.(); + this.#apiConfig?.receiverListener(this.#receiverAvailability); + this.isAvailable = true; + break; - } /** * Once the bridge detects a session creation, session info @@ -144,19 +114,9 @@ export default class { */ case "cast:sessionCreated": { const status = message.data; - const receiverDevice = this.#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.receiver.volume = status.volume; + status.receiver.displayStatus = new ReceiverDisplayStatus( status.statusText, status.appImages ); @@ -166,7 +126,7 @@ export default class { status.appId, status.displayName, status.appImages, - receiver + status.receiver ); session.namespaces = status.namespaces; @@ -178,9 +138,9 @@ export default class { /** * If session created via requestSession, the success - * callback will be set, otherwise the session was created - * by the extension and the session listener should be - * called instead. + * callback will be set, otherwise the session was + * created by the extension and the session listener + * should be called instead. */ if (this.#requestSessionSuccessCallback) { this.#requestSessionSuccessCallback(session); @@ -263,49 +223,15 @@ export default class { break; } - case "cast:receiverDeviceUp": { - const { receiverDevice } = message.data; - if (this.#receiverDevices.has(receiverDevice.id)) { - break; - } + case "cast:updateReceiverAvailability": { + const availability = message.data.isAvailable + ? ReceiverAvailability.AVAILABLE + : ReceiverAvailability.UNAVAILABLE; - this.#receiverDevices.set(receiverDevice.id, receiverDevice); - - if (this.#apiConfig) { - // Notify listeners of new cast destination - this.#apiConfig.receiverListener( - ReceiverAvailability.AVAILABLE - ); - } - - break; - } - - case "cast:receiverDeviceDown": { - const { receiverDeviceId } = message.data; - - this.#receiverDevices.delete(receiverDeviceId); - - if (this.#receiverDevices.size === 0) { - if (this.#apiConfig) { - this.#apiConfig.receiverListener( - ReceiverAvailability.UNAVAILABLE - ); - } - } - - break; - } - - case "cast:selectReceiver/selected": { - logger.info("Selected receiver"); - - if (this.#sessionRequest) { - this.#sendSessionRequest( - this.#sessionRequest, - message.data.receiverDevice - ); - this.#sessionRequest = undefined; + // If availability has changed, call receiver listeners + if (availability !== this.#receiverAvailability) { + this.#receiverAvailability = availability; + this.#apiConfig?.receiverListener(availability); } break; @@ -324,35 +250,13 @@ export default class { break; } - case "cast:receiverStoppedAction": { - const device = this.#receiverDevices.get(message.data.deviceId); - if (!device) break; - + case "cast:sendReceiverAction": { for (const actionListener of this.#receiverActionListeners) { - actionListener(createReceiver(device), ReceiverAction.STOP); + actionListener(message.data.receiver, message.data.action); } break; } - - // Session request initiated via receiver selector - case "cast:launchApp": { - if (this.#sessionRequest) { - logger.error("Session request already in progress."); - break; - } - if (!this.#apiConfig?.sessionRequest) { - logger.error("Session request not found!"); - break; - } - - this.#sendSessionRequest( - this.#apiConfig.sessionRequest, - message.data.receiverDevice - ); - - break; - } } } @@ -371,25 +275,20 @@ export default class { this.#apiConfig = apiConfig; + if (successCallback) { + this.#initializeSuccessCallback = successCallback; + } + eventMessaging.page.sendMessage({ subject: "main:initializeCast", - data: { appId: this.#apiConfig.sessionRequest.appId } + data: { apiConfig: this.#apiConfig } }); - - successCallback?.(); - - this.#apiConfig.receiverListener( - this.#receiverDevices.size - ? ReceiverAvailability.AVAILABLE - : ReceiverAvailability.UNAVAILABLE - ); } requestSession( successCallback: RequestSessionSuccessCallback, errorCallback: ErrorCallback, - newSessionRequest?: SessionRequest, - receiverDevice?: ReceiverDevice + newSessionRequest?: SessionRequest ) { logger.info("cast.requestSession"); @@ -410,40 +309,23 @@ export default class { return; } - // No receivers available - if (!this.#receiverDevices.size) { + if (this.#receiverAvailability === ReceiverAvailability.UNAVAILABLE) { errorCallback?.(new Error_(ErrorCode.RECEIVER_UNAVAILABLE)); return; } - /** - * Store session request for use in return message from - * receiver selection. - */ + // Store used session request this.#sessionRequest = newSessionRequest ?? this.#apiConfig.sessionRequest; this.#requestSessionSuccessCallback = successCallback; this.#requestSessionErrorCallback = errorCallback; - /** - * If a receiver was provided, skip the receiver selector - * process. - */ - if (receiverDevice) { - if ( - receiverDevice?.id && - this.#receiverDevices.has(receiverDevice.id) - ) { - this.#sendSessionRequest(this.#sessionRequest, receiverDevice); - } - } else { - // Open receiver selector UI - eventMessaging.page.sendMessage({ - subject: "main:selectReceiver", - data: { sessionRequest: this.#sessionRequest } - }); - } + // Open receiver selector UI + eventMessaging.page.sendMessage({ + subject: "main:selectReceiver", + data: { sessionRequest: this.#sessionRequest } + }); } requestSessionById(_sessionId: string): void { diff --git a/ext/src/cast/utils.ts b/ext/src/cast/utils.ts index 1f948c6..fc72e07 100644 --- a/ext/src/cast/utils.ts +++ b/ext/src/cast/utils.ts @@ -1,5 +1,6 @@ import { ReceiverDevice, ReceiverDeviceCapabilities } from "../types"; -import { Capability } from "./sdk/enums"; +import { Receiver } from "./sdk/classes"; +import { Capability, ReceiverType } from "./sdk/enums"; /** * Check receiver device capabilities bitflags against array of @@ -64,3 +65,19 @@ export function getEstimatedTime(opts: GetEstimatedTimeOpts) { return estimatedTime; } + +/** + * Create `chrome.cast.Receiver` object from receiver device info. + */ +export function createReceiver(device: ReceiverDevice) { + const receiver = new Receiver( + device.id, + device.friendlyName, + convertCapabilitiesFlags(device.capabilities) + ); + + // Currently only supports CAST receivers + receiver.receiverType = ReceiverType.CAST; + + return receiver; +} diff --git a/ext/src/messaging.ts b/ext/src/messaging.ts index f998f5c..68c7106 100644 --- a/ext/src/messaging.ts +++ b/ext/src/messaging.ts @@ -16,9 +16,10 @@ import type { SenderMediaMessage, SenderMessage } from "./cast/sdk/types"; -import type { SessionRequest } from "./cast/sdk/classes"; +import type { ApiConfig, Receiver, SessionRequest } from "./cast/sdk/classes"; import type { ReceiverDevice, ReceiverSelectorMediaType } from "./types"; +import type { ReceiverAction } from "./cast/sdk/enums"; /** * Messages are JSON objects with a `subject` string key and a @@ -76,24 +77,26 @@ type ExtMessageDefinitions = { "main:selectReceiver": { sessionRequest: SessionRequest; }; - /** Return message to the cast API when a receiver is selected. */ - "cast:selectReceiver/selected": ReceiverSelection; /** Return message to the cast API when a selection is cancelled. */ "cast:selectReceiver/cancelled": undefined; - /** Sent to the cast API when a receiver app is stopped. */ - "cast:receiverStoppedAction": { deviceId: string }; + /** + * Sent to the cast API when a session is requested or stopped via + * the extension UI. + */ + "cast:sendReceiverAction": { receiver: Receiver; action: ReceiverAction }; /** * Tells the cast manager to provide the cast API instance with * receiver data. */ - "main:initializeCast": { appId: string }; + "main:initializeCast": { apiConfig: ApiConfig }; "cast:initialized": { isAvailable: boolean }; - "cast:receiverDeviceUp": { receiverDevice: ReceiverDevice }; - "cast:receiverDeviceDown": { receiverDeviceId: string }; - "cast:launchApp": { receiverDevice: ReceiverDevice }; + "cast:sessionCreated": CastSessionCreatedDetails & { receiver: Receiver }; + "cast:sessionUpdated": CastSessionUpdatedDetails; + + "cast:updateReceiverAvailability": { isAvailable: boolean }; }; /** @@ -189,8 +192,9 @@ type AppMessageDefinitions = { * updates. Updated details is a mutable subset of session details * otherwise fixed on creation. */ - "cast:sessionCreated": CastSessionCreatedDetails; - "cast:sessionUpdated": CastSessionUpdatedDetails; + "main:castSessionCreated": CastSessionCreatedDetails; + "main:castSessionUpdated": CastSessionUpdatedDetails; + /** * Sent to cast API instances whenever a session is stopped. */