diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index edd3ff7..2c8cf2a 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -43,8 +43,8 @@ For an instance created for a page script SDK: 1. The [`contentInitial.ts`](./ext/src/cast/contentInitial.ts) content script is run at document start and handles some compatibility issues that can't be addressed via extension APIs (like SDK scripts directly loaded from `chrome-extension://` URLs). 2. The page loads the SDK via the usual Google-hosted `cast_sender.js` loader script. 3. The extension intercepts this script load, injects the [`contentBridge.ts`](./ext/src/cast/contentBridge.ts) script that creates a messaging connection to the Cast Manager (via extension messaging) that registers an instance for that context, and waits for a page messaging connection to forward messages through (as described [here](#communication)). The initial request is then transparently redirected to the extension-hosted SDK page script at [`src/cast/content.ts`](./src/cast/content.ts). -4. The SDK page script then creates the SDK objects ([`window.chrome.cast`](https://developers.google.com/cast/docs/reference/web_sender/chrome.cast)), handles loading the Framework API (if requested) and adds a page messaging listener for `cast:intitialized` events. -5. The Cast Manager sends a `cast:initialized` message to the SDK, which then calls the app's initialization handler ([`window.__onGCastApiAvailable`](https://developers.google.com/cast/docs/web_sender/integrate#initialization)). +4. The SDK page script then creates the SDK objects ([`window.chrome.cast`](https://developers.google.com/cast/docs/reference/web_sender/chrome.cast)), handles loading the Framework API (if requested) and adds a page messaging listener for `cast:instanceCreated` events. +5. The Cast Manager sends a `cast:instanceCreated` message to the SDK, which then calls the sender app's entry handler ([`window.__onGCastApiAvailable`](https://developers.google.com/cast/docs/web_sender/integrate#initialization)). #### Extension script @@ -55,14 +55,15 @@ For an instance created for an extension script: Depending on the extension script context: - If **background**: The Cast Manager is called directly, registering a new cast instance, providing it with a port for a newly-created message channel (since extension messaging is only supported between contexts). Page messaging is hooked up such that messages from the SDK are sent to the Cast Manager through this channel and vice versa. - If **content/extension page**: The `contentBridge.ts` script is imported as a module, with the usual side-effects of creating a messaging connection to the Cast Manager and hooking up page messaging (as described for page script instances). -3. Listeners are added for the `cast:initialized` message, so that the `ensureInit` function can resolve its promise and provide a Cast Manager port after initialization. +3. Listeners are added for the `cast:instanceCreated` message, so that the `ensureInit` function can resolve its promise and provide a Cast Manager port after initialization. #### All contexts The process now continues identically for all contexts: -1. The page's now-active sender app calls the [`chrome.cast.initialize`](https://developers.google.com/cast/docs/reference/web_sender/chrome.cast#.initialize) API function, sending a `main:initializeCast` message to the Cast Manager, providing it with the [`chrome.cast.ApiConfig`](https://developers.google.com/cast/docs/reference/web_sender/chrome.cast.ApiConfig) data and prompting a receiver availability update. -2. The page is now free to request a session if receivers are available. +1. The page's now-active sender app calls the [`chrome.cast.initialize`](https://developers.google.com/cast/docs/reference/web_sender/chrome.cast#.initialize) API function, sending a `main:initializeCastSdk` message to the Cast Manager, providing it with the [`chrome.cast.ApiConfig`](https://developers.google.com/cast/docs/reference/web_sender/chrome.cast.ApiConfig) data and prompting a receiver availability update. +2. The SDK handles the first `cast:receiverAvailabilityUpdated` message as an response to the `main:initializeCastSdk` message and calls the appropriate app callbacks. +3. The page is now free to request a session if receivers are available. ### Sessions diff --git a/ext/src/background/castManager.ts b/ext/src/background/castManager.ts index 4546125..95db785 100644 --- a/ext/src/background/castManager.ts +++ b/ext/src/background/castManager.ts @@ -169,7 +169,7 @@ const castManager = new (class { this.activeInstances.add(instance); instance.contentPort.postMessage({ - subject: "cast:initialized", + subject: "cast:instanceCreated", data: { isAvailable: (await bridge.getInfo()).isVersionCompatible } }); @@ -349,7 +349,7 @@ const castManager = new (class { } switch (message.subject) { - case "main:initializeCast": + case "main:initializeCastSdk": instance.apiConfig = message.data.apiConfig; instance.contentPort.postMessage({ subject: "cast:receiverAvailabilityUpdated", diff --git a/ext/src/cast/content.ts b/ext/src/cast/content.ts index e967d82..98fb3ea 100644 --- a/ext/src/cast/content.ts +++ b/ext/src/cast/content.ts @@ -32,7 +32,7 @@ if (document.currentScript) { pageMessenging.page.addListener(async message => { switch (message.subject) { - case "cast:initialized": { + case "cast:instanceCreated": { // If framework API is loading, wait until completed await frameworkScriptPromise; diff --git a/ext/src/cast/export.ts b/ext/src/cast/export.ts index bcd0d5b..f4becfe 100644 --- a/ext/src/cast/export.ts +++ b/ext/src/cast/export.ts @@ -65,7 +65,7 @@ export function ensureInit(contextTabId?: number): Promise { // castManager -> cast instance managerPort.addEventListener("message", ev => { const message = ev.data as Message; - if (message.subject === "cast:initialized") { + if (message.subject === "cast:instanceCreated") { if (message.data.isAvailable) { resolve(existingPort); } else { @@ -91,7 +91,7 @@ export function ensureInit(contextTabId?: number): Promise { backgroundPort.onMessage.addListener(function onManagerMessage( message: Message ) { - if (message.subject === "cast:initialized") { + if (message.subject === "cast:instanceCreated") { if (message.data.isAvailable) { resolve(pageMessenging.page.messagePort); } else { diff --git a/ext/src/cast/sdk/index.ts b/ext/src/cast/sdk/index.ts index de79b0d..8827a94 100644 --- a/ext/src/cast/sdk/index.ts +++ b/ext/src/cast/sdk/index.ts @@ -54,8 +54,10 @@ export default class { #apiConfig?: ApiConfig; #sessionRequest?: SessionRequest; + #isInitialized = false; + /** Current receiver availability. */ - #receiverAvailability = ReceiverAvailability.UNAVAILABLE; + #receiverAvailability?: ReceiverAvailability; #initializeSuccessCallback?: () => void; @@ -105,10 +107,37 @@ export default class { #onMessage(message: Message) { switch (message.subject) { - case "cast:initialized": + case "cast:instanceCreated": this.isAvailable = true; - this.#initializeSuccessCallback?.(); - this.#apiConfig?.receiverListener(this.#receiverAvailability); + break; + + case "cast:receiverAvailabilityUpdated": { + /** + * The first availability update happens after + * initialize is called. + */ + if (!this.#isInitialized) { + this.#isInitialized = true; + this.#initializeSuccessCallback?.(); + } + + const availability = message.data.isAvailable + ? ReceiverAvailability.AVAILABLE + : ReceiverAvailability.UNAVAILABLE; + + // If availability has changed, call receiver listeners + if (availability !== this.#receiverAvailability) { + this.#receiverAvailability = availability; + this.#apiConfig?.receiverListener(availability); + } + + break; + } + + case "cast:receiverAction": + for (const actionListener of this.#receiverActionListeners) { + actionListener(message.data.receiver, message.data.action); + } break; // Popup closed before session established @@ -127,6 +156,7 @@ export default class { * and data needed to create cast API objects is sent. */ case "cast:sessionCreated": { + this.#sessionRequest = undefined; const status = message.data; status.receiver.volume = status.volume; @@ -248,26 +278,6 @@ export default class { break; } - - case "cast:receiverAvailabilityUpdated": { - const availability = message.data.isAvailable - ? ReceiverAvailability.AVAILABLE - : ReceiverAvailability.UNAVAILABLE; - - // If availability has changed, call receiver listeners - if (availability !== this.#receiverAvailability) { - this.#receiverAvailability = availability; - this.#apiConfig?.receiverListener(availability); - } - - break; - } - - case "cast:receiverAction": - for (const actionListener of this.#receiverActionListeners) { - actionListener(message.data.receiver, message.data.action); - } - break; } } @@ -291,7 +301,7 @@ export default class { } pageMessenging.page.sendMessage({ - subject: "main:initializeCast", + subject: "main:initializeCastSdk", data: { apiConfig: this.#apiConfig } }); } diff --git a/ext/src/messaging.ts b/ext/src/messaging.ts index 3bd6da7..da7cd9b 100644 --- a/ext/src/messaging.ts +++ b/ext/src/messaging.ts @@ -66,10 +66,19 @@ type ExtMessageDefinitions = { * stopped. Used to provide cast API receiver action updates. */ "main:receiverStopped": { deviceId: string }; - /** Allows the selector popup to send cast NS_RECEIVER messages. */ - "main:sendReceiverMessage": ReceiverSelectorReceiverMessage; - /** Allows the selector popup to send cast NS_MEDIA messages. */ - "main:sendMediaMessage": ReceiverSelectorMediaMessage; + + /** + * Tells the cast manager to provide the cast API instance with + * receiver data. + */ + "main:initializeCastSdk": { apiConfig: ApiConfig }; + "cast:initialized": { isAvailable: boolean }; + + /** + * Sent to the cast API when a session is requested or stopped via + * the extension UI. + */ + "cast:receiverAction": { receiver: Receiver; action: ReceiverAction }; /** * Sent from the cast API to trigger receiver selection on session @@ -81,23 +90,16 @@ type ExtMessageDefinitions = { /** Return message to the cast API when a selection is cancelled. */ "cast:sessionRequestCancelled": undefined; - /** - * Sent to the cast API when a session is requested or stopped via - * the extension UI. - */ - "cast:receiverAction": { receiver: Receiver; action: ReceiverAction }; - - /** - * Tells the cast manager to provide the cast API instance with - * receiver data. - */ - "main:initializeCast": { apiConfig: ApiConfig }; - "cast:initialized": { isAvailable: boolean }; + "cast:instanceCreated": { isAvailable: boolean }; + "cast:receiverAvailabilityUpdated": { isAvailable: boolean }; "cast:sessionCreated": CastSessionCreatedDetails & { receiver: Receiver }; "cast:sessionUpdated": CastSessionUpdatedDetails; - "cast:receiverAvailabilityUpdated": { isAvailable: boolean }; + /** Allows the selector popup to send cast NS_RECEIVER messages. */ + "main:sendReceiverMessage": ReceiverSelectorReceiverMessage; + /** Allows the selector popup to send cast NS_MEDIA messages. */ + "main:sendMediaMessage": ReceiverSelectorMediaMessage; }; /**