More receiver selector refactoring

This commit is contained in:
hensm
2022-04-29 01:14:06 +01:00
parent 70ac18511a
commit b1100bd258
15 changed files with 123 additions and 145 deletions

View File

@@ -1,35 +1,45 @@
"use strict";
import logger from "../../lib/logger";
import messaging, { Port, Message } from "../../messaging";
import options from "../../lib/options";
import { TypedEventTarget } from "../../lib/TypedEventTarget";
import { ReceiverDevice } from "../../types";
import { SessionRequest } from "../../cast/sdk/classes";
import logger from "../lib/logger";
import messaging, { Port, Message } from "../messaging";
import options from "../lib/options";
import { TypedEventTarget } from "../lib/TypedEventTarget";
import { SessionRequest } from "../cast/sdk/classes";
import {
ReceiverSelectionCast,
ReceiverSelectionStop,
ReceiverDevice,
ReceiverSelectionActionType,
ReceiverSelectorMediaType
} from "./index";
} from "../types";
const POPUP_URL = browser.runtime.getURL("ui/popup/index.html");
export interface PageInfo {
/** Info about sender page context. */
export interface ReceiverSelectorPageInfo {
url: string;
tabId: number;
frameId: number;
sessionRequest?: SessionRequest;
}
export interface ReceiverSelectionCast {
actionType: ReceiverSelectionActionType.Cast;
receiverDevice: ReceiverDevice;
mediaType: ReceiverSelectorMediaType;
filePath?: string;
}
export interface ReceiverSelectionStop {
actionType: ReceiverSelectionActionType.Stop;
receiverDevice: ReceiverDevice;
}
export type ReceiverSelection = ReceiverSelectionCast | ReceiverSelectionStop;
interface ReceiverSelectorEvents {
selected: ReceiverSelectionCast;
error: string;
cancelled: void;
stop: ReceiverSelectionStop;
}
/**
* Manages the receiver selector popup window and communication with the
* extension page hosted within.
@@ -38,7 +48,7 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
/** Popup window ID. */
private windowId?: number;
/** Message port to extension page within popup window. */
/** Message port to extension page. */
private messagePort?: Port;
private messagePortDisconnected?: boolean;
@@ -50,9 +60,7 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
private wasReceiverSelected = false;
private appId?: string;
private pageInfo?: PageInfo;
#isOpen = false;
private pageInfo?: ReceiverSelectorPageInfo;
constructor() {
super();
@@ -63,6 +71,7 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
this.onWindowsFocusChanged = this.onWindowsFocusChanged.bind(this);
browser.windows.onRemoved.addListener(this.onWindowsRemoved);
browser.windows.onFocusChanged.addListener(this.onWindowsFocusChanged);
/**
* Handle incoming message channel connection from popup
@@ -73,7 +82,7 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
/** Is receiver selector window currently open. */
get isOpen() {
return this.#isOpen;
return this.windowId !== undefined;
}
/**
@@ -84,7 +93,7 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
defaultMediaType: ReceiverSelectorMediaType;
availableMediaTypes: ReceiverSelectorMediaType;
appId?: string;
pageInfo?: PageInfo;
pageInfo?: ReceiverSelectorPageInfo;
}) {
this.appId = opts.appId;
this.pageInfo = opts.pageInfo;
@@ -139,15 +148,7 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
...popupSizePosition
});
this.#isOpen = true;
this.windowId = popup.id;
// Add focus listener
if (await options.get("receiverSelectorCloseIfFocusLost")) {
browser.windows.onFocusChanged.addListener(
this.onWindowsFocusChanged
);
}
}
/** Updates receiver devices displayed in the receiver selector. */
@@ -258,8 +259,6 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
return;
}
this.#isOpen = false;
browser.windows.onRemoved.removeListener(this.onWindowsRemoved);
browser.windows.onFocusChanged.removeListener(
this.onWindowsFocusChanged
@@ -279,21 +278,18 @@ export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorE
}
/**
* Closes popup window if another browser window is brought
* into focus. Doesn't apply if no window is focused
* `WINDOW_ID_NONE` or if the popup window is re-focused.
* Closes popup window if another browser window is brought into
* focus. Doesn't apply if no window is focused `WINDOW_ID_NONE`
* or if the popup window is re-focused.
*/
private onWindowsFocusChanged(windowId: number) {
private async onWindowsFocusChanged(windowId: number) {
if (!this.windowId) return;
if (
windowId !== browser.windows.WINDOW_ID_NONE &&
windowId !== this.windowId
) {
// Only run once
browser.windows.onFocusChanged.removeListener(
this.onWindowsFocusChanged
);
if (this.windowId) {
if (await options.get("receiverSelectorCloseIfFocusLost")) {
browser.windows.remove(this.windowId);
}
}

View File

@@ -5,9 +5,9 @@ import logger from "../lib/logger";
import options from "../lib/options";
import bridge, { BridgeInfo } from "../lib/bridge";
import CastManager from "../cast/CastManager";
import receiverDevices from "./receiverDevices";
import ReceiverSelectorManager from "./receiverSelector/ReceiverSelectorManager";
import castManager from "./castManager";
import deviceManager from "./deviceManager";
import selectorManager from "./selectorManager";
import { initMenus } from "./menus";
import { initWhitelist } from "./whitelist";
@@ -101,8 +101,8 @@ async function init() {
await notifyBridgeCompat();
await receiverDevices.init();
await CastManager.init();
await deviceManager.init();
await castManager.init();
await initMenus();
await initWhitelist();
@@ -119,9 +119,9 @@ async function init() {
return;
}
const selection = await ReceiverSelectorManager.getSelection(tab.id);
const selection = await selectorManager.getSelection(tab.id);
if (selection) {
CastManager.loadSender({
castManager.loadSender({
tabId: tab.id,
frameId: 0,
selection

View File

@@ -0,0 +1,380 @@
"use strict";
import bridge from "../lib/bridge";
import logger from "../lib/logger";
import messaging, { Message, Port } from "../messaging";
import options from "../lib/options";
import { stringify } from "../lib/utils";
import {
ReceiverSelectionActionType,
ReceiverSelectorMediaType
} from "../types";
import { ReceiverSelection } from "./ReceiverSelector";
import deviceManager from "./deviceManager";
import selectorManager from "./selectorManager";
type AnyPort = Port | MessagePort;
export interface CastInstance {
bridgePort: Port;
contentPort: AnyPort;
contentTabId?: number;
contentFrameId?: number;
appId?: string;
}
/** Keeps track of cast API instances and provides bridge messaging. */
export default new (class {
private activeInstances = new Set<CastInstance>();
public async init() {
// Handle incoming instance connections
messaging.onConnect.addListener(async port => {
if (port.name === "cast") {
this.createInstance(port);
}
});
// Forward receiver eventes to cast instances
deviceManager.addEventListener("receiverDeviceUp", ev => {
for (const instance of this.activeInstances) {
instance.contentPort.postMessage({
subject: "cast:receiverDeviceUp",
data: { receiverDevice: ev.detail.deviceInfo }
});
}
});
deviceManager.addEventListener("receiverDeviceDown", ev => {
for (const instance of this.activeInstances) {
instance.contentPort.postMessage({
subject: "cast:receiverDeviceDown",
data: { receiverDeviceId: ev.detail.deviceId }
});
}
});
}
/**
* Finds a cast instance at the given tab (and optionally frame) ID.
*/
public getInstance(tabId: number, frameId?: number) {
for (const instance of this.activeInstances) {
if (instance.contentTabId === tabId) {
// If frame ID doesn't match go to next instance
if (frameId && instance.contentFrameId !== frameId) {
continue;
}
return instance;
}
}
}
/**
* Creates a cast instance with a given port and connects messaging
* correctly depending on the type of port.
*/
public async createInstance(port: AnyPort) {
const instance = await (port instanceof MessagePort
? this.createInstanceFromBackground(port)
: this.createInstanceFromContent(port));
this.activeInstances.add(instance);
instance.contentPort.postMessage({
subject: "cast:initialized",
data: await bridge.getInfo()
});
return instance;
}
/** Creates a cast instance with a `MessagePort` content port. */
private async createInstanceFromBackground(
contentPort: MessagePort
): Promise<CastInstance> {
const instance: CastInstance = {
bridgePort: await bridge.connect(),
contentPort
};
instance.bridgePort.onDisconnect.addListener(() => {
contentPort.close();
this.activeInstances.delete(instance);
});
// bridge -> content
instance.bridgePort.onMessage.addListener(message => {
contentPort.postMessage(message);
});
// content -> (any)
contentPort.addEventListener("message", ev => {
this.handleContentMessage(instance, ev.data);
});
return instance;
}
/**
* Creates a cast instance with a WebExtension `Port` content port.
*/
private async createInstanceFromContent(
contentPort: Port
): Promise<CastInstance> {
if (
contentPort.sender?.tab?.id === undefined ||
contentPort.sender?.frameId === undefined
) {
throw logger.error(
"Cast instance created from content with an invalid port context."
);
}
/**
* If there's already an active instance for the sender
* tab/frame ID, disconnect it.
*
* TODO: Fix this behaviour!
*/
for (const instance of this.activeInstances) {
if (
instance.contentTabId === contentPort.sender.tab.id &&
instance.contentFrameId === contentPort.sender.frameId
) {
instance.bridgePort.disconnect();
}
}
const instance: CastInstance = {
bridgePort: await bridge.connect(),
contentPort,
contentTabId: contentPort.sender.tab.id,
contentFrameId: contentPort.sender.frameId
};
// content -> (any)
const onContentPortMessage = (message: Message) => {
this.handleContentMessage(instance, message);
};
// bridge -> content
const onBridgePortMessage = (message: Message) => {
contentPort.postMessage(message);
};
const onDisconnect = () => {
instance.bridgePort.onMessage.removeListener(onBridgePortMessage);
contentPort.onMessage.removeListener(onContentPortMessage);
instance.bridgePort.disconnect();
contentPort.disconnect();
this.activeInstances.delete(instance);
};
instance.bridgePort.onDisconnect.addListener(onDisconnect);
instance.bridgePort.onMessage.addListener(onBridgePortMessage);
contentPort.onDisconnect.addListener(onDisconnect);
contentPort.onMessage.addListener(onContentPortMessage);
return instance;
}
/**
* 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
) {
const [destination] = message.subject.split(":");
if (destination === "bridge") {
instance.bridgePort.postMessage(message);
}
switch (message.subject) {
// Cast API has been initialized
case "main:initializeCast": {
instance.appId = message.data.appId;
for (const receiverDevice of deviceManager.getDevices()) {
instance.contentPort.postMessage({
subject: "cast:receiverDeviceUp",
data: { receiverDevice }
});
}
break;
}
// User has triggered receiver selection via the cast API
case "main:selectReceiver": {
if (
instance.contentTabId === undefined ||
instance.contentFrameId === undefined
) {
throw logger.error(
"Cast instance associated with content sender missing tab/frame ID"
);
}
try {
const selection = await selectorManager.getSelection(
instance.contentTabId,
instance.contentFrameId,
{ sessionRequest: message.data.sessionRequest }
);
// Handle cancellation
if (!selection) {
instance.contentPort.postMessage({
subject: "cast:selectReceiver/cancelled"
});
break;
}
switch (selection.actionType) {
case ReceiverSelectionActionType.Cast: {
/**
* 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:selectReceiver/cancelled"
});
this.loadSender({
tabId: instance.contentTabId,
frameId: instance.contentFrameId,
selection
});
break;
}
instance.contentPort.postMessage({
subject: "cast:selectReceiver/selected",
data: selection
});
break;
}
case ReceiverSelectionActionType.Stop: {
instance.contentPort.postMessage({
subject: "cast:selectReceiver/stopped",
data: selection
});
break;
}
}
} catch (err) {
// TODO: Report errors properly
instance.contentPort.postMessage({
subject: "cast:selectReceiver/cancelled"
});
}
break;
}
/**
* TODO: If we're closing a selector, make sure it's the
* same one that caused the session creation.
*/
case "main:closeReceiverSelector": {
const selector = await selectorManager.getSelector();
const shouldClose = await options.get(
"receiverSelectorWaitForConnection"
);
if (selector.isOpen && shouldClose) {
selector.close();
}
break;
}
}
}
/**
* Loads the appropriate sender for a given receiver selector
* response.
*/
public async loadSender(opts: {
tabId: number;
frameId?: number;
selection: ReceiverSelection;
}) {
// Cancelled
if (!opts.selection) {
return;
}
if (opts.selection.actionType !== ReceiverSelectionActionType.Cast) {
return;
}
switch (opts.selection.mediaType) {
case ReceiverSelectorMediaType.App: {
const instance = this.getInstance(opts.tabId, opts.frameId);
if (!instance) {
throw logger.error(
`Cast instance not found at tabId ${opts.tabId} / frameId ${opts.frameId}`
);
}
instance.contentPort.postMessage({
subject: "cast:launchApp",
data: { receiverDevice: opts.selection.receiverDevice }
});
break;
}
case ReceiverSelectorMediaType.Tab:
case ReceiverSelectorMediaType.Screen: {
await browser.tabs.executeScript(opts.tabId, {
code: stringify`
window.selectedMedia = ${opts.selection.mediaType};
window.selectedReceiver = ${opts.selection.receiverDevice};
`,
frameId: opts.frameId
});
await browser.tabs.executeScript(opts.tabId, {
file: "cast/senders/mirroring.js",
frameId: opts.frameId
});
break;
}
case ReceiverSelectorMediaType.File: {
const fileUrl = new URL(`file://${opts.selection.filePath}`);
const { init } = await import("../cast/senders/media");
init({
mediaUrl: fileUrl.href,
receiver: opts.selection.receiverDevice
});
break;
}
}
}
})();

View File

@@ -2,17 +2,17 @@
import logger from "../lib/logger";
import options from "../lib/options";
import { stringify } from "../lib/utils";
import {
ReceiverSelection,
ReceiverSelectionActionType,
ReceiverSelectorMediaType
} from "./receiverSelector";
} from "../types";
import ReceiverSelectorManager from "./receiverSelector/ReceiverSelectorManager";
import CastManager from "../cast/CastManager";
import { ReceiverSelection } from "./ReceiverSelector";
import selectorManager from "./selectorManager";
import castManager from "./castManager";
const _ = browser.i18n.getMessage;
@@ -142,7 +142,7 @@ async function onMenuClicked(
let selection: Nullable<ReceiverSelection> = null;
try {
selection = await ReceiverSelectorManager.getSelection(
selection = await selectorManager.getSelection(
tab.id,
info.frameId,
{ withMediaSender: castMediaMenuClicked }
@@ -160,7 +160,7 @@ async function onMenuClicked(
}
if (castMenuClicked) {
CastManager.loadSender({
castManager.loadSender({
tabId: tab.id,
frameId: info.frameId,
selection
@@ -187,7 +187,7 @@ async function onMenuClicked(
});
} else {
// Handle other responses
CastManager.loadSender({
castManager.loadSender({
tabId: tab.id,
frameId: info.frameId,
selection

View File

@@ -1,29 +0,0 @@
"use strict";
import { ReceiverDevice } from "../../types";
export enum ReceiverSelectorMediaType {
None = 0,
App = 1,
Tab = 2,
Screen = 4,
File = 8
}
export enum ReceiverSelectionActionType {
Cast = 1,
Stop = 2
}
export interface ReceiverSelectionCast {
actionType: ReceiverSelectionActionType.Cast;
receiverDevice: ReceiverDevice;
mediaType: ReceiverSelectorMediaType;
filePath?: string;
}
export interface ReceiverSelectionStop {
actionType: ReceiverSelectionActionType.Stop;
receiverDevice: ReceiverDevice;
}
export type ReceiverSelection = ReceiverSelectionCast | ReceiverSelectionStop;

View File

@@ -1,23 +1,24 @@
"use strict";
import options from "../../lib/options";
import logger from "../../lib/logger";
import options from "../lib/options";
import logger from "../lib/logger";
import CastManager from "../../cast/CastManager";
import { SessionRequest } from "../../cast/sdk/classes";
import receiverDevices from "../receiverDevices";
import { getMediaTypesForPageUrl } from "../lib/utils";
import { SessionRequest } from "../cast/sdk/classes";
import { getMediaTypesForPageUrl } from "../../lib/utils";
import castManager from "./castManager";
import deviceManager from "./deviceManager";
import ReceiverSelector, {
ReceiverSelection,
ReceiverSelectionCast,
ReceiverSelectionStop
} from "./ReceiverSelector";
import {
ReceiverSelection,
ReceiverSelectionActionType,
ReceiverSelectionCast,
ReceiverSelectionStop,
ReceiverSelectorMediaType
} from "./index";
import ReceiverSelector from "./ReceiverSelector";
} from "../types";
let sharedSelector: ReceiverSelector;
async function getSelector() {
@@ -46,12 +47,12 @@ async function getSelection(
contextTabId: number,
contextFrameId = 0,
selectionOpts?: {
sessionRequest: SessionRequest;
sessionRequest?: SessionRequest;
withMediaSender?: boolean;
}
): Promise<ReceiverSelection | null> {
return new Promise(async (resolve, reject) => {
let castInstance = CastManager.getInstance(
let castInstance = castManager.getInstance(
contextTabId,
contextFrameId
);
@@ -112,15 +113,12 @@ async function getSelection(
sharedSelector = new ReceiverSelector();
function onReceiverChange() {
sharedSelector.update(receiverDevices.getDevices());
sharedSelector.update(deviceManager.getDevices());
}
receiverDevices.addEventListener("receiverDeviceUp", onReceiverChange);
receiverDevices.addEventListener(
"receiverDeviceDown",
onReceiverChange
);
receiverDevices.addEventListener(
deviceManager.addEventListener("receiverDeviceUp", onReceiverChange);
deviceManager.addEventListener("receiverDeviceDown", onReceiverChange);
deviceManager.addEventListener(
"receiverDeviceUpdated",
onReceiverChange
);
@@ -139,7 +137,7 @@ async function getSelection(
function onSelectorStop(ev: CustomEvent<ReceiverSelectionStop>) {
logger.info("Stopping receiver app...", ev.detail);
receiverDevices.stopReceiverApp(ev.detail.receiverDevice.id);
deviceManager.stopReceiverApp(ev.detail.receiverDevice.id);
removeListeners();
resolve({
@@ -172,25 +170,25 @@ async function getSelection(
);
sharedSelector.removeEventListener("error", onSelectorError);
receiverDevices.removeEventListener(
deviceManager.removeEventListener(
"receiverDeviceUp",
onReceiverChange
);
receiverDevices.removeEventListener(
deviceManager.removeEventListener(
"receiverDeviceDown",
onReceiverChange
);
receiverDevices.removeEventListener(
deviceManager.removeEventListener(
"receiverDeviceUpdated",
onReceiverChange
);
}
// Ensure status manager is initialized
await receiverDevices.init();
await deviceManager.init();
sharedSelector.open({
receiverDevices: receiverDevices.getDevices(),
receiverDevices: deviceManager.getDevices(),
defaultMediaType,
availableMediaTypes,
appId: castInstance?.appId,