diff --git a/ext/src/background/ReceiverSelector.ts b/ext/src/background/ReceiverSelector.ts index ad16569..4070c3e 100644 --- a/ext/src/background/ReceiverSelector.ts +++ b/ext/src/background/ReceiverSelector.ts @@ -3,9 +3,9 @@ import logger from "../lib/logger"; import messaging, { Port, Message } from "../messaging"; import options from "../lib/options"; - import { TypedEventTarget } from "../lib/TypedEventTarget"; -import { SenderMediaMessage, SenderMessage } from "../cast/sdk/types"; +import { getMediaTypesForPageUrl } from "../lib/utils"; + import { ReceiverDevice, ReceiverSelectionActionType, @@ -13,6 +13,13 @@ import { ReceiverSelectorPageInfo } from "../types"; +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"; + const POPUP_URL = browser.runtime.getURL("ui/popup/index.html"); export interface ReceiverSelectionCast { @@ -45,6 +52,16 @@ interface ReceiverSelectorEvents { mediaMessage: ReceiverSelectorMediaMessage; } +let baseConfig: BaseConfig; +baseConfigStorage + .get("baseConfig") + .then(value => { + baseConfig = value.baseConfig; + }) + .catch(() => { + logger.error("Failed to get Chromecast base config!"); + }); + /** * Manages the receiver selector popup window and communication with the * extension page hosted within. @@ -316,4 +333,249 @@ export default class ReceiverSelector extends TypedEventTarget { + return new Promise(async (resolve, reject) => { + let castInstance = castManager.getInstance( + contextTabId, + contextFrameId + ); + + /** + * If the current context is running the mirroring app, pretend + * it doesn't exist because it shouldn't be launched like this. + */ + if (castInstance?.appId === (await options.get("mirroringAppId"))) { + castInstance = undefined; + } + + let defaultMediaType = ReceiverSelectorMediaType.Tab; + let availableMediaTypes = ReceiverSelectorMediaType.None; + + let pageUrl: string | undefined; + try { + pageUrl = ( + await browser.webNavigation.getFrame({ + tabId: contextTabId, + frameId: contextFrameId + }) + ).url; + + availableMediaTypes = getMediaTypesForPageUrl(pageUrl); + } catch { + logger.error( + "Failed to locate frame, falling back to default available media types." + ); + } + + // Enable app media type if sender application is present + if (castInstance || selectionOpts?.withMediaSender) { + defaultMediaType = ReceiverSelectorMediaType.App; + availableMediaTypes |= ReceiverSelectorMediaType.App; + } + + const opts = await options.getAll(); + + // Disable mirroring media types if mirroring is not enabled + if (!opts.mirroringEnabled) { + availableMediaTypes &= ~( + ReceiverSelectorMediaType.Tab | + ReceiverSelectorMediaType.Screen + ); + } + + // Remove file media type if local media is not enabled + if (!opts.mediaEnabled || !opts.localMediaEnabled) { + availableMediaTypes &= ~ReceiverSelectorMediaType.File; + } + + // Close an existing open selector + if (ReceiverSelector.shared && ReceiverSelector.shared.isOpen) { + ReceiverSelector.shared.close(); + } + + // Get a new selector for each selection + ReceiverSelector.shared = new ReceiverSelector(); + + function onReceiverChange() { + ReceiverSelector.shared.update(deviceManager.getDevices()); + } + + deviceManager.addEventListener( + "receiverDeviceUp", + onReceiverChange + ); + deviceManager.addEventListener( + "receiverDeviceDown", + onReceiverChange + ); + deviceManager.addEventListener( + "receiverDeviceUpdated", + onReceiverChange + ); + deviceManager.addEventListener( + "receiverDeviceMediaUpdated", + onReceiverChange + ); + + function onSelectorSelected( + ev: CustomEvent + ) { + logger.info("Selected receiver", ev.detail); + + resolve({ + actionType: ReceiverSelectionActionType.Cast, + receiverDevice: ev.detail.receiverDevice, + mediaType: ev.detail.mediaType + }); + } + function onSelectorStop(ev: CustomEvent) { + logger.info("Stopping receiver app...", ev.detail); + + deviceManager.stopReceiverApp(ev.detail.receiverDevice.id); + + resolve({ + actionType: ReceiverSelectionActionType.Stop, + receiverDevice: ev.detail.receiverDevice + }); + } + function onSelectorCancelled() { + logger.info("Cancelled receiver selection"); + + resolve(null); + } + function onSelectorError(ev: CustomEvent) { + reject(ev.detail); + } + function onReceiverMessage( + ev: CustomEvent + ) { + deviceManager.sendReceiverMessage( + ev.detail.deviceId, + ev.detail.message + ); + } + function onMediaMessage( + ev: CustomEvent + ) { + deviceManager.sendMediaMessage( + ev.detail.deviceId, + ev.detail.message + ); + } + + ReceiverSelector.shared.addEventListener( + "selected", + onSelectorSelected + ); + ReceiverSelector.shared.addEventListener("stop", onSelectorStop); + ReceiverSelector.shared.addEventListener( + "cancelled", + onSelectorCancelled + ); + ReceiverSelector.shared.addEventListener("error", onSelectorError); + ReceiverSelector.shared.addEventListener( + "receiverMessage", + onReceiverMessage + ); + ReceiverSelector.shared.addEventListener( + "mediaMessage", + onMediaMessage + ); + ReceiverSelector.shared.addEventListener("close", removeListeners); + + function removeListeners() { + ReceiverSelector.shared.removeEventListener( + "selected", + onSelectorSelected + ); + ReceiverSelector.shared.removeEventListener( + "stop", + onSelectorStop + ); + ReceiverSelector.shared.removeEventListener( + "cancelled", + onSelectorCancelled + ); + ReceiverSelector.shared.removeEventListener( + "error", + onSelectorError + ); + ReceiverSelector.shared.removeEventListener( + "receiverMessage", + onReceiverMessage + ); + ReceiverSelector.shared.removeEventListener( + "mediaMessage", + onMediaMessage + ); + ReceiverSelector.shared.removeEventListener( + "close", + removeListeners + ); + + deviceManager.removeEventListener( + "receiverDeviceUp", + onReceiverChange + ); + deviceManager.removeEventListener( + "receiverDeviceDown", + onReceiverChange + ); + deviceManager.removeEventListener( + "receiverDeviceUpdated", + onReceiverChange + ); + deviceManager.removeEventListener( + "receiverDeviceMediaUpdated", + onReceiverChange + ); + } + + // Ensure status manager is initialized + await deviceManager.init(); + + let isRequestAppAudioCompatible: Optional; + if (castInstance?.appId) { + const appTag = getAppTag(baseConfig, castInstance.appId); + isRequestAppAudioCompatible = appTag?.supports_audio_only; + } + + ReceiverSelector.shared.open({ + receiverDevices: deviceManager.getDevices(), + defaultMediaType, + availableMediaTypes, + appId: castInstance?.appId, + // Create page info + pageInfo: pageUrl + ? { + url: pageUrl, + tabId: contextTabId, + frameId: contextFrameId, + sessionRequest: selectionOpts?.sessionRequest, + isRequestAppAudioCompatible + } + : undefined + }); + }); + } } diff --git a/ext/src/background/background.ts b/ext/src/background/background.ts index b9482b4..72f3884 100755 --- a/ext/src/background/background.ts +++ b/ext/src/background/background.ts @@ -7,11 +7,11 @@ import bridge, { BridgeInfo } from "../lib/bridge"; import castManager from "./castManager"; import deviceManager from "./deviceManager"; -import selectorManager from "./selectorManager"; +import ReceiverSelector from "./ReceiverSelector"; import { initMenus } from "./menus"; import { initWhitelist } from "./whitelist"; -import { baseConfigStorage, fetchBaseConfig } from "../cast/googleapi"; +import { baseConfigStorage, fetchBaseConfig } from "../cast/googleApi"; const _ = browser.i18n.getMessage; @@ -145,7 +145,7 @@ async function init() { return; } - const selection = await selectorManager.getSelection(tab.id); + const selection = await ReceiverSelector.getSelection(tab.id); if (selection) { castManager.loadSender({ tabId: tab.id, diff --git a/ext/src/background/castManager.ts b/ext/src/background/castManager.ts index 6115b99..2e2e65b 100644 --- a/ext/src/background/castManager.ts +++ b/ext/src/background/castManager.ts @@ -11,10 +11,8 @@ import { ReceiverSelectorMediaType } from "../types"; -import { ReceiverSelection } from "./ReceiverSelector"; - import deviceManager from "./deviceManager"; -import selectorManager from "./selectorManager"; +import ReceiverSelector, { ReceiverSelection } from "./ReceiverSelector"; type AnyPort = Port | MessagePort; @@ -225,7 +223,7 @@ export default new (class { } try { - const selection = await selectorManager.getSelection( + const selection = await ReceiverSelector.getSelection( instance.contentTabId, instance.contentFrameId, { sessionRequest: message.data.sessionRequest } @@ -297,7 +295,7 @@ export default new (class { * same one that caused the session creation. */ case "main:closeReceiverSelector": { - const selector = await selectorManager.getSelector(); + const selector = ReceiverSelector.shared; const shouldClose = await options.get( "receiverSelectorWaitForConnection" ); diff --git a/ext/src/background/menus.ts b/ext/src/background/menus.ts index 844cdf6..825c2c2 100644 --- a/ext/src/background/menus.ts +++ b/ext/src/background/menus.ts @@ -9,9 +9,7 @@ import { ReceiverSelectorMediaType } from "../types"; -import { ReceiverSelection } from "./ReceiverSelector"; - -import selectorManager from "./selectorManager"; +import ReceiverSelector, { ReceiverSelection } from "./ReceiverSelector"; import castManager from "./castManager"; const _ = browser.i18n.getMessage; @@ -157,10 +155,12 @@ async function onMenuClicked( let selection: Nullable = null; try { - selection = await selectorManager.getSelection( + selection = await ReceiverSelector.getSelection( tab.id, info.frameId, - { withMediaSender: castMediaMenuClicked } + { + withMediaSender: castMediaMenuClicked + } ); } catch (err) { logger.error("Failed to get receiver selection (cast menu)", err); diff --git a/ext/src/background/selectorManager.ts b/ext/src/background/selectorManager.ts deleted file mode 100644 index 6d830f2..0000000 --- a/ext/src/background/selectorManager.ts +++ /dev/null @@ -1,257 +0,0 @@ -"use strict"; - -import options from "../lib/options"; -import logger from "../lib/logger"; - -import { getMediaTypesForPageUrl } from "../lib/utils"; -import { BaseConfig, baseConfigStorage, getAppTag } from "../cast/googleapi"; -import { SessionRequest } from "../cast/sdk/classes"; - -import castManager from "./castManager"; -import deviceManager from "./deviceManager"; - -import ReceiverSelector, { - ReceiverSelection, - ReceiverSelectionCast, - ReceiverSelectionStop, - ReceiverSelectorMediaMessage, - ReceiverSelectorReceiverMessage -} from "./ReceiverSelector"; - -import { - ReceiverSelectionActionType, - ReceiverSelectorMediaType -} from "../types"; - -let baseConfig: BaseConfig; -baseConfigStorage - .get("baseConfig") - .then(value => { - baseConfig = value.baseConfig; - }) - .catch(() => { - logger.error("Failed to get Chromecast base config!"); - }); - -let sharedSelector: ReceiverSelector; -async function getSelector() { - if (!sharedSelector) { - try { - sharedSelector = new ReceiverSelector(); - } catch (err) { - throw logger.error("Failed to create receiver selector."); - } - } - - return sharedSelector; -} - -/** - * Opens a receiver selector with the specified default/available media - * types. - * - * Returns a promise that: - * - Resolves to a ReceiverSelection object if selection is - * successful. - * - Resolves to null if the selection is cancelled. - * - Rejects if the selection fails. - */ -async function getSelection( - contextTabId: number, - contextFrameId = 0, - selectionOpts?: { - sessionRequest?: SessionRequest; - withMediaSender?: boolean; - } -): Promise { - return new Promise(async (resolve, reject) => { - let castInstance = castManager.getInstance( - contextTabId, - contextFrameId - ); - - /** - * If the current context is running the mirroring app, pretend - * it doesn't exist because it shouldn't be launched like this. - */ - if (castInstance?.appId === (await options.get("mirroringAppId"))) { - castInstance = undefined; - } - - let defaultMediaType = ReceiverSelectorMediaType.Tab; - let availableMediaTypes = ReceiverSelectorMediaType.None; - - let pageUrl: string | undefined; - try { - pageUrl = ( - await browser.webNavigation.getFrame({ - tabId: contextTabId, - frameId: contextFrameId - }) - ).url; - - availableMediaTypes = getMediaTypesForPageUrl(pageUrl); - } catch { - logger.error( - "Failed to locate frame, falling back to default available media types." - ); - } - - // Enable app media type if sender application is present - if (castInstance || selectionOpts?.withMediaSender) { - defaultMediaType = ReceiverSelectorMediaType.App; - availableMediaTypes |= ReceiverSelectorMediaType.App; - } - - const opts = await options.getAll(); - - // Disable mirroring media types if mirroring is not enabled - if (!opts.mirroringEnabled) { - availableMediaTypes &= ~( - ReceiverSelectorMediaType.Tab | ReceiverSelectorMediaType.Screen - ); - } - - // Remove file media type if local media is not enabled - if (!opts.mediaEnabled || !opts.localMediaEnabled) { - availableMediaTypes &= ~ReceiverSelectorMediaType.File; - } - - // Close an existing open selector - if (sharedSelector && sharedSelector.isOpen) { - sharedSelector.close(); - } - - // Get a new selector for each selection - sharedSelector = new ReceiverSelector(); - - function onReceiverChange() { - sharedSelector.update(deviceManager.getDevices()); - } - - deviceManager.addEventListener("receiverDeviceUp", onReceiverChange); - deviceManager.addEventListener("receiverDeviceDown", onReceiverChange); - deviceManager.addEventListener( - "receiverDeviceUpdated", - onReceiverChange - ); - deviceManager.addEventListener( - "receiverDeviceMediaUpdated", - onReceiverChange - ); - - function onSelectorSelected(ev: CustomEvent) { - logger.info("Selected receiver", ev.detail); - - resolve({ - actionType: ReceiverSelectionActionType.Cast, - receiverDevice: ev.detail.receiverDevice, - mediaType: ev.detail.mediaType - }); - } - function onSelectorStop(ev: CustomEvent) { - logger.info("Stopping receiver app...", ev.detail); - - deviceManager.stopReceiverApp(ev.detail.receiverDevice.id); - - resolve({ - actionType: ReceiverSelectionActionType.Stop, - receiverDevice: ev.detail.receiverDevice - }); - } - function onSelectorCancelled() { - logger.info("Cancelled receiver selection"); - - resolve(null); - } - function onSelectorError(ev: CustomEvent) { - reject(ev.detail); - } - function onReceiverMessage( - ev: CustomEvent - ) { - deviceManager.sendReceiverMessage( - ev.detail.deviceId, - ev.detail.message - ); - } - function onMediaMessage(ev: CustomEvent) { - deviceManager.sendMediaMessage( - ev.detail.deviceId, - ev.detail.message - ); - } - - sharedSelector.addEventListener("selected", onSelectorSelected); - sharedSelector.addEventListener("stop", onSelectorStop); - sharedSelector.addEventListener("cancelled", onSelectorCancelled); - sharedSelector.addEventListener("error", onSelectorError); - sharedSelector.addEventListener("receiverMessage", onReceiverMessage); - sharedSelector.addEventListener("mediaMessage", onMediaMessage); - sharedSelector.addEventListener("close", removeListeners); - - function removeListeners() { - sharedSelector.removeEventListener("selected", onSelectorSelected); - sharedSelector.removeEventListener("stop", onSelectorStop); - sharedSelector.removeEventListener( - "cancelled", - onSelectorCancelled - ); - sharedSelector.removeEventListener("error", onSelectorError); - sharedSelector.removeEventListener( - "receiverMessage", - onReceiverMessage - ); - sharedSelector.removeEventListener("mediaMessage", onMediaMessage); - sharedSelector.removeEventListener("close", removeListeners); - - deviceManager.removeEventListener( - "receiverDeviceUp", - onReceiverChange - ); - deviceManager.removeEventListener( - "receiverDeviceDown", - onReceiverChange - ); - deviceManager.removeEventListener( - "receiverDeviceUpdated", - onReceiverChange - ); - deviceManager.removeEventListener( - "receiverDeviceMediaUpdated", - onReceiverChange - ); - } - - // Ensure status manager is initialized - await deviceManager.init(); - - let isRequestAppAudioCompatible: Optional; - if (castInstance?.appId) { - const appTag = getAppTag(baseConfig, castInstance.appId); - isRequestAppAudioCompatible = appTag?.supports_audio_only; - } - - sharedSelector.open({ - receiverDevices: deviceManager.getDevices(), - defaultMediaType, - availableMediaTypes, - appId: castInstance?.appId, - // Create page info - pageInfo: pageUrl - ? { - url: pageUrl, - tabId: contextTabId, - frameId: contextFrameId, - sessionRequest: selectionOpts?.sessionRequest, - isRequestAppAudioCompatible - } - : undefined - }); - }); -} - -export default { - getSelection, - getSelector -};