mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-11 10:09:59 +00:00
Restructure background script (#70)
Splits some background script functionality into separate modules: - Receiver selector handling is moved to ./SelectorManager. - Status bridge handling is moved to ./StatusManager. - Menu creation and updates are handled in ./createMenus. - Shim creation is handled in ./createShim. TypedEventTarget allows EventTarget-derived classes to export typed events. Options type definition is moved to ./lib/options, module assumes more responsibility for update handling and provides a "changed" event. Private cast._requestSession method allows bypassing receiver selector.
This commit is contained in:
@@ -6,6 +6,23 @@ import nativeMessaging from "./nativeMessaging";
|
||||
import options from "./options";
|
||||
|
||||
|
||||
async function connect (): Promise<browser.runtime.Port> {
|
||||
const applicationName = await options.get("bridgeApplicationName");
|
||||
const bridgePort = nativeMessaging.connectNative(applicationName);
|
||||
|
||||
bridgePort.onDisconnect.addListener(() => {
|
||||
if (bridgePort.error) {
|
||||
console.error(`${applicationName} disconnected:`
|
||||
, this.bridgePort.error.message);
|
||||
} else {
|
||||
console.info(`${applicationName} disconnected`);
|
||||
}
|
||||
});
|
||||
|
||||
return bridgePort;
|
||||
}
|
||||
|
||||
|
||||
export interface BridgeInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
@@ -16,7 +33,7 @@ export interface BridgeInfo {
|
||||
isVersionNewer: boolean;
|
||||
}
|
||||
|
||||
export default async function getBridgeInfo (): Promise<BridgeInfo> {
|
||||
async function getInfo (): Promise<BridgeInfo> {
|
||||
const applicationName = await options.get("bridgeApplicationName");
|
||||
let applicationVersion: string;
|
||||
|
||||
@@ -65,3 +82,9 @@ export default async function getBridgeInfo (): Promise<BridgeInfo> {
|
||||
, isVersionNewer
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
connect
|
||||
, getInfo
|
||||
};
|
||||
45
ext/src/lib/endpoints.ts
Normal file
45
ext/src/lib/endpoints.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Cast sender API loader script URL.
|
||||
*
|
||||
* Since the actual cast sender API script is hosted locally
|
||||
* within Chrome, this script just acts a loader script for
|
||||
* the real script whilst also doing some UA string
|
||||
* checking.
|
||||
*/
|
||||
export const CAST_LOADER_SCRIPT_URL =
|
||||
"https://www.gstatic.com/cv/js/sender/v1/cast_sender.js";
|
||||
|
||||
/**
|
||||
* Framework API loader script URL.
|
||||
*
|
||||
* Same URL as the usual loader script, but the additional
|
||||
* search parameter is checked from within the script and
|
||||
* the framework API script is conditionally loaded in
|
||||
* addition to the regular API script.
|
||||
*/
|
||||
export const CAST_FRAMEWORK_LOADER_SCRIPT_URL =
|
||||
`${CAST_LOADER_SCRIPT_URL}?loadCastFramework=1`;
|
||||
|
||||
/**
|
||||
* Cast API script URLs.
|
||||
*
|
||||
* Cast functionality in Chrome was previously provided by
|
||||
* an extension. The cast API script is still provided via
|
||||
* chrome-extension URLs for compatibility reasons.
|
||||
*/
|
||||
export const CAST_SCRIPT_URLS = [
|
||||
"chrome-extension://pkedcjkdefgpdelpbcmbmeomcjbeemfm/cast_sender.js"
|
||||
, "chrome-extension://enhhojjnijigcajfphajepfemndkmdlo/cast_sender.js"
|
||||
];
|
||||
|
||||
/**
|
||||
* Framework API script URL.
|
||||
*
|
||||
* Unlike the basic cast sender API, the framework API is
|
||||
* not hosted locally within Chrome and is the only script
|
||||
* fetched directly from Google servers.
|
||||
*/
|
||||
export const CAST_FRAMEWORK_SCRIPT_URL =
|
||||
"https://www.gstatic.com/cast/sdk/libs/sender/1.0/cast_framework.js";
|
||||
52
ext/src/lib/loadSender.ts
Normal file
52
ext/src/lib/loadSender.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
"use strict";
|
||||
|
||||
import { stringify } from "./utils";
|
||||
|
||||
import { ReceiverSelection
|
||||
, ReceiverSelectorMediaType } from "../receiver_selectors";
|
||||
|
||||
|
||||
interface LoadSenderOptions {
|
||||
tabId: number;
|
||||
frameId: number;
|
||||
selection: ReceiverSelection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the appropriate sender for a given receiver
|
||||
* selector response.
|
||||
*/
|
||||
export default async function loadSender (opts: LoadSenderOptions) {
|
||||
// Cancelled
|
||||
if (!opts.selection) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (opts.selection.mediaType) {
|
||||
case ReceiverSelectorMediaType.Tab:
|
||||
case ReceiverSelectorMediaType.Screen: {
|
||||
await browser.tabs.executeScript(opts.tabId, {
|
||||
code: stringify`
|
||||
window.selectedMedia = ${opts.selection.mediaType};
|
||||
window.selectedReceiver = ${opts.selection.receiver};
|
||||
`
|
||||
, frameId: opts.frameId
|
||||
});
|
||||
|
||||
await browser.tabs.executeScript(opts.tabId, {
|
||||
file: "senders/mirroringCast.js"
|
||||
, frameId: opts.frameId
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case ReceiverSelectorMediaType.File: {
|
||||
const fileUrl = new URL(`file://${opts.selection.filePath}`);
|
||||
const mediaSession = await mediaCasting.loadMediaUrl(
|
||||
fileUrl.href, opts.selection.receiver);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
81
ext/src/lib/mediaCasting.ts
Normal file
81
ext/src/lib/mediaCasting.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
"use strict";
|
||||
|
||||
import cast, { ensureInit } from "../shim/export";
|
||||
import options from "./options";
|
||||
|
||||
import { Receiver } from "../types";
|
||||
|
||||
|
||||
function getMediaSession (
|
||||
receiver?: Receiver): Promise<cast.Session> {
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
await ensureInit();
|
||||
|
||||
/**
|
||||
* If a receiver is available, call requestSession. If a
|
||||
* specific receiver was specified, bypass receiver selector
|
||||
* and create session directly.
|
||||
*/
|
||||
function receiverListener (availability: string) {
|
||||
if (availability === cast.ReceiverAvailability.AVAILABLE) {
|
||||
if (receiver) {
|
||||
cast._requestSession(receiver, resolve, reject);
|
||||
} else {
|
||||
cast.requestSession(resolve, reject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sessionRequest = new cast.SessionRequest(
|
||||
cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID);
|
||||
|
||||
const apiConfig = new cast.ApiConfig(
|
||||
sessionRequest
|
||||
, null // sessionListener
|
||||
, receiverListener); // receiverListener
|
||||
|
||||
cast.initialize(apiConfig);
|
||||
});
|
||||
}
|
||||
|
||||
function loadMediaUrl (
|
||||
mediaUrl: string
|
||||
, receiver: Receiver): Promise<cast.Session> {
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
|
||||
const isLocalMedia = mediaUrl.startsWith("file://");
|
||||
const isLocalMediaEnabled = await options.get("localMediaEnabled");
|
||||
|
||||
if (isLocalMedia && !isLocalMediaEnabled) {
|
||||
console.error("fx_cast (Debug): Local media casting not enabled");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const mediaUrlObject = new URL(mediaUrl);
|
||||
const mediaInfo = new cast.media.MediaInfo(mediaUrlObject.href, null);
|
||||
|
||||
mediaInfo.metadata = new cast.media.GenericMediaMetadata();
|
||||
mediaInfo.metadata.metadataType = cast.media.MetadataType.GENERIC;
|
||||
mediaInfo.metadata.title = mediaUrlObject.pathname;
|
||||
|
||||
|
||||
const mediaSession = await getMediaSession(receiver);
|
||||
|
||||
const loadRequest = new cast.media.LoadRequest(mediaInfo);
|
||||
loadRequest.autoplay = false;
|
||||
|
||||
mediaSession.loadMedia(loadRequest
|
||||
, null // successCallback
|
||||
, () => { reject(); }); // errorCallback
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
getMediaSession
|
||||
, loadMediaUrl
|
||||
};
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
import { Message } from "../types";
|
||||
|
||||
|
||||
interface Details {
|
||||
tabId: number;
|
||||
frameId: number;
|
||||
}
|
||||
|
||||
type SenderCallback = (message: Message, details: Details) => void;
|
||||
|
||||
|
||||
const routeMap = new Map<string, SenderCallback>();
|
||||
|
||||
function register (routeName: string, senderCallback: SenderCallback) {
|
||||
routeMap.set(routeName, senderCallback);
|
||||
}
|
||||
|
||||
function deregister (routeName: string) {
|
||||
routeMap.delete(routeName);
|
||||
}
|
||||
|
||||
function handleMessage (message: Message, details?: Details) {
|
||||
const destination = message.subject.split(":")[0];
|
||||
if (routeMap.has(destination)) {
|
||||
routeMap.get(destination)(message, details);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
register
|
||||
, deregister
|
||||
, handleMessage
|
||||
};
|
||||
@@ -1,79 +1,149 @@
|
||||
"use strict";
|
||||
|
||||
import defaultOptions, { Options } from "../defaultOptions";
|
||||
import defaultOptions from "../defaultOptions";
|
||||
|
||||
import { Message } from "../types";
|
||||
import { TypedEventTarget } from "./typedEvents";
|
||||
|
||||
|
||||
/**
|
||||
* Fetches `options` key from storage and returns it as
|
||||
* Options interface type.
|
||||
*/
|
||||
async function getAll (): Promise<Options> {
|
||||
const { options }: { options: Options } =
|
||||
await browser.storage.sync.get("options");
|
||||
export interface Options {
|
||||
bridgeApplicationName: string;
|
||||
mediaEnabled: boolean;
|
||||
mediaSyncElement: boolean;
|
||||
mediaStopOnUnload: boolean;
|
||||
localMediaEnabled: boolean;
|
||||
localMediaServerPort: number;
|
||||
mirroringEnabled: boolean;
|
||||
mirroringAppId: string;
|
||||
userAgentWhitelistEnabled: boolean;
|
||||
userAgentWhitelist: string[];
|
||||
|
||||
return options;
|
||||
[key: string]: Options[keyof Options];
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes Options object and sets to `options` storage key.
|
||||
* Returns storage promise.
|
||||
*/
|
||||
async function setAll (options: Options): Promise<void> {
|
||||
return browser.storage.sync.set({ options });
|
||||
|
||||
interface EventMap {
|
||||
"changed": Array<keyof Options>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets specific option from storage and returns it as its
|
||||
* type from Options interface type.
|
||||
*/
|
||||
async function get<T extends keyof Options> (name: T): Promise<Options[T]> {
|
||||
const options = await getAll();
|
||||
// tslint:disable-next-line:new-parens
|
||||
export default new class extends TypedEventTarget<EventMap> {
|
||||
constructor () {
|
||||
super();
|
||||
|
||||
if (options.hasOwnProperty(name)) {
|
||||
return options[name];
|
||||
browser.storage.onChanged.addListener((changes, areaName) => {
|
||||
if (areaName !== "sync") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Types issue
|
||||
const _changes = changes as {
|
||||
[key: string]: browser.storage.StorageChange
|
||||
};
|
||||
|
||||
if ("options" in _changes) {
|
||||
const { oldValue, newValue } = _changes.options;
|
||||
const changedKeys = [];
|
||||
|
||||
for (const key in newValue) {
|
||||
// Don't track added keys
|
||||
if (!(key in oldValue)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const oldKeyValue = oldValue[key];
|
||||
const newKeyValue = newValue[key];
|
||||
|
||||
// Equality comparison
|
||||
if (oldKeyValue === newKeyValue) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Array comparison
|
||||
if (oldKeyValue instanceof Array
|
||||
&& newKeyValue instanceof Array) {
|
||||
if (oldKeyValue.length === newKeyValue.length
|
||||
&& oldKeyValue.every((value, index) =>
|
||||
value === newKeyValue[index])) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
changedKeys.push(key);
|
||||
}
|
||||
|
||||
this.dispatchEvent(new CustomEvent("changed", {
|
||||
detail: changedKeys
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets specific option to storage. Returns storage
|
||||
* promise.
|
||||
*/
|
||||
async function set<T extends keyof Options> (
|
||||
name: T
|
||||
, value: Options[T]): Promise<void> {
|
||||
/**
|
||||
* Fetches `options` key from storage and returns it as
|
||||
* Options interface type.
|
||||
*/
|
||||
public async getAll (): Promise<Options> {
|
||||
const { options }: { options: Options } =
|
||||
await browser.storage.sync.get("options");
|
||||
|
||||
const options = await getAll();
|
||||
options[name] = value;
|
||||
return setAll(options);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes Options object and sets to `options` storage key.
|
||||
* Returns storage promise.
|
||||
*/
|
||||
public async setAll (options: Options): Promise<void> {
|
||||
return browser.storage.sync.set({ options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets existing options from storage and compares it
|
||||
* against defaults. Any options in defaults and not in
|
||||
* storage are set. Does not override any existing options.
|
||||
*/
|
||||
async function update (defaults = defaultOptions): Promise<void> {
|
||||
const oldOpts = await getAll();
|
||||
const newOpts: Partial<Options> = {};
|
||||
/**
|
||||
* Gets specific option from storage and returns it as its
|
||||
* type from Options interface type.
|
||||
*/
|
||||
public async get<T extends keyof Options> (name: T): Promise<Options[T]> {
|
||||
const options = await this.getAll();
|
||||
|
||||
// Find options not already in storage
|
||||
for (const [ optName, optVal ] of Object.entries(defaults)) {
|
||||
if (!oldOpts.hasOwnProperty(optName)) {
|
||||
newOpts[optName] = optVal;
|
||||
if (options.hasOwnProperty(name)) {
|
||||
return options[name];
|
||||
}
|
||||
}
|
||||
|
||||
// Update storage with default values of new options
|
||||
return setAll({
|
||||
...oldOpts
|
||||
, ...newOpts
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Sets specific option to storage. Returns storage
|
||||
* promise.
|
||||
*/
|
||||
public async set<T extends keyof Options> (
|
||||
name: T
|
||||
, value: Options[T]): Promise<void> {
|
||||
|
||||
const options = await this.getAll();
|
||||
options[name] = value;
|
||||
return this.setAll(options);
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
get, getAll
|
||||
, set, setAll
|
||||
, update
|
||||
/**
|
||||
* Gets existing options from storage and compares it
|
||||
* against defaults. Any options in defaults and not in
|
||||
* storage are set. Does not override any existing options.
|
||||
*/
|
||||
public async update (defaults = defaultOptions): Promise<void> {
|
||||
const oldOpts = await this.getAll();
|
||||
const newOpts: Partial<Options> = {};
|
||||
|
||||
// Find options not already in storage
|
||||
for (const [ optName, optVal ] of Object.entries(defaults)) {
|
||||
if (!oldOpts.hasOwnProperty(optName)) {
|
||||
newOpts[optName] = optVal;
|
||||
}
|
||||
}
|
||||
|
||||
// Update storage with default values of new options
|
||||
return this.setAll({
|
||||
...oldOpts
|
||||
, ...newOpts
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
21
ext/src/lib/typedEvents.ts
Normal file
21
ext/src/lib/typedEvents.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
"use strict";
|
||||
|
||||
export interface TypedEvents {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export class TypedEventTarget<T extends TypedEvents> extends EventTarget {
|
||||
public addEventListener<K extends keyof T> (
|
||||
type: K, listener: (ev: CustomEvent<T[K]>) => void): void {
|
||||
super.addEventListener(type as string, listener);
|
||||
}
|
||||
|
||||
public removeEventListener<K extends keyof T> (
|
||||
type: K, listener: (ev: CustomEvent<T[K]>) => void): void {
|
||||
super.removeEventListener(type as string, listener);
|
||||
}
|
||||
|
||||
public dispatchEvent<K extends keyof T> (ev: CustomEvent<T[K]>): boolean {
|
||||
return super.dispatchEvent(ev);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,28 @@ export function getNextEllipsis (ellipsis: string): string {
|
||||
/* tslint:enable:curly */
|
||||
}
|
||||
|
||||
/**
|
||||
* Template literal tag function, JSON-encodes substitutions.
|
||||
*/
|
||||
export function stringify (
|
||||
templateStrings: TemplateStringsArray
|
||||
, ...substitutions: any[]) {
|
||||
|
||||
let formattedString = "";
|
||||
|
||||
for (const templateString of templateStrings) {
|
||||
if (!formattedString) {
|
||||
formattedString += templateString;
|
||||
continue;
|
||||
}
|
||||
|
||||
formattedString += JSON.stringify(substitutions.shift());
|
||||
formattedString += templateString;
|
||||
}
|
||||
|
||||
return formattedString;
|
||||
}
|
||||
|
||||
|
||||
interface WindowCenteredProps {
|
||||
width: number;
|
||||
|
||||
Reference in New Issue
Block a user