From d3d83eb1c3af9222414c690fd0e6f0ed5cc1118b Mon Sep 17 00:00:00 2001 From: hensm Date: Fri, 2 Sep 2022 16:11:23 +0100 Subject: [PATCH] Add auto-expansion of media controls for devices with connected sessions --- ext/src/_locales/en/messages.json | 4 +++ ext/src/background/ReceiverSelector.ts | 20 ++++++------ ext/src/background/castManager.ts | 43 ++++++++++++++++---------- ext/src/defaultOptions.ts | 2 ++ ext/src/messaging.ts | 3 +- ext/src/ui/options/Options.svelte | 16 ++++++++++ ext/src/ui/popup/Popup.svelte | 25 +++++++++------ ext/src/ui/popup/Receiver.svelte | 20 +++++++++++- 8 files changed, 93 insertions(+), 40 deletions(-) diff --git a/ext/src/_locales/en/messages.json b/ext/src/_locales/en/messages.json index 0d952a3..3ee725f 100755 --- a/ext/src/_locales/en/messages.json +++ b/ext/src/_locales/en/messages.json @@ -388,6 +388,10 @@ "message": "Close after losing focus", "description": "Receiver selector close if focus lost option checkbox label." }, + "optionsReceiverSelectorExpandActive": { + "message": "Expand media controls for connected devices", + "description": "Receiver selector expand active checkbox label." + }, "optionsSiteWhitelistCategoryName": { "message": "Site whitelist", diff --git a/ext/src/background/ReceiverSelector.ts b/ext/src/background/ReceiverSelector.ts index 168264f..1345c3f 100644 --- a/ext/src/background/ReceiverSelector.ts +++ b/ext/src/background/ReceiverSelector.ts @@ -14,7 +14,7 @@ import type { const POPUP_URL = browser.runtime.getURL("ui/popup/index.html"); export interface ReceiverSelection { - receiverDevice: ReceiverDevice; + device: ReceiverDevice; mediaType: ReceiverSelectorMediaType; } @@ -49,7 +49,7 @@ export default class ReceiverSelector extends TypedEventTarget; +const activeInstances = new Set(); + /** Keeps track of cast API instances and provides bridge messaging. */ const castManager = new (class { - private activeInstances = new Set(); - async init() { // Handle incoming instance connections messaging.onConnect.addListener(async port => { @@ -127,7 +127,7 @@ const castManager = new (class { const updateReceiverAvailability = () => { const isAvailable = deviceManager.getDevices().length > 0; - for (const instance of this.activeInstances) { + for (const instance of activeInstances) { instance.contentPort.postMessage({ subject: "cast:receiverAvailabilityUpdated", data: { isAvailable } @@ -146,7 +146,7 @@ const castManager = new (class { * Finds a cast instance at the given tab (and optionally frame) ID. */ getInstanceAt(tabId: number, frameId?: number) { - for (const instance of this.activeInstances) { + for (const instance of activeInstances) { if (instance.contentContext?.tabId === tabId) { // If frame ID doesn't match go to next instance if (frameId && instance.contentContext.frameId !== frameId) { @@ -159,7 +159,7 @@ const castManager = new (class { } getInstanceByDeviceId(deviceId: string) { - for (const instance of this.activeInstances) { + for (const instance of activeInstances) { if (instance.session?.deviceId === deviceId) return instance; } } @@ -177,7 +177,7 @@ const castManager = new (class { ? this.createInstanceFromBackground(port, contentContext) : this.createInstanceFromContent(port, isTrusted)); - this.activeInstances.add(instance); + activeInstances.add(instance); instance.contentPort.postMessage({ subject: "cast:instanceCreated", @@ -201,10 +201,10 @@ const castManager = new (class { // Ensure only one instance per context if (contentContext) { - for (const instance of this.activeInstances) { + for (const instance of activeInstances) { if (isSameContext(instance.contentContext, contentContext)) { instance.bridgePort.disconnect(); - this.activeInstances.delete(instance); + activeInstances.delete(instance); break; } } @@ -212,7 +212,7 @@ const castManager = new (class { instance.bridgePort.onDisconnect.addListener(() => { contentPort.close(); - this.activeInstances.delete(instance); + activeInstances.delete(instance); }); // bridge -> cast instance @@ -246,7 +246,7 @@ const castManager = new (class { } // Ensure only one instance per context - for (const instance of this.activeInstances) { + for (const instance of activeInstances) { if ( isSameContext( instance.contentContext, @@ -277,7 +277,7 @@ const castManager = new (class { instance.bridgePort.disconnect(); contentPort.disconnect(); - this.activeInstances.delete(instance); + activeInstances.delete(instance); }; instance.bridgePort.onDisconnect.addListener(onDisconnect); @@ -433,7 +433,7 @@ const castManager = new (class { subject: "bridge:createCastSession", data: { appId: sessionRequest.appId, - receiverDevice: selection.receiverDevice + receiverDevice: selection.device } }); } catch (err) { @@ -498,7 +498,7 @@ const castManager = new (class { instance.contentPort.postMessage({ subject: "cast:receiverAction", data: { - receiver: createReceiver(selection.receiverDevice), + receiver: createReceiver(selection.device), action: ReceiverAction.CAST } }); @@ -507,7 +507,7 @@ const castManager = new (class { subject: "bridge:createCastSession", data: { appId: instance.apiConfig?.sessionRequest.appId, - receiverDevice: selection.receiverDevice + receiverDevice: selection.device } }); @@ -519,7 +519,7 @@ const castManager = new (class { await browser.tabs.executeScript(contentContext.tabId, { code: stringify` window.mirroringMediaType = ${selection.mediaType}; - window.receiverDevice = ${selection.receiverDevice}; + window.receiverDevice = ${selection.device}; window.contextTabId = ${contentContext.tabId}; `, frameId: contentContext.frameId @@ -695,7 +695,7 @@ async function getReceiverSelection(selectionOpts: { ); receiverSelector.open({ - receiverDevices: deviceManager.getDevices(), + devices: deviceManager.getDevices(), defaultMediaType, availableMediaTypes, appInfo, @@ -751,7 +751,16 @@ function createSelector() { selector.addEventListener("mediaMessage", onMediaMessage); // Update selector data whenever devices change/update - const onDeviceChange = () => selector.update(deviceManager.getDevices()); + const onDeviceChange = () => { + const connectedSessionIds: string[] = []; + for (const instance of activeInstances) { + if (instance.session) { + connectedSessionIds.push(instance.session.sessionId); + } + } + + selector.update(deviceManager.getDevices(), connectedSessionIds); + }; deviceManager.addEventListener("deviceUp", onDeviceChange); deviceManager.addEventListener("deviceDown", onDeviceChange); diff --git a/ext/src/defaultOptions.ts b/ext/src/defaultOptions.ts index 08a7d9b..416bd5b 100644 --- a/ext/src/defaultOptions.ts +++ b/ext/src/defaultOptions.ts @@ -16,6 +16,7 @@ export interface Options { mirroringAppId: string; receiverSelectorCloseIfFocusLost: boolean; receiverSelectorWaitForConnection: boolean; + receiverSelectorExpandActive: boolean; siteWhitelistEnabled: boolean; siteWhitelist: WhitelistItemData[]; siteWhitelistCustomUserAgent: string; @@ -40,6 +41,7 @@ export default { mirroringAppId: MIRRORING_APP_ID, receiverSelectorCloseIfFocusLost: true, receiverSelectorWaitForConnection: true, + receiverSelectorExpandActive: true, siteWhitelistEnabled: true, siteWhitelist: [{ pattern: "https://www.netflix.com/*", isEnabled: true }], siteWhitelistCustomUserAgent: "", diff --git a/ext/src/messaging.ts b/ext/src/messaging.ts index fa4299b..f85ed48 100644 --- a/ext/src/messaging.ts +++ b/ext/src/messaging.ts @@ -49,7 +49,8 @@ type ExtMessageDefinitions = { }; /** Updates selector popup with new data. */ "popup:update": { - receiverDevices: ReceiverDevice[]; + devices: ReceiverDevice[]; + connectedSessionIds?: string[]; defaultMediaType?: ReceiverSelectorMediaType; availableMediaTypes?: ReceiverSelectorMediaType; }; diff --git a/ext/src/ui/options/Options.svelte b/ext/src/ui/options/Options.svelte index 874f82c..f0383ec 100644 --- a/ext/src/ui/options/Options.svelte +++ b/ext/src/ui/options/Options.svelte @@ -259,6 +259,22 @@ {_("optionsReceiverSelectorCloseIfFocusLost")} + +
+
+ +
+ +
{/if} diff --git a/ext/src/ui/popup/Popup.svelte b/ext/src/ui/popup/Popup.svelte index a54e6ce..11e75ec 100644 --- a/ext/src/ui/popup/Popup.svelte +++ b/ext/src/ui/popup/Popup.svelte @@ -29,6 +29,8 @@ /** Devices to display. */ let devices: ReceiverDevice[] = []; + /** IDs of sessions connected by this extension. */ + let connectedSessionIds: string[] = []; /** Sender app info (if available). */ let appInfo: Optional; @@ -172,6 +174,8 @@ break; case "popup:update": { + updateKnownApp(); + if ( message.data.availableMediaTypes !== undefined && message.data.defaultMediaType !== undefined @@ -183,9 +187,11 @@ } } - updateKnownApp(); + devices = message.data.devices; - devices = message.data.receiverDevices; + if (message.data.connectedSessionIds) { + connectedSessionIds = message.data.connectedSessionIds; + } break; } @@ -288,30 +294,27 @@ } } - function onReceiverCast(receiverDevice: ReceiverDevice) { + function onReceiverCast(device: ReceiverDevice) { isConnecting = true; port?.postMessage({ subject: "main:receiverSelected", - data: { - receiverDevice, - mediaType - } + data: { device, mediaType } }); } - function onReceiverStop(receiverDevice: ReceiverDevice) { + function onReceiverStop(device: ReceiverDevice) { port?.postMessage({ subject: "main:sendReceiverMessage", data: { - deviceId: receiverDevice.id, + deviceId: device.id, message: { requestId: 0, type: "STOP" } } }); port?.postMessage({ subject: "main:receiverStopped", - data: { deviceId: receiverDevice.id } + data: { deviceId: device.id } }); } @@ -378,8 +381,10 @@ {:else} {#each devices as device} import { createEventDispatcher, onMount } from "svelte"; + import type { Options } from "../../lib/options"; + import { ReceiverDevice, ReceiverDeviceCapabilities } from "../../types"; import type { Port } from "../../messaging"; @@ -33,8 +35,11 @@ /** Whether any media types are available for this receiver. */ export let isAnyMediaTypeAvailable: boolean; - /** Receiver device to display. */ + /** Device to display. */ export let device: ReceiverDevice; + export let connectedSessionIds: string[]; + + export let opts: Nullable; /** Current receiver application (if available) */ $: application = device.status?.applications?.[0]; @@ -79,8 +84,20 @@ /** Whether media controls are shown. */ let isExpanded = false; + let isExpandedUserModified = false; + + // Unexpand if media status disappears $: if (!device.mediaStatus) { isExpanded = false; + } else if ( + // If app is running + application?.appId && + // And user hasn't manually changed the expanded state + !isExpandedUserModified && + // And auto-expansion is enabled + opts?.receiverSelectorExpandActive + ) { + isExpanded = connectedSessionIds.includes(application?.transportId); } /** Whether a session request is in progress for this receiver.. */ @@ -429,6 +446,7 @@ disabled={!mediaStatus} on:click={() => { isExpanded = !isExpanded; + isExpandedUserModified = true; }} />