Add auto-expansion of media controls for devices with connected sessions

This commit is contained in:
hensm
2022-09-02 16:11:23 +01:00
parent 1cb4f2eecb
commit d3d83eb1c3
8 changed files with 93 additions and 40 deletions

View File

@@ -388,6 +388,10 @@
"message": "Close after losing focus", "message": "Close after losing focus",
"description": "Receiver selector close if focus lost option checkbox label." "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": { "optionsSiteWhitelistCategoryName": {
"message": "Site whitelist", "message": "Site whitelist",

View File

@@ -14,7 +14,7 @@ import type {
const POPUP_URL = browser.runtime.getURL("ui/popup/index.html"); const POPUP_URL = browser.runtime.getURL("ui/popup/index.html");
export interface ReceiverSelection { export interface ReceiverSelection {
receiverDevice: ReceiverDevice; device: ReceiverDevice;
mediaType: ReceiverSelectorMediaType; mediaType: ReceiverSelectorMediaType;
} }
@@ -49,7 +49,7 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
private messagePort?: Port; private messagePort?: Port;
private messagePortDisconnected?: boolean; private messagePortDisconnected?: boolean;
private receiverDevices?: ReceiverDevice[]; private devices?: ReceiverDevice[];
private defaultMediaType?: ReceiverSelectorMediaType; private defaultMediaType?: ReceiverSelectorMediaType;
private availableMediaTypes?: ReceiverSelectorMediaType; private availableMediaTypes?: ReceiverSelectorMediaType;
@@ -86,7 +86,7 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
* Creates and opens a receiver selector window. * Creates and opens a receiver selector window.
*/ */
public async open(opts: { public async open(opts: {
receiverDevices: ReceiverDevice[]; devices: ReceiverDevice[];
defaultMediaType: ReceiverSelectorMediaType; defaultMediaType: ReceiverSelectorMediaType;
availableMediaTypes: ReceiverSelectorMediaType; availableMediaTypes: ReceiverSelectorMediaType;
appInfo?: ReceiverSelectorAppInfo; appInfo?: ReceiverSelectorAppInfo;
@@ -100,7 +100,7 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
await browser.windows.remove(this.windowId); await browser.windows.remove(this.windowId);
} }
this.receiverDevices = opts.receiverDevices; this.devices = opts.devices;
this.defaultMediaType = opts.defaultMediaType; this.defaultMediaType = opts.defaultMediaType;
this.availableMediaTypes = opts.availableMediaTypes; this.availableMediaTypes = opts.availableMediaTypes;
@@ -149,13 +149,11 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
} }
/** Updates receiver devices displayed in the receiver selector. */ /** Updates receiver devices displayed in the receiver selector. */
public update(receiverDevices: ReceiverDevice[]) { public update(devices: ReceiverDevice[], connectedSessionIds: string[]) {
this.receiverDevices = receiverDevices; this.devices = devices;
this.messagePort?.postMessage({ this.messagePort?.postMessage({
subject: "popup:update", subject: "popup:update",
data: { data: { devices, connectedSessionIds }
receiverDevices: this.receiverDevices
}
}); });
} }
@@ -191,7 +189,7 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
}); });
if ( if (
this.receiverDevices === undefined || this.devices === undefined ||
this.defaultMediaType === undefined || this.defaultMediaType === undefined ||
this.availableMediaTypes === undefined this.availableMediaTypes === undefined
) { ) {
@@ -214,7 +212,7 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
this.messagePort.postMessage({ this.messagePort.postMessage({
subject: "popup:update", subject: "popup:update",
data: { data: {
receiverDevices: this.receiverDevices, devices: this.devices,
defaultMediaType: this.defaultMediaType, defaultMediaType: this.defaultMediaType,
availableMediaTypes: this.availableMediaTypes availableMediaTypes: this.availableMediaTypes
} }

View File

@@ -108,10 +108,10 @@ function isSameContext(ctx1?: ContentContext, ctx2?: ContentContext) {
let baseConfig: BaseConfig; let baseConfig: BaseConfig;
let receiverSelector: Optional<ReceiverSelector>; let receiverSelector: Optional<ReceiverSelector>;
const activeInstances = new Set<CastInstance>();
/** Keeps track of cast API instances and provides bridge messaging. */ /** Keeps track of cast API instances and provides bridge messaging. */
const castManager = new (class { const castManager = new (class {
private activeInstances = new Set<CastInstance>();
async init() { async init() {
// Handle incoming instance connections // Handle incoming instance connections
messaging.onConnect.addListener(async port => { messaging.onConnect.addListener(async port => {
@@ -127,7 +127,7 @@ const castManager = new (class {
const updateReceiverAvailability = () => { const updateReceiverAvailability = () => {
const isAvailable = deviceManager.getDevices().length > 0; const isAvailable = deviceManager.getDevices().length > 0;
for (const instance of this.activeInstances) { for (const instance of activeInstances) {
instance.contentPort.postMessage({ instance.contentPort.postMessage({
subject: "cast:receiverAvailabilityUpdated", subject: "cast:receiverAvailabilityUpdated",
data: { isAvailable } data: { isAvailable }
@@ -146,7 +146,7 @@ const castManager = new (class {
* Finds a cast instance at the given tab (and optionally frame) ID. * Finds a cast instance at the given tab (and optionally frame) ID.
*/ */
getInstanceAt(tabId: number, frameId?: number) { getInstanceAt(tabId: number, frameId?: number) {
for (const instance of this.activeInstances) { for (const instance of activeInstances) {
if (instance.contentContext?.tabId === tabId) { if (instance.contentContext?.tabId === tabId) {
// If frame ID doesn't match go to next instance // If frame ID doesn't match go to next instance
if (frameId && instance.contentContext.frameId !== frameId) { if (frameId && instance.contentContext.frameId !== frameId) {
@@ -159,7 +159,7 @@ const castManager = new (class {
} }
getInstanceByDeviceId(deviceId: string) { getInstanceByDeviceId(deviceId: string) {
for (const instance of this.activeInstances) { for (const instance of activeInstances) {
if (instance.session?.deviceId === deviceId) return instance; if (instance.session?.deviceId === deviceId) return instance;
} }
} }
@@ -177,7 +177,7 @@ const castManager = new (class {
? this.createInstanceFromBackground(port, contentContext) ? this.createInstanceFromBackground(port, contentContext)
: this.createInstanceFromContent(port, isTrusted)); : this.createInstanceFromContent(port, isTrusted));
this.activeInstances.add(instance); activeInstances.add(instance);
instance.contentPort.postMessage({ instance.contentPort.postMessage({
subject: "cast:instanceCreated", subject: "cast:instanceCreated",
@@ -201,10 +201,10 @@ const castManager = new (class {
// Ensure only one instance per context // Ensure only one instance per context
if (contentContext) { if (contentContext) {
for (const instance of this.activeInstances) { for (const instance of activeInstances) {
if (isSameContext(instance.contentContext, contentContext)) { if (isSameContext(instance.contentContext, contentContext)) {
instance.bridgePort.disconnect(); instance.bridgePort.disconnect();
this.activeInstances.delete(instance); activeInstances.delete(instance);
break; break;
} }
} }
@@ -212,7 +212,7 @@ const castManager = new (class {
instance.bridgePort.onDisconnect.addListener(() => { instance.bridgePort.onDisconnect.addListener(() => {
contentPort.close(); contentPort.close();
this.activeInstances.delete(instance); activeInstances.delete(instance);
}); });
// bridge -> cast instance // bridge -> cast instance
@@ -246,7 +246,7 @@ const castManager = new (class {
} }
// Ensure only one instance per context // Ensure only one instance per context
for (const instance of this.activeInstances) { for (const instance of activeInstances) {
if ( if (
isSameContext( isSameContext(
instance.contentContext, instance.contentContext,
@@ -277,7 +277,7 @@ const castManager = new (class {
instance.bridgePort.disconnect(); instance.bridgePort.disconnect();
contentPort.disconnect(); contentPort.disconnect();
this.activeInstances.delete(instance); activeInstances.delete(instance);
}; };
instance.bridgePort.onDisconnect.addListener(onDisconnect); instance.bridgePort.onDisconnect.addListener(onDisconnect);
@@ -433,7 +433,7 @@ const castManager = new (class {
subject: "bridge:createCastSession", subject: "bridge:createCastSession",
data: { data: {
appId: sessionRequest.appId, appId: sessionRequest.appId,
receiverDevice: selection.receiverDevice receiverDevice: selection.device
} }
}); });
} catch (err) { } catch (err) {
@@ -498,7 +498,7 @@ const castManager = new (class {
instance.contentPort.postMessage({ instance.contentPort.postMessage({
subject: "cast:receiverAction", subject: "cast:receiverAction",
data: { data: {
receiver: createReceiver(selection.receiverDevice), receiver: createReceiver(selection.device),
action: ReceiverAction.CAST action: ReceiverAction.CAST
} }
}); });
@@ -507,7 +507,7 @@ const castManager = new (class {
subject: "bridge:createCastSession", subject: "bridge:createCastSession",
data: { data: {
appId: instance.apiConfig?.sessionRequest.appId, 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, { await browser.tabs.executeScript(contentContext.tabId, {
code: stringify` code: stringify`
window.mirroringMediaType = ${selection.mediaType}; window.mirroringMediaType = ${selection.mediaType};
window.receiverDevice = ${selection.receiverDevice}; window.receiverDevice = ${selection.device};
window.contextTabId = ${contentContext.tabId}; window.contextTabId = ${contentContext.tabId};
`, `,
frameId: contentContext.frameId frameId: contentContext.frameId
@@ -695,7 +695,7 @@ async function getReceiverSelection(selectionOpts: {
); );
receiverSelector.open({ receiverSelector.open({
receiverDevices: deviceManager.getDevices(), devices: deviceManager.getDevices(),
defaultMediaType, defaultMediaType,
availableMediaTypes, availableMediaTypes,
appInfo, appInfo,
@@ -751,7 +751,16 @@ function createSelector() {
selector.addEventListener("mediaMessage", onMediaMessage); selector.addEventListener("mediaMessage", onMediaMessage);
// Update selector data whenever devices change/update // 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("deviceUp", onDeviceChange);
deviceManager.addEventListener("deviceDown", onDeviceChange); deviceManager.addEventListener("deviceDown", onDeviceChange);

View File

@@ -16,6 +16,7 @@ export interface Options {
mirroringAppId: string; mirroringAppId: string;
receiverSelectorCloseIfFocusLost: boolean; receiverSelectorCloseIfFocusLost: boolean;
receiverSelectorWaitForConnection: boolean; receiverSelectorWaitForConnection: boolean;
receiverSelectorExpandActive: boolean;
siteWhitelistEnabled: boolean; siteWhitelistEnabled: boolean;
siteWhitelist: WhitelistItemData[]; siteWhitelist: WhitelistItemData[];
siteWhitelistCustomUserAgent: string; siteWhitelistCustomUserAgent: string;
@@ -40,6 +41,7 @@ export default {
mirroringAppId: MIRRORING_APP_ID, mirroringAppId: MIRRORING_APP_ID,
receiverSelectorCloseIfFocusLost: true, receiverSelectorCloseIfFocusLost: true,
receiverSelectorWaitForConnection: true, receiverSelectorWaitForConnection: true,
receiverSelectorExpandActive: true,
siteWhitelistEnabled: true, siteWhitelistEnabled: true,
siteWhitelist: [{ pattern: "https://www.netflix.com/*", isEnabled: true }], siteWhitelist: [{ pattern: "https://www.netflix.com/*", isEnabled: true }],
siteWhitelistCustomUserAgent: "", siteWhitelistCustomUserAgent: "",

View File

@@ -49,7 +49,8 @@ type ExtMessageDefinitions = {
}; };
/** Updates selector popup with new data. */ /** Updates selector popup with new data. */
"popup:update": { "popup:update": {
receiverDevices: ReceiverDevice[]; devices: ReceiverDevice[];
connectedSessionIds?: string[];
defaultMediaType?: ReceiverSelectorMediaType; defaultMediaType?: ReceiverSelectorMediaType;
availableMediaTypes?: ReceiverSelectorMediaType; availableMediaTypes?: ReceiverSelectorMediaType;
}; };

View File

@@ -259,6 +259,22 @@
{_("optionsReceiverSelectorCloseIfFocusLost")} {_("optionsReceiverSelectorCloseIfFocusLost")}
</label> </label>
</div> </div>
<div class="option option--inline">
<div class="option__control">
<input
id="receiverSelectorExpandActive"
type="checkbox"
bind:checked={opts.receiverSelectorExpandActive}
/>
</div>
<label
class="option__label"
for="receiverSelectorExpandActive"
>
{_("optionsReceiverSelectorExpandActive")}
</label>
</div>
</fieldset> </fieldset>
{/if} {/if}

View File

@@ -29,6 +29,8 @@
/** Devices to display. */ /** Devices to display. */
let devices: ReceiverDevice[] = []; let devices: ReceiverDevice[] = [];
/** IDs of sessions connected by this extension. */
let connectedSessionIds: string[] = [];
/** Sender app info (if available). */ /** Sender app info (if available). */
let appInfo: Optional<ReceiverSelectorAppInfo>; let appInfo: Optional<ReceiverSelectorAppInfo>;
@@ -172,6 +174,8 @@
break; break;
case "popup:update": { case "popup:update": {
updateKnownApp();
if ( if (
message.data.availableMediaTypes !== undefined && message.data.availableMediaTypes !== undefined &&
message.data.defaultMediaType !== 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; break;
} }
@@ -288,30 +294,27 @@
} }
} }
function onReceiverCast(receiverDevice: ReceiverDevice) { function onReceiverCast(device: ReceiverDevice) {
isConnecting = true; isConnecting = true;
port?.postMessage({ port?.postMessage({
subject: "main:receiverSelected", subject: "main:receiverSelected",
data: { data: { device, mediaType }
receiverDevice,
mediaType
}
}); });
} }
function onReceiverStop(receiverDevice: ReceiverDevice) { function onReceiverStop(device: ReceiverDevice) {
port?.postMessage({ port?.postMessage({
subject: "main:sendReceiverMessage", subject: "main:sendReceiverMessage",
data: { data: {
deviceId: receiverDevice.id, deviceId: device.id,
message: { requestId: 0, type: "STOP" } message: { requestId: 0, type: "STOP" }
} }
}); });
port?.postMessage({ port?.postMessage({
subject: "main:receiverStopped", subject: "main:receiverStopped",
data: { deviceId: receiverDevice.id } data: { deviceId: device.id }
}); });
} }
</script> </script>
@@ -378,8 +381,10 @@
{:else} {:else}
{#each devices as device} {#each devices as device}
<Receiver <Receiver
{opts}
{port} {port}
{device} {device}
{connectedSessionIds}
{isMediaTypeAvailable} {isMediaTypeAvailable}
isAnyMediaTypeAvailable={availableMediaTypes !== isAnyMediaTypeAvailable={availableMediaTypes !==
ReceiverSelectorMediaType.None && ReceiverSelectorMediaType.None &&

View File

@@ -1,6 +1,8 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher, onMount } from "svelte"; import { createEventDispatcher, onMount } from "svelte";
import type { Options } from "../../lib/options";
import { ReceiverDevice, ReceiverDeviceCapabilities } from "../../types"; import { ReceiverDevice, ReceiverDeviceCapabilities } from "../../types";
import type { Port } from "../../messaging"; import type { Port } from "../../messaging";
@@ -33,8 +35,11 @@
/** Whether any media types are available for this receiver. */ /** Whether any media types are available for this receiver. */
export let isAnyMediaTypeAvailable: boolean; export let isAnyMediaTypeAvailable: boolean;
/** Receiver device to display. */ /** Device to display. */
export let device: ReceiverDevice; export let device: ReceiverDevice;
export let connectedSessionIds: string[];
export let opts: Nullable<Options>;
/** Current receiver application (if available) */ /** Current receiver application (if available) */
$: application = device.status?.applications?.[0]; $: application = device.status?.applications?.[0];
@@ -79,8 +84,20 @@
/** Whether media controls are shown. */ /** Whether media controls are shown. */
let isExpanded = false; let isExpanded = false;
let isExpandedUserModified = false;
// Unexpand if media status disappears
$: if (!device.mediaStatus) { $: if (!device.mediaStatus) {
isExpanded = false; 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.. */ /** Whether a session request is in progress for this receiver.. */
@@ -429,6 +446,7 @@
disabled={!mediaStatus} disabled={!mediaStatus}
on:click={() => { on:click={() => {
isExpanded = !isExpanded; isExpanded = !isExpanded;
isExpandedUserModified = true;
}} }}
/> />