From 3eba371c5f8e4c778876784eb0cb652e5611f629 Mon Sep 17 00:00:00 2001 From: Matt Hensman Date: Fri, 9 Sep 2022 23:32:49 +0100 Subject: [PATCH] Implement auto join policy handling --- ext/src/background/castManager.ts | 734 +++++++++++++++++----------- ext/src/background/deviceManager.ts | 41 +- ext/src/cast/knownApps.ts | 2 +- ext/src/cast/sdk/Session.ts | 9 +- ext/src/cast/sdk/index.ts | 10 +- ext/src/cast/senders/media.ts | 5 +- ext/src/cast/senders/mirroring.ts | 5 +- 7 files changed, 493 insertions(+), 313 deletions(-) diff --git a/ext/src/background/castManager.ts b/ext/src/background/castManager.ts index 493791c..17621e2 100644 --- a/ext/src/background/castManager.ts +++ b/ext/src/background/castManager.ts @@ -17,32 +17,78 @@ import { } from "../types"; import type { ApiConfig } from "../cast/sdk/classes"; -import { ReceiverAction } from "../cast/sdk/enums"; -import { DEFAULT_MEDIA_RECEIVER_APP_ID } from "../cast/sdk/media"; +import { AutoJoinPolicy, ReceiverAction } from "../cast/sdk/enums"; import { createReceiver } from "../cast/utils"; -import deviceManager from "./deviceManager"; - import ReceiverSelector, { ReceiverSelection, ReceiverSelectorMediaMessage, ReceiverSelectorReceiverMessage } from "./ReceiverSelector"; +import deviceManager from "./deviceManager"; + type AnyPort = Port | TypedMessagePort; export interface ContentContext { tabId: number; frameId: number; + origin?: string; +} + +/** Checks if two content contexts match. */ +function isSameContext(ctx1?: ContentContext, ctx2?: ContentContext) { + if (!ctx1 || !ctx2) return false; + return ctx1?.tabId === ctx2?.tabId && ctx1?.frameId === ctx2?.frameId; } interface CastSession { - sessionId: string; + bridgePort: Port; deviceId: string; + appId: string; + sessionId?: string; + initialContentContext?: ContentContext; +} + +/** Creates a cast session object and sets up messaging. */ +async function createCastSession(opts: { + deviceId: string; + instance: CastInstance; + appId?: string; +}) { + // If not explicitly provided, use session request app ID + if (!opts.appId) { + if (!opts.instance.apiConfig?.sessionRequest) { + throw logger.error( + "App ID not provided and instance missing valid session request!" + ); + } + opts.appId = opts.instance.apiConfig.sessionRequest.appId; + } + + const session: CastSession = { + bridgePort: await bridge.connect(), + deviceId: opts.deviceId, + appId: opts.appId, + initialContentContext: opts.instance.contentContext + }; + + opts.instance.session = session; + opts.instance.bridgeMessageListener = message => { + handleBridgeMessage(opts.instance, message); + }; + + session.bridgePort.onMessage.addListener( + opts.instance.bridgeMessageListener + ); + session.bridgePort.onDisconnect.addListener(() => + destroyCastInstance(opts.instance) + ); + + return session; } export interface CastInstance { - bridgePort: Port; contentPort: AnyPort; contentContext?: ContentContext; @@ -53,17 +99,18 @@ export interface CastInstance { apiConfig?: ApiConfig; /** Established session details. */ session?: CastSession; + + /** Listener for bridge messages. */ + bridgeMessageListener?: (message: Message) => void; } /** Creates a cast instance object and associated bridge instance. */ -async function createCastInstance(opts: { - bridgePort?: Port; +function createCastInstance(opts: { contentPort: AnyPort; contentContext?: { tabId: number; frameId?: number }; isTrusted?: boolean; }) { const instance: CastInstance = { - bridgePort: opts.bridgePort ?? (await bridge.connect()), contentPort: opts.contentPort, isTrusted: opts.isTrusted ?? false }; @@ -81,35 +128,43 @@ async function createCastInstance(opts: { !(opts.contentPort instanceof MessagePort) && opts.contentPort.sender?.tab?.id ) { + // Get origin from content port + let origin: Optional; + if (opts.contentPort.sender?.tab?.url) { + try { + ({ origin } = new URL(opts.contentPort.sender.tab.url)); + // eslint-disable-next-line no-empty + } catch {} + } + instance.contentContext = { tabId: opts.contentPort.sender.tab.id, - frameId: opts.contentPort.sender.frameId ?? 0 + frameId: opts.contentPort.sender.frameId ?? 0, + origin }; } return instance; } -/** Disconnects either instance content port type. */ -function disconnectContentPort(port: AnyPort) { - if (port instanceof MessagePort) { - port.close(); +/** Removes cast instance and disconnects messaging ports. */ +function destroyCastInstance(instance: CastInstance) { + if (instance.contentPort instanceof MessagePort) { + instance.contentPort.close(); } else { - port.disconnect(); + instance.contentPort.disconnect(); } + + if (instance.session && instance.bridgeMessageListener) { + instance.session.bridgePort.onMessage.removeListener( + instance.bridgeMessageListener + ); + } + + activeInstances.delete(instance); } -/** Checks if two content contexts match. */ -function isSameContext(ctx1?: ContentContext, ctx2?: ContentContext) { - if (!ctx1 || !ctx2) return false; - return ctx1?.tabId === ctx2?.tabId && ctx1?.frameId === ctx2?.frameId; -} - -let baseConfig: BaseConfig; -let receiverSelector: Optional; - -const activeInstances = new Set(); - +/** Whitelist of safe message types from content. */ const allowedContentMessages: Array = [ "main:initializeCastSdk", "main:requestSession", @@ -117,6 +172,17 @@ const allowedContentMessages: Array = [ "bridge:sendCastSessionMessage" ]; +/** Chromecast base config to check compatibility with audio devices. */ +let baseConfig: BaseConfig; +/** Shared receiver selector. */ +let receiverSelector: Optional; + +/** Set of active cast instances. */ +const activeInstances = new Set(); + +/** Map of active session IDs to session info objects. */ +const activeSessions = new Map(); + /** Keeps track of cast API instances and provides bridge messaging. */ const castManager = new (class { async init() { @@ -130,7 +196,7 @@ const castManager = new (class { } }); - // Pass receiver availability updates to cast API. + // Pass receiver availability updates to cast API const updateReceiverAvailability = () => { const isAvailable = deviceManager.getDevices().length > 0; @@ -147,6 +213,25 @@ const castManager = new (class { "deviceDown", updateReceiverAvailability ); + + deviceManager.addEventListener("applicationClosed", ev => { + const session = activeSessions.get(ev.detail.sessionId); + if (!session?.sessionId) return; + + // Remove session from instances and notify SDK + for (const instance of activeInstances) { + if (instance.session === session) { + instance.contentPort.postMessage({ + subject: "cast:sessionStopped", + data: { sessionId: session.sessionId } + }); + + delete instance.session; + } + } + + activeSessions.delete(session.sessionId); + }); } /** @@ -200,7 +285,6 @@ const castManager = new (class { contentContext?: ContentContext ): Promise { const instance = await createCastInstance({ - bridgePort: await bridge.connect(), contentPort, contentContext, isTrusted: true @@ -210,26 +294,15 @@ const castManager = new (class { if (contentContext) { for (const instance of activeInstances) { if (isSameContext(instance.contentContext, contentContext)) { - instance.bridgePort.disconnect(); - activeInstances.delete(instance); + destroyCastInstance(instance); break; } } } - instance.bridgePort.onDisconnect.addListener(() => { - contentPort.close(); - activeInstances.delete(instance); - }); - - // bridge -> cast instance - instance.bridgePort.onMessage.addListener(message => { - this.handleBridgeMessage(instance, message); - }); - // cast instance -> (any) contentPort.addEventListener("message", ev => { - this.handleContentMessage(instance, ev.data); + handleContentMessage(instance, ev.data); }); contentPort.start(); @@ -252,220 +325,21 @@ const castManager = new (class { ); } - // Ensure only one instance per context - for (const instance of activeInstances) { - if ( - isSameContext( - instance.contentContext, - contentPort.sender as ContentContext - ) - ) { - instance.bridgePort.disconnect(); - disconnectContentPort(instance.contentPort); - break; - } - } - const instance = await createCastInstance({ contentPort, isTrusted }); // cast instance -> (any) const onContentPortMessage = (message: Message) => { - this.handleContentMessage(instance, message); - }; - // bridge -> cast instance - const onBridgePortMessage = (message: Message) => { - this.handleBridgeMessage(instance, message); + handleContentMessage(instance, message); }; - const onDisconnect = () => { - instance.bridgePort.onMessage.removeListener(onBridgePortMessage); - contentPort.onMessage.removeListener(onContentPortMessage); - - instance.bridgePort.disconnect(); - contentPort.disconnect(); - - activeInstances.delete(instance); - }; - - instance.bridgePort.onDisconnect.addListener(onDisconnect); - instance.bridgePort.onMessage.addListener(onBridgePortMessage); - - contentPort.onDisconnect.addListener(onDisconnect); contentPort.onMessage.addListener(onContentPortMessage); + contentPort.onDisconnect.addListener(() => { + destroyCastInstance(instance); + }); return instance; } - private async handleBridgeMessage( - instance: CastInstance, - message: Message - ) { - // Intercept messages to store relevant info - switch (message.subject) { - case "main:castSessionCreated": { - // Close after session is created - if ( - receiverSelector?.isOpen && - // If selector context is the same as the instance context - isSameContext( - receiverSelector.pageInfo, - instance.contentContext - ) && - // If selector is supposed to close - (await options.get("receiverSelectorWaitForConnection")) - ) { - receiverSelector.close(); - } - - const { receiverId: deviceId } = message.data; - - instance.session = { - 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); - } - - /** - * Handle content messages from the cast instance. These will either - * be handled here in the background script or forwarded to the - * bridge associated with the cast instance. - */ - private async handleContentMessage( - instance: CastInstance, - message: Message - ) { - // Limit untrusted instances to allowed messages subset - if ( - !allowedContentMessages.includes(message.subject) && - !instance.isTrusted - ) { - logger.error(`Forbidden message type! (${message.subject})`); - disconnectContentPort(instance.contentPort); - return; - } - - const [destination] = message.subject.split(":"); - if (destination === "bridge") { - instance.bridgePort.postMessage(message); - } - - switch (message.subject) { - case "main:initializeCastSdk": - instance.apiConfig = message.data.apiConfig; - instance.contentPort.postMessage({ - subject: "cast:receiverAvailabilityUpdated", - data: { - isAvailable: deviceManager.getDevices().length > 0 - } - }); - - break; - - // User has triggered receiver selection via the cast API - case "main:requestSession": { - const { sessionRequest, receiverDevice } = message.data; - - // Handle trusted instance receiver selection bypass - if (receiverDevice) { - if (!instance.isTrusted) { - logger.error( - "Cast instance not trusted to bypass receiver selection!" - ); - disconnectContentPort(instance.contentPort); - break; - } - - instance.bridgePort.postMessage({ - subject: "bridge:createCastSession", - data: { - appId: sessionRequest.appId, - receiverDevice - } - }); - - break; - } - - try { - const selection = await getReceiverSelection({ - castInstance: instance - }); - - // Handle cancellation - if (!selection) { - instance.contentPort.postMessage({ - subject: "cast:sessionRequestCancelled" - }); - - break; - } - - /** - * If the media type returned from the selector has - * been changed, we need to cancel the current - * sender and switch it out for the right one. - */ - if (selection.mediaType !== ReceiverSelectorMediaType.App) { - instance.contentPort.postMessage({ - subject: "cast:sessionRequestCancelled" - }); - - if (!instance.contentContext) { - throw logger.error("Missing content context"); - } - this.loadSender(selection, instance.contentContext); - - break; - } - - instance.bridgePort.postMessage({ - subject: "bridge:createCastSession", - data: { - appId: sessionRequest.appId, - receiverDevice: selection.device - } - }); - } catch (err) { - // TODO: Report errors properly - instance.contentPort.postMessage({ - subject: "cast:sessionRequestCancelled" - }); - } - - break; - } - } - } - /** * Gets a receiver selection and loads the appropriate sender for a * given context. @@ -481,63 +355,344 @@ const castManager = new (class { if (!selection) return; - this.loadSender(selection, { tabId, frameId }); + loadSender(selection, { tabId, frameId }); } +})(); - /** - * Loads the appropriate sender for a given receiver selector - * response. - */ - private async loadSender( - selection: ReceiverSelection, - contentContext: ContentContext - ) { - // Cancelled - if (!selection) { - return; +export default castManager; + +/** Handles messages to cast instances from bridge. */ +async function handleBridgeMessage(instance: CastInstance, message: Message) { + // Intercept messages to store relevant info + switch (message.subject) { + case "main:castSessionCreated": { + // Close after session is created + if ( + receiverSelector?.isOpen && + // If selector context is the same as the instance context + isSameContext( + receiverSelector.pageInfo, + instance.contentContext + ) && + // If selector is supposed to close + (await options.get("receiverSelectorWaitForConnection")) + ) { + receiverSelector.close(); + } + + const { receiverId: deviceId } = message.data; + + if (!instance.session) { + logger.error("Instance is missing session!"); + break; + } + + instance.session.sessionId = message.data.sessionId; + activeSessions.set(message.data.sessionId, instance.session); + + 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; } - switch (selection.mediaType) { - case ReceiverSelectorMediaType.App: { - const instance = this.getInstanceAt( - contentContext.tabId, - contentContext.frameId - ); - if (!instance) { - throw logger.error( - `Cast instance not found at tabId ${contentContext.tabId} / frameId ${contentContext.frameId}` - ); + case "main:castSessionUpdated": + instance.contentPort.postMessage({ + subject: "cast:sessionUpdated", + data: message.data + }); + } + + instance.contentPort.postMessage(message); +} + +/** + * Handle content messages from the cast instance. These will either + * be handled here in the background script or forwarded to the + * bridge associated with the cast instance. + */ +async function handleContentMessage(instance: CastInstance, message: Message) { + // Limit untrusted instances to allowed messages subset + if ( + !allowedContentMessages.includes(message.subject) && + !instance.isTrusted + ) { + logger.error(`Forbidden message type! (${message.subject})`); + destroyCastInstance(instance); + return; + } + + const [destination] = message.subject.split(":"); + if (destination === "bridge") { + instance.session?.bridgePort.postMessage(message); + } + + switch (message.subject) { + case "main:initializeCastSdk": + instance.apiConfig = message.data.apiConfig; + instance.contentPort.postMessage({ + subject: "cast:receiverAvailabilityUpdated", + data: { + isAvailable: deviceManager.getDevices().length > 0 + } + }); + + // No need to check for existing sessions if page-scoped + if ( + instance.apiConfig.autoJoinPolicy === AutoJoinPolicy.PAGE_SCOPED + ) { + break; + } + + // Check existing sessions for a valid auto join target + sessionLoop: for (const [, session] of activeSessions) { + if ( + !session.sessionId || + session.appId !== instance.apiConfig.sessionRequest.appId + ) { + continue; } - if (!instance.apiConfig?.sessionRequest.appId) { - throw logger.error("Invalid session request"); - } + switch (instance.apiConfig.autoJoinPolicy) { + case AutoJoinPolicy.TAB_AND_ORIGIN_SCOPED: + // Ensure matching content tontext + if ( + !isSameContext( + instance.contentContext, + session.initialContentContext + ) + ) + break; + // eslint-disable-next-line no-fallthrough + case AutoJoinPolicy.ORIGIN_SCOPED: { + // Ensure matching origin + if ( + instance.contentContext?.origin !== + session.initialContentContext?.origin + ) + break; - instance.contentPort.postMessage({ - subject: "cast:receiverAction", - data: { - receiver: createReceiver(selection.device), - action: ReceiverAction.CAST + instance.session = session; + instance.bridgeMessageListener = message => + handleBridgeMessage(instance, message); + + session.bridgePort.onMessage.addListener( + instance.bridgeMessageListener + ); + session.bridgePort.onDisconnect.addListener(() => + destroyCastInstance(instance) + ); + + const device = deviceManager.getDeviceById( + session.deviceId + ); + if (!device?.status?.applications?.length) { + throw logger.error("Invalid device state"); + } + + /** + * Re-create sessionCreated message. Since the + * sender app hasn't requested a session, this + * will be handled by calling the session + * listener. + */ + const application = device?.status?.applications[0]; + instance.contentPort.postMessage({ + subject: "cast:sessionCreated", + data: { + appId: application.appId, + appImages: [], + displayName: application.displayName, + namespaces: application.namespaces, + receiver: createReceiver(device), + receiverFriendlyName: device.friendlyName, + receiverId: device.id, + senderApps: [], + sessionId: session.sessionId, + statusText: application.statusText, + transportId: session.sessionId, + volume: device.status.volume + } + }); + + break sessionLoop; } + } + } + + break; + + // User has triggered receiver selection via the cast API + case "main:requestSession": { + const { sessionRequest, receiverDevice } = message.data; + + // Handle trusted instance receiver selection bypass + if (receiverDevice) { + if (receiverSelector?.isOpen && instance.contentContext) { + receiverSelector.pageInfo = { + ...instance.contentContext, + url: ( + await browser.webNavigation.getFrame({ + tabId: instance.contentContext?.tabId, + frameId: instance.contentContext?.frameId + }) + ).url + }; + } + + if (!instance.isTrusted) { + logger.error( + "Cast instance not trusted to bypass receiver selection!" + ); + destroyCastInstance(instance); + break; + } + + const session = await createCastSession({ + instance, + deviceId: receiverDevice.id, + appId: sessionRequest.appId }); - instance.bridgePort.postMessage({ + session.bridgePort.postMessage({ subject: "bridge:createCastSession", data: { - appId: instance.apiConfig?.sessionRequest.appId, - receiverDevice: selection.device + appId: sessionRequest.appId, + receiverDevice } }); break; } - case ReceiverSelectorMediaType.Screen: - await createMirroringPopup(selection.device); - break; + try { + const selection = await getReceiverSelection({ + castInstance: instance + }); + + // Handle cancellation + if (!selection) { + instance.contentPort.postMessage({ + subject: "cast:sessionRequestCancelled" + }); + + break; + } + + /** + * If the media type returned from the selector has + * been changed, we need to cancel the current + * sender and switch it out for the right one. + */ + if (selection.mediaType !== ReceiverSelectorMediaType.App) { + instance.contentPort.postMessage({ + subject: "cast:sessionRequestCancelled" + }); + + if (!instance.contentContext) { + throw logger.error("Missing content context"); + } + loadSender(selection, instance.contentContext); + + break; + } + + const session = await createCastSession({ + instance, + deviceId: selection.device.id, + appId: sessionRequest.appId + }); + + session.bridgePort.postMessage({ + subject: "bridge:createCastSession", + data: { + appId: sessionRequest.appId, + receiverDevice: selection.device + } + }); + } catch (err) { + // TODO: Report errors properly + instance.contentPort.postMessage({ + subject: "cast:sessionRequestCancelled" + }); + } + + break; } } -})(); +} + +/** + * Loads the appropriate sender for a given receiver selector response. + */ +async function loadSender( + selection: ReceiverSelection, + contentContext: ContentContext +) { + // Cancelled + if (!selection) { + return; + } + + switch (selection.mediaType) { + case ReceiverSelectorMediaType.App: { + const instance = castManager.getInstanceAt( + contentContext.tabId, + contentContext.frameId + ); + if (!instance) { + throw logger.error( + `Cast instance not found at tabId ${contentContext.tabId} / frameId ${contentContext.frameId}` + ); + } + + if (!instance.apiConfig?.sessionRequest.appId) { + throw logger.error("Invalid session request"); + } + + instance.contentPort.postMessage({ + subject: "cast:receiverAction", + data: { + receiver: createReceiver(selection.device), + action: ReceiverAction.CAST + } + }); + + const session = await createCastSession({ + instance, + deviceId: selection.device.id + }); + + session.bridgePort.postMessage({ + subject: "bridge:createCastSession", + data: { + appId: session.appId, + receiverDevice: selection.device + } + }); + + break; + } + + case ReceiverSelectorMediaType.Screen: + await createMirroringPopup(selection.device); + break; + } +} /** * Opens a receiver selector with the specified default/available media @@ -596,15 +751,9 @@ async function getReceiverSelection(selectionOpts: { selectionOpts.tabId, selectionOpts.frameId ); - /** - * If the app in that context is the extension mirroring app or - * the default receiver, just ignore it. - */ - const contextAppId = contextInstance?.apiConfig?.sessionRequest.appId; - if ( - contextAppId !== opts.mirroringAppId && - contextAppId !== DEFAULT_MEDIA_RECEIVER_APP_ID - ) { + + // Ignore extension senders + if (!contextInstance?.isTrusted) { selectionOpts.castInstance = contextInstance; } } @@ -754,7 +903,7 @@ function createSelector() { const onDeviceChange = () => { const connectedSessionIds: string[] = []; for (const instance of activeInstances) { - if (instance.session) { + if (instance.session?.sessionId) { connectedSessionIds.push(instance.session.sessionId); } } @@ -793,6 +942,7 @@ function createSelector() { return selector; } +/** Creates and manages mirroring popup window. */ async function createMirroringPopup(device: ReceiverDevice) { let popup: browser.windows.Window; try { @@ -826,5 +976,3 @@ async function createMirroringPopup(device: ReceiverDevice) { browser.windows.onRemoved.removeListener(onWindowRemoved); }); } - -export default castManager; diff --git a/ext/src/background/deviceManager.ts b/ext/src/background/deviceManager.ts index d9ba0fa..4e42f44 100644 --- a/ext/src/background/deviceManager.ts +++ b/ext/src/background/deviceManager.ts @@ -16,14 +16,11 @@ import { PlayerState } from "../cast/sdk/media/enums"; interface EventMap { deviceUp: { deviceInfo: ReceiverDevice }; deviceDown: { deviceId: string }; - deviceUpdated: { - deviceId: string; - status: ReceiverStatus; - }; - deviceMediaUpdated: { - deviceId: string; - status: MediaStatus; - }; + 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 { @@ -168,10 +165,25 @@ export default new (class extends TypedEventTarget { 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; @@ -185,6 +197,19 @@ export default new (class extends TypedEventTarget { }) ); + // Send new application found event + if ( + !oldApplication && + application && + !application.isIdleScreen + ) { + this.dispatchEvent( + new CustomEvent("applicationFound", { + detail: { deviceId, appId: application.appId } + }) + ); + } + break; } diff --git a/ext/src/cast/knownApps.ts b/ext/src/cast/knownApps.ts index f489a9b..89f894e 100644 --- a/ext/src/cast/knownApps.ts +++ b/ext/src/cast/knownApps.ts @@ -16,7 +16,7 @@ export default { "CA5E8412": { name: "Netflix", matches: "https://www.netflix.com/*" }, "233637DE": { name: "YouTube", matches: "https://www.youtube.com/*" }, "CC32E753": { name: "Spotify", matches: "https://open.spotify.com/*" }, - "5E81F6DB": { + "2BA92214": { name: "BBC iPlayer", matches: "https://www.bbc.co.uk/iplayer*" }, diff --git a/ext/src/cast/sdk/Session.ts b/ext/src/cast/sdk/Session.ts index b358b1a..c4ebc99 100644 --- a/ext/src/cast/sdk/Session.ts +++ b/ext/src/cast/sdk/Session.ts @@ -12,8 +12,8 @@ import type { SenderMessage } from "./types"; -import { SessionStatus } from "./enums"; -import type { +import { ErrorCode, SessionStatus } from "./enums"; +import { Error as CastError, Image, Receiver, @@ -268,6 +268,11 @@ export default class Session { successCallback?: (media: Media) => void, errorCallback?: (err: CastError) => void ) { + if (!loadRequest) { + errorCallback?.(new CastError(ErrorCode.INVALID_PARAMETER)); + return; + } + this.#loadMediaSuccessCallback = successCallback; this.#loadMediaErrorCallback = errorCallback; diff --git a/ext/src/cast/sdk/index.ts b/ext/src/cast/sdk/index.ts index b67adaf..89fb637 100644 --- a/ext/src/cast/sdk/index.ts +++ b/ext/src/cast/sdk/index.ts @@ -222,7 +222,7 @@ export default class { case "cast:sessionStopped": { const { sessionId } = message.data; const session = this.#sessions.get(sessionId); - if (session) { + if (session?.status === SessionStatus.CONNECTED) { session.status = SessionStatus.STOPPED; const updateListeners = SessionUpdateListeners.get(session); @@ -349,7 +349,7 @@ export default class { }); } - requestSessionById(_sessionId: string): void { + requestSessionById(_sessionId: string) { logger.info("STUB :: cast.requestSessionById"); } @@ -357,15 +357,15 @@ export default class { _receivers: Receiver[], _successCallback?: () => void, _errorCallback?: (err: CastError) => void - ): void { + ) { logger.info("STUB :: cast.setCustomReceivers"); } - setPageContext(_win: Window): void { + setPageContext(_win: Window) { logger.info("STUB :: cast.setPageContext"); } - setReceiverDisplayStatus(_sessionId: string): void { + setReceiverDisplayStatus(_sessionId: string) { logger.info("STUB :: cast.setReceiverDisplayStatus"); } diff --git a/ext/src/cast/senders/media.ts b/ext/src/cast/senders/media.ts index 9596cb4..f417eb6 100644 --- a/ext/src/cast/senders/media.ts +++ b/ext/src/cast/senders/media.ts @@ -4,7 +4,7 @@ import options from "../../lib/options"; import type { Message } from "../../messaging"; // Cast types -import type { ReceiverAvailability } from "../sdk/enums"; +import { AutoJoinPolicy, ReceiverAvailability } from "../sdk/enums"; import type Session from "../sdk/Session"; import type Media from "../sdk/media/Media"; @@ -88,7 +88,8 @@ export default class MediaSender { capabilities ), this.sessionListener.bind(this), - this.receiverListener.bind(this) + this.receiverListener.bind(this), + AutoJoinPolicy.PAGE_SCOPED ), undefined, err => { diff --git a/ext/src/cast/senders/mirroring.ts b/ext/src/cast/senders/mirroring.ts index 8f49175..b5d6964 100644 --- a/ext/src/cast/senders/mirroring.ts +++ b/ext/src/cast/senders/mirroring.ts @@ -3,7 +3,7 @@ import { Logger } from "../../lib/logger"; import type { ReceiverDevice } from "../../types"; -import type { ReceiverAvailability } from "../sdk/enums"; +import { AutoJoinPolicy, ReceiverAvailability } from "../sdk/enums"; import type Session from "../sdk/Session"; import cast, { ensureInit } from "../export"; @@ -79,7 +79,8 @@ export default class MirroringSender { const apiConfig = new cast.ApiConfig( sessionRequest, this.sessionListener, - this.receiverListener + this.receiverListener, + AutoJoinPolicy.PAGE_SCOPED ); cast.initialize(apiConfig);