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:
Matt Hensman
2019-07-26 00:09:51 +01:00
committed by GitHub
parent 2fe72ed24c
commit ba8c28bf39
40 changed files with 1751 additions and 1241 deletions

View File

@@ -1,73 +0,0 @@
"use strict";
import EventEmitter from "events";
import fs from "fs";
import http from "http";
import mime from "mime-types";
import { Message
, SendMessageCallback } from "./types";
export default class MediaServer extends EventEmitter {
private httpServer: http.Server;
constructor (
private filePath: string
, private port: number) {
super();
this.httpServer = http.createServer(this.requestListener.bind(this));
}
public start () {
this.httpServer.listen(this.port, () => {
this.emit("started");
});
}
public stop () {
if (this.httpServer && this.httpServer.listening) {
this.httpServer.close(() => {
this.emit("stopped");
});
}
}
private requestListener (
req: http.IncomingMessage
, res: http.ServerResponse) {
const { size: fileSize } = fs.statSync(this.filePath);
const { range } = req.headers;
const contentType = mime.lookup(this.filePath) || "video/mp4";
// Partial content HTTP 206
if (range) {
const bounds = range.substring(6).split("-");
const start = parseInt(bounds[0]);
const end = bounds[1] ? parseInt(bounds[1]) : fileSize - 1;
const chunkSize = (end - start) + 1;
res.writeHead(206, {
"Accept-Ranges": "bytes"
, "Content-Range": `bytes ${start}-${end}/${fileSize}`
, "Content-Length": chunkSize
, "Content-Type": contentType
});
fs.createReadStream(this.filePath, { start, end }).pipe(res);
} else {
res.writeHead(200, {
"Content-Length": fileSize
, "Content-Type": contentType
});
fs.createReadStream(this.filePath).pipe(res);
}
}
}

View File

@@ -8,7 +8,6 @@ import mime from "mime-types";
import path from "path"; import path from "path";
import Media from "./Media"; import Media from "./Media";
import MediaServer from "./MediaServer";
import Session from "./Session"; import Session from "./Session";
import StatusListener from "./StatusListener"; import StatusListener from "./StatusListener";
@@ -32,11 +31,11 @@ events.EventEmitter.defaultMaxListeners = 50;
const browser = new dnssd.Browser(dnssd.tcp("googlecast")); const browser = new dnssd.Browser(dnssd.tcp("googlecast"));
// Local media server // Local media server
let mediaServer: MediaServer; let mediaServer: http.Server;
process.on("SIGTERM", () => { process.on("SIGTERM", () => {
if (mediaServer) { if (mediaServer && mediaServer.listening) {
mediaServer.stop(); mediaServer.close();
} }
}); });
@@ -47,18 +46,28 @@ const encodeTransform = new EncodeTransform();
// stdin -> stdout // stdin -> stdout
process.stdin process.stdin
.pipe(decodeTransform) .pipe(decodeTransform)
.pipe(new ResponseTransform(handleMessage))
.pipe(encodeTransform) decodeTransform.on("data", handleMessage);
encodeTransform
.pipe(process.stdout); .pipe(process.stdout);
/** /**
* Encode and send a message to the extension. * Encode and send a message to the extension. If message is
* a string, send that as the message subject, else send a
* passed message object.
*/ */
function sendMessage (message: object) { function sendMessage (message: string | object) {
try { try {
encodeTransform.write(message); if (typeof message === "string") {
encodeTransform.write({
subject: message
});
} else {
encodeTransform.write(message);
}
} catch (err) { } catch (err) {
console.error("Failed to encode message"); console.error("Failed to encode message", err);
} }
} }
@@ -72,6 +81,7 @@ const existingSessions: Map<string, Session> = new Map();
const existingMedia: Map<string, Media> = new Map(); const existingMedia: Map<string, Media> = new Map();
let receiverSelectorApp: child_process.ChildProcess; let receiverSelectorApp: child_process.ChildProcess;
let receiverSelectorAppClosed = true;
/** /**
* Handle incoming messages from the extension and forward * Handle incoming messages from the extension and forward
@@ -130,10 +140,18 @@ async function handleMessage (message: Message) {
return; return;
} }
if (message.subject.startsWith("bridge:/receiverSelector/")) {
handleReceiverSelectorMessage(message);
}
if (message.subject.startsWith("bridge:/mediaServer/")) {
handleMediaServerMessage(message);
}
switch (message.subject) { switch (message.subject) {
case "bridge:/getInfo": { case "bridge:/getInfo": {
const extensionVersion = message.data; const extensionVersion = message.data;
return __applicationVersion; encodeTransform.write(__applicationVersion);
} }
case "bridge:/initialize": { case "bridge:/initialize": {
@@ -142,8 +160,11 @@ async function handleMessage (message: Message) {
break; break;
} }
}
}
function handleReceiverSelectorMessage (message: Message) {
switch (message.subject) {
case "bridge:/receiverSelector/open": { case "bridge:/receiverSelector/open": {
const receiverSelectorData = message.data; const receiverSelectorData = message.data;
@@ -167,6 +188,8 @@ async function handleMessage (message: Message) {
path.join(process.cwd(), "selector") path.join(process.cwd(), "selector")
, [ receiverSelectorData ]); , [ receiverSelectorData ]);
receiverSelectorAppClosed = false;
receiverSelectorApp.stdout!.setEncoding("utf8"); receiverSelectorApp.stdout!.setEncoding("utf8");
receiverSelectorApp.stdout!.on("data", data => { receiverSelectorApp.stdout!.on("data", data => {
sendMessage({ sendMessage({
@@ -175,7 +198,7 @@ async function handleMessage (message: Message) {
}); });
}); });
receiverSelectorApp.addListener("error", err => { receiverSelectorApp.on("error", err => {
sendMessage({ sendMessage({
subject: "main:/receiverSelector/error" subject: "main:/receiverSelector/error"
, data: err.message , data: err.message
@@ -183,9 +206,13 @@ async function handleMessage (message: Message) {
}); });
receiverSelectorApp.on("close", () => { receiverSelectorApp.on("close", () => {
sendMessage({ if (!receiverSelectorAppClosed) {
subject: "main:/receiverSelector/close" receiverSelectorAppClosed = true;
});
sendMessage({
subject: "main:/receiverSelector/close"
});
}
}); });
break; break;
@@ -193,34 +220,78 @@ async function handleMessage (message: Message) {
case "bridge:/receiverSelector/close": { case "bridge:/receiverSelector/close": {
receiverSelectorApp.kill(); receiverSelectorApp.kill();
receiverSelectorAppClosed = true;
break; break;
} }
}
}
function handleMediaServerMessage (message: Message) {
switch (message.subject) {
case "bridge:/mediaServer/start": { case "bridge:/mediaServer/start": {
const { filePath, port } = message.data; const { filePath, port }
: { filePath: string, port: number } = message.data;
mediaServer = new MediaServer(filePath, port); const contentType = mime.lookup(filePath);
mediaServer.start();
mediaServer.on("started", () => { if (!contentType) {
sendMessage({ sendMessage("mediaCast:/mediaServer/error");
subject: "mediaCast:/mediaServer/started" break;
}); }
if (mediaServer && mediaServer.listening) {
mediaServer.close();
}
mediaServer = http.createServer((req, res) => {
const { size: fileSize } = fs.statSync(filePath);
const { range } = req.headers;
// Partial content HTTP 206
if (range) {
const bounds = range.substring(6).split("-");
const start = parseInt(bounds[0]);
const end = bounds[1] ? parseInt(bounds[1]) : fileSize - 1;
res.writeHead(206, {
"Accept-Ranges": "bytes"
, "Content-Range": `bytes ${start}-${end}/${fileSize}`
, "Content-Length": (end - start) + 1
, "Content-Type": contentType
});
fs.createReadStream(filePath, { start, end }).pipe(res);
} else {
res.writeHead(200, {
"Content-Length": fileSize
, "Content-Type": contentType
});
fs.createReadStream(filePath).pipe(res);
}
}); });
mediaServer.on("stopped", () => { mediaServer.on("listening", () => {
sendMessage({ sendMessage("mediaCast:/mediaServer/started");
subject: "mediaCast:/mediaServer/stopped"
});
}); });
mediaServer.on("close", () => {
console.error("mediaServer close");
sendMessage("mediaCast:/mediaServer/stopped");
});
mediaServer.on("error", (a) => {
console.error("mediaServer error", a);
sendMessage("mediaCast:/mediaServer/error");
});
mediaServer.listen(port);
break; break;
} }
case "bridge:/mediaServer/stop": { case "bridge:/mediaServer/stop": {
if (mediaServer) { if (mediaServer && mediaServer.listening) {
mediaServer.stop(); mediaServer.close();
} }
break; break;

View File

@@ -70,7 +70,7 @@ const webpackConfig = require(`${ROOT}/webpack.config.js`)({
webpackConfig.mode = argv.mode; webpackConfig.mode = argv.mode;
webpackConfig.devtool = argv.mode === "production" webpackConfig.devtool = argv.mode === "production"
? "none" ? "none"
: "eval"; : "source-map";
// Clean // Clean

2
ext/package-lock.json generated
View File

@@ -6377,7 +6377,7 @@
}, },
"pretty-format": { "pretty-format": {
"version": "3.8.0", "version": "3.8.0",
"resolved": "http://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
"integrity": "sha1-v77VbV6ad2ZF9LH/eqGjrE+jw4U=", "integrity": "sha1-v77VbV6ad2ZF9LH/eqGjrE+jw4U=",
"dev": true "dev": true
}, },

View File

@@ -0,0 +1,85 @@
"use strict";
import options from "./lib/options";
import { getReceiverSelector
, ReceiverSelection
, ReceiverSelector
, ReceiverSelectorMediaType
, ReceiverSelectorType } from "./receiver_selectors";
import StatusManager from "./StatusManager";
let sharedSelector: ReceiverSelector;
async function getSelector () {
return getReceiverSelector(
await options.get("receiverSelectorType"));
}
async function getSharedSelector () {
if (!sharedSelector) {
sharedSelector = await getSelector();
}
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 (
defaultMediaType =
ReceiverSelectorMediaType.Tab
, availableMediaTypes =
ReceiverSelectorMediaType.Tab
| ReceiverSelectorMediaType.Screen)
// | ReceiverSelectorMediaType.File)
: Promise<ReceiverSelection> {
return new Promise(async (resolve, reject) => {
/**
* Close any existing selector, and renew to minimize issues
* with bridge failing.
*/
await getSharedSelector();
if (sharedSelector.isOpen) {
sharedSelector.close();
}
sharedSelector = await getSelector();
sharedSelector.addEventListener("selected", ev => {
console.info("fx_cast (Debug): Selected receiver", ev.detail);
resolve(ev.detail);
});
sharedSelector.addEventListener("cancelled", ev => {
console.info("fx_cast (Debug): Cancelled receiver selection");
resolve(null);
});
sharedSelector.addEventListener("error", ev => {
console.error("fx_cast (Debug): Failed to select receiver");
reject();
});
sharedSelector.open(
StatusManager.getReceivers()
, defaultMediaType
, availableMediaTypes);
});
}
export default {
getSelection
, getSharedSelector
};

154
ext/src/StatusManager.ts Normal file
View File

@@ -0,0 +1,154 @@
"use strict";
import bridge from "./lib/bridge";
import options from "./lib/options";
import { TypedEventTarget } from "./lib/typedEvents";
import { Message, Receiver, ReceiverStatus } from "./types";
interface ReceiverStatusMessage extends Message {
subject: "receiverStatus";
data: {
id: string;
status: ReceiverStatus;
};
}
interface ServiceDownMessage extends Message {
subject: "shim:/serviceDown";
data: {
id: string;
};
}
interface ServiceUpMessage extends Message {
subject: "shim:/serviceUp";
data: Receiver;
}
interface EventMap {
"serviceUp": ServiceUpMessage["data"];
"serviceDown": ServiceDownMessage["data"];
"statusUpdate": ReceiverStatusMessage["data"];
}
// tslint:disable-next-line:new-parens
export default new class extends TypedEventTarget<EventMap> {
private bridgePort: browser.runtime.Port;
private receivers = new Map<string, Receiver>();
constructor () {
super();
// Bind listeners
this.onBridgePortMessage = this.onBridgePortMessage.bind(this);
this.onBridgePortDisconnect = this.onBridgePortDisconnect.bind(this);
this.initBridgePort();
}
public getReceivers () {
return Array.from(this.receivers.values());
}
private async initBridgePort () {
this.bridgePort = await bridge.connect();
this.bridgePort.onMessage.addListener(this.onBridgePortMessage);
this.bridgePort.onDisconnect.addListener(this.onBridgePortDisconnect);
this.bridgePort.postMessage({
subject: "bridge:/initialize"
, data: {
shouldWatchStatus: true
}
});
}
/**
* Handles incoming bridge status messages, manages the
* receiver list, and dispatches events.
*/
private onBridgePortMessage (message: Message) {
switch (message.subject) {
case "shim:/serviceUp": {
const { data: receiver } = (message as ServiceUpMessage);
this.receivers.set(receiver.id, receiver);
const serviceUpEvent = new CustomEvent("serviceUp", {
detail: receiver
});
this.dispatchEvent(serviceUpEvent);
break;
}
case "shim:/serviceDown": {
const { data: { id }} = (message as ServiceDownMessage);
if (this.receivers.has(id)) {
this.receivers.delete(id);
}
const serviceDownEvent = new CustomEvent("serviceDown", {
detail: { id }
});
this.dispatchEvent(serviceDownEvent);
break;
}
case "receiverStatus": {
const { data: { id, status }}
= (message as ReceiverStatusMessage);
const receiver = this.receivers.get(id);
// Merge with existing
this.receivers.set(id, {
...receiver
, status: {
...receiver.status
, ...status
}
});
}
}
}
/**
* Runs once the status bridge has disconnected. Sends
* serviceDown messages for all receivers to all shims to
* update receiver availability, then clears the receiver
* list.
*
* Attempts to reinitialize the status bridge after 10
* seconds. If it fails immediately, this handler will be
* triggered again and the timer is reset for another 10
* seconds.
*/
private onBridgePortDisconnect () {
for (const [, receiver] of this.receivers) {
const serviceDownEvent = new CustomEvent("serviceDown", {
detail: { id: receiver.id }
});
this.dispatchEvent(serviceDownEvent);
}
// Cleanup
this.receivers.clear();
this.bridgePort.onDisconnect.removeListener(
this.onBridgePortDisconnect);
this.bridgePort.onMessage.removeListener(this.onBridgePortMessage);
this.bridgePort = null;
window.setTimeout(async () => {
this.initBridgePort();
}, 10000);
}
};

278
ext/src/createMenus.ts Normal file
View File

@@ -0,0 +1,278 @@
"use strict";
import options from "./lib/options";
import { TypedEventTarget } from "./lib/typedEvents";
const _ = browser.i18n.getMessage;
const URL_PATTERN_HTTP = "http://*/*";
const URL_PATTERN_HTTPS = "https://*/*";
const URL_PATTERN_FILE = "file://*/*";
const URL_PATTERNS_REMOTE = [ URL_PATTERN_HTTP, URL_PATTERN_HTTPS ];
const URL_PATTERNS_ALL = [ ...URL_PATTERNS_REMOTE, URL_PATTERN_FILE ];
type MenuId = string | number;
let menuIdMediaCast: MenuId;
let menuIdMirroringCast: MenuId;
let menuIdWhitelist: MenuId;
let menuIdWhitelistRecommended: MenuId;
const whitelistChildMenuPatterns = new Map<MenuId, string>();
let hasCreatedMenus = false;
export default async function createMenus () {
if (!hasCreatedMenus) {
hasCreatedMenus = true;
const opts = await options.getAll();
// <video>/<audio> "Cast..." context menu item
menuIdMediaCast = await browser.menus.create({
contexts: [ "audio", "video" ]
, title: _("contextCast")
, visible: opts.mediaEnabled
, targetUrlPatterns: opts.localMediaEnabled
? URL_PATTERNS_ALL
: URL_PATTERNS_REMOTE
});
// Screen/Tab mirroring "Cast..." context menu item
menuIdMirroringCast = await browser.menus.create({
contexts: [ "browser_action", "page", "tools_menu" ]
, title: _("contextCast")
, visible: opts.mirroringEnabled
// Mirroring doesn't work from file:// urls
, documentUrlPatterns: URL_PATTERNS_REMOTE
});
menuIdWhitelist = await browser.menus.create({
contexts: [ "browser_action" ]
, title: _("contextAddToWhitelist")
, enabled: false
});
menuIdWhitelistRecommended = await browser.menus.create({
title: _("contextAddToWhitelistRecommended")
, parentId: menuIdWhitelist
});
await browser.menus.create({
type: "separator"
, parentId: menuIdWhitelist
});
}
return {
menuIdMediaCast
, menuIdMirroringCast
, menuIdWhitelist
, menuIdWhitelistRecommended
, whitelistChildMenuPatterns
};
}
options.addEventListener("changed", async ev => {
const alteredOpts = ev.detail;
const opts = await options.getAll();
if (alteredOpts.includes("mirroringEnabled")) {
browser.menus.update(menuIdMirroringCast, {
visible: opts.mirroringEnabled
});
}
if (alteredOpts.includes("mediaEnabled")) {
browser.menus.update(menuIdMediaCast, {
visible: opts.mediaEnabled
});
}
if (alteredOpts.includes("localMediaEnabled")) {
browser.menus.update(menuIdMediaCast, {
targetUrlPatterns: opts.localMediaEnabled
? URL_PATTERNS_ALL
: URL_PATTERNS_REMOTE
});
}
});
browser.menus.onClicked.addListener(async info => {
if (info.parentMenuItemId === menuIdWhitelist) {
const pattern = whitelistChildMenuPatterns.get(info.menuItemId);
const whitelist = await options.get("userAgentWhitelist");
// Add to whitelist and update options
whitelist.push(pattern);
await options.set("userAgentWhitelist", whitelist);
}
});
browser.menus.onShown.addListener(async info => {
// Only rebuild menus if whitelist menu present
// WebExt typings are broken again here, so ugly casting
const menuIds = info.menuIds as unknown as number[];
if (menuIds.includes(menuIdWhitelist as number)) {
return;
}
/**
* If page URL doesn't exist, we're not on a page and have
* nothing to whitelist, so disable the menu and return.
*/
if (!info.pageUrl) {
browser.menus.update(menuIdWhitelist, {
enabled: false
});
browser.menus.refresh();
return;
}
const url = new URL(info.pageUrl);
const urlHasOrigin = url.origin !== "null";
/**
* If the page URL doesn't have an origin, we're not on a
* remote page and have nothing to whitelist, so disable the
* menu and return.
*/
if (!urlHasOrigin) {
browser.menus.update(menuIdWhitelist, {
enabled: false
});
browser.menus.refresh();
return;
}
// Enable the whitelist menu
browser.menus.update(menuIdWhitelist, {
enabled: true
});
for (const [ menuId ] of whitelistChildMenuPatterns) {
// Clear all page-specific temporary menus
if (menuId !== menuIdWhitelistRecommended) {
browser.menus.remove(menuId);
}
whitelistChildMenuPatterns.delete(menuId);
}
// If there is more than one subdomain, get the base domain
const baseDomain = (url.host.match(/\./g) || []).length > 1
? url.host.substring(url.host.indexOf(".") + 1)
: url.host;
const patternRecommended = `${url.origin}/*`;
const patternSearch = `${url.origin}${url.pathname}${url.search}`;
const patternWildcardProtocol = `*://${url.host}/*`;
const patternWildcardSubdomain = `${url.protocol}//*.${baseDomain}/*`;
const patternWildcardProtocolAndSubdomain = `*://*.${baseDomain}/*`;
// Update recommended menu item
browser.menus.update(menuIdWhitelistRecommended, {
title: _("contextAddToWhitelistRecommended", patternRecommended)
});
whitelistChildMenuPatterns.set(
menuIdWhitelistRecommended, patternRecommended);
if (url.search) {
const whitelistSearchMenuId = await browser.menus.create({
title: _("contextAddToWhitelistAdvancedAdd", patternSearch)
, parentId: menuIdWhitelist
});
whitelistChildMenuPatterns.set(
whitelistSearchMenuId, patternSearch);
}
/**
* Split URL path into segments and add menu items for each
* partial path as the segments are removed.
*/
{
const pathTrimmed = url.pathname.endsWith("/")
? url.pathname.substring(0, url.pathname.length - 1)
: url.pathname;
const pathSegments = pathTrimmed.split("/")
.filter(segment => segment)
.reverse();
if (pathSegments.length) {
let index = 0;
for (const pathSegment of pathSegments) {
const partialPath = pathSegments
.slice(index)
.reverse()
.join("/");
const pattern = `${url.origin}/${partialPath}/*`;
const partialPathMenuId = await browser.menus.create({
title: _("contextAddToWhitelistAdvancedAdd", pattern)
, parentId: menuIdWhitelist
});
whitelistChildMenuPatterns.set(
partialPathMenuId, pattern);
index++;
}
}
}
const wildcardProtocolMenuId = await browser.menus.create({
title: _("contextAddToWhitelistAdvancedAdd"
, patternWildcardProtocol)
, parentId: menuIdWhitelist
});
whitelistChildMenuPatterns.set(
wildcardProtocolMenuId, patternWildcardProtocol);
const wildcardSubdomainMenuId = await browser.menus.create({
title: _("contextAddToWhitelistAdvancedAdd"
, patternWildcardSubdomain)
, parentId: menuIdWhitelist
});
whitelistChildMenuPatterns.set(
wildcardSubdomainMenuId, patternWildcardSubdomain);
const wildcardProtocolAndSubdomainMenuId = await browser.menus.create({
title: _("contextAddToWhitelistAdvancedAdd"
, patternWildcardProtocolAndSubdomain)
, parentId: menuIdWhitelist
});
whitelistChildMenuPatterns.set(
wildcardProtocolAndSubdomainMenuId
, patternWildcardProtocolAndSubdomain);
await browser.menus.refresh();
});

155
ext/src/createShim.ts Normal file
View File

@@ -0,0 +1,155 @@
"use strict";
import bridge from "./lib/bridge";
import loadSender from "./lib/loadSender";
import options from "./lib/options";
import { TypedEventTarget } from "./lib/typedEvents";
import { Message } from "./types";
import { ReceiverSelectorMediaType } from "./receiver_selectors";
import SelectorManager from "./SelectorManager";
import StatusManager from "./StatusManager";
export interface Shim {
bridgePort: browser.runtime.Port;
contentPort?: browser.runtime.Port;
contentTabId?: number;
contentFrameId?: number;
}
export default async function createShim (
port: browser.runtime.Port): Promise<Shim> {
const contentPort = port;
const contentTabId = port.sender.tab.id;
const contentFrameId = port.sender.frameId;
const bridgePort = await bridge.connect();
/**
* If either the bridge port or the content port disconnects,
* just teardown all communication.
*/
function onDisconnect () {
bridgePort.onMessage.removeListener(onBridgePortMessage);
contentPort.onMessage.removeListener(onContentPortMessage);
// Ensure all ports are disconnected
contentPort.disconnect();
bridgePort.disconnect();
}
bridgePort.onDisconnect.addListener(onDisconnect);
contentPort.onDisconnect.addListener(onDisconnect);
// Add listeners
bridgePort.onMessage.addListener(onBridgePortMessage);
contentPort.onMessage.addListener(onContentPortMessage);
function onBridgePortMessage (message: Message) {
contentPort.postMessage(message);
}
async function onContentPortMessage (message: Message) {
const [ destination ] = message.subject.split(":/");
if (destination === "bridge") {
bridgePort.postMessage(message);
}
switch (message.subject) {
case "main:/shimInitialized": {
for (const receiver of StatusManager.getReceivers()) {
contentPort.postMessage({
subject: "shim:/serviceUp"
, data: { id: receiver.id }
});
}
break;
}
case "main:/selectReceiverBegin": {
const allMediaTypes =
ReceiverSelectorMediaType.App
| ReceiverSelectorMediaType.Tab
| ReceiverSelectorMediaType.Screen
| ReceiverSelectorMediaType.File;
try {
const selection = await SelectorManager.getSelection(
ReceiverSelectorMediaType.App
, allMediaTypes);
// Handle cancellation
if (!selection) {
contentPort.postMessage({
subject: "shim:/selectReceiverCancelled"
});
break;
}
/**
* 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) {
contentPort.postMessage({
subject: "shim:/selectReceiverCancelled"
});
loadSender({
tabId: contentTabId
, frameId: contentFrameId
, selection
});
break;
}
// Pass selection back to shim
contentPort.postMessage({
subject: "shim:/selectReceiverEnd"
, data: selection
});
} catch (err) {
// TODO: Report errors properly
contentPort.postMessage({
subject: "shim:/selectReceiverCancelled"
});
}
break;
}
case "main:/sessionCreated": {
const selector = await SelectorManager.getSharedSelector();
if (selector.isOpen) {
selector.close();
}
break;
}
}
}
contentPort.postMessage({
subject: "shim:/initialized"
, data: await bridge.getInfo()
});
return {
bridgePort
, contentPort
, contentTabId
, contentFrameId
};
}

View File

@@ -1,29 +1,10 @@
"use strict"; "use strict";
import { Options } from "./lib/options";
import { ReceiverSelectorType } from "./receiver_selectors"; import { ReceiverSelectorType } from "./receiver_selectors";
export interface Options {
bridgeApplicationName: string;
mediaEnabled: boolean;
mediaSyncElement: boolean;
mediaStopOnUnload: boolean;
localMediaEnabled: boolean;
localMediaServerPort: number;
mirroringEnabled: boolean;
mirroringAppId: string;
receiverSelectorType: ReceiverSelectorType;
// TODO: Implement export default {
receiverSelectorCloseIfFocusLost: boolean;
receiverSelectorWaitForConnection: boolean;
userAgentWhitelistEnabled: boolean;
userAgentWhitelist: string[];
[key: string]: Options[keyof Options];
}
const options: Options = {
bridgeApplicationName: APPLICATION_NAME bridgeApplicationName: APPLICATION_NAME
, mediaEnabled: true , mediaEnabled: true
, mediaSyncElement: false , mediaSyncElement: false
@@ -39,6 +20,4 @@ const options: Options = {
, userAgentWhitelist: [ , userAgentWhitelist: [
"https://www.netflix.com/*" "https://www.netflix.com/*"
] ]
}; } as Options;
export default options;

4
ext/src/global.d.ts vendored
View File

@@ -37,8 +37,8 @@ declare interface RTCPeerConnection {
} }
declare interface MediaDevices { declare interface MediaDevices {
getDisplayMedia (constraints: MediaStreamConstraints) getDisplayMedia (constraints: MediaStreamConstraints)
: Promise<MediaStream>; : Promise<MediaStream>;
} }

View File

@@ -6,6 +6,23 @@ import nativeMessaging from "./nativeMessaging";
import options from "./options"; 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 { export interface BridgeInfo {
name: string; name: string;
version: string; version: string;
@@ -16,7 +33,7 @@ export interface BridgeInfo {
isVersionNewer: boolean; isVersionNewer: boolean;
} }
export default async function getBridgeInfo (): Promise<BridgeInfo> { async function getInfo (): Promise<BridgeInfo> {
const applicationName = await options.get("bridgeApplicationName"); const applicationName = await options.get("bridgeApplicationName");
let applicationVersion: string; let applicationVersion: string;
@@ -65,3 +82,9 @@ export default async function getBridgeInfo (): Promise<BridgeInfo> {
, isVersionNewer , isVersionNewer
}; };
} }
export default {
connect
, getInfo
};

52
ext/src/lib/loadSender.ts Normal file
View 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;
}
}
}

View 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
};

View File

@@ -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
};

View File

@@ -1,79 +1,149 @@
"use strict"; "use strict";
import defaultOptions, { Options } from "../defaultOptions"; import defaultOptions from "../defaultOptions";
import { Message } from "../types";
import { TypedEventTarget } from "./typedEvents";
/** export interface Options {
* Fetches `options` key from storage and returns it as bridgeApplicationName: string;
* Options interface type. mediaEnabled: boolean;
*/ mediaSyncElement: boolean;
async function getAll (): Promise<Options> { mediaStopOnUnload: boolean;
const { options }: { options: Options } = localMediaEnabled: boolean;
await browser.storage.sync.get("options"); 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. interface EventMap {
* Returns storage promise. "changed": Array<keyof Options>;
*/
async function setAll (options: Options): Promise<void> {
return browser.storage.sync.set({ options });
} }
/** // tslint:disable-next-line:new-parens
* Gets specific option from storage and returns it as its export default new class extends TypedEventTarget<EventMap> {
* type from Options interface type. constructor () {
*/ super();
async function get<T extends keyof Options> (name: T): Promise<Options[T]> {
const options = await getAll();
if (options.hasOwnProperty(name)) { browser.storage.onChanged.addListener((changes, areaName) => {
return options[name]; 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 * Fetches `options` key from storage and returns it as
* promise. * Options interface type.
*/ */
async function set<T extends keyof Options> ( public async getAll (): Promise<Options> {
name: T const { options }: { options: Options } =
, value: Options[T]): Promise<void> { await browser.storage.sync.get("options");
const options = await getAll(); return options;
options[name] = value; }
return setAll(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 * Gets specific option from storage and returns it as its
* against defaults. Any options in defaults and not in * type from Options interface type.
* storage are set. Does not override any existing options. */
*/ public async get<T extends keyof Options> (name: T): Promise<Options[T]> {
async function update (defaults = defaultOptions): Promise<void> { const options = await this.getAll();
const oldOpts = await getAll();
const newOpts: Partial<Options> = {};
// Find options not already in storage if (options.hasOwnProperty(name)) {
for (const [ optName, optVal ] of Object.entries(defaults)) { return options[name];
if (!oldOpts.hasOwnProperty(optName)) {
newOpts[optName] = optVal;
} }
} }
// Update storage with default values of new options /**
return setAll({ * Sets specific option to storage. Returns storage
...oldOpts * promise.
, ...newOpts */
}); 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 * Gets existing options from storage and compares it
, set, setAll * against defaults. Any options in defaults and not in
, update * 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
});
}
}; };

View 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);
}
}

View File

@@ -9,6 +9,28 @@ export function getNextEllipsis (ellipsis: string): string {
/* tslint:enable:curly */ /* 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 { interface WindowCenteredProps {
width: number; width: number;

File diff suppressed because it is too large Load Diff

View File

@@ -27,6 +27,15 @@
"scripts": [ "main.js" ] "scripts": [ "main.js" ]
} }
, "content_scripts": [
{
"all_frames": true
, "js": [ "shim/content.js" ]
, "matches": [ "<all_urls>" ]
, "run_at": "document_start"
}
]
, "content_security_policy": "CONTENT_SECURITY_POLICY" , "content_security_policy": "CONTENT_SECURITY_POLICY"
, "default_locale": "en" , "default_locale": "en"
, "manifest_version": 2 , "manifest_version": 2

View File

@@ -1,40 +0,0 @@
"use strict";
import { Message, Receiver, ReceiverStatus } from "./types";
export interface ReceiverStatusMessage extends Message {
subject: "receiverStatus";
data: {
id: string;
status: ReceiverStatus;
};
}
export interface ServiceDownMessage extends Message {
subject: "shim:/serviceDown";
data: {
id: string;
};
}
export interface ServiceUpMessage extends Message {
subject: "shim:/serviceUp";
data: Receiver;
}
export interface NativeReceiverSelectorSelectedMessage extends Message {
subject: "main:/receiverSelector/selected";
data: Receiver;
}
export interface NativeReceiverSelectorCloseMessage extends Message {
subject: "main:/receiverSelector/error";
data: string;
}
export interface NativeReceiverSelectorErrorMessage extends Message {
subject: "main:/receiverSelector/error";
data: string;
}

View File

@@ -1,39 +1,56 @@
"use strict"; "use strict";
import nativeMessaging from "../lib/nativeMessaging"; import bridge from "../lib/bridge";
import options from "../lib/options"; import options from "../lib/options";
import ReceiverSelector, { import ReceiverSelector, {
ReceiverSelectorMediaType } from "./ReceiverSelector"; ReceiverSelection
, ReceiverSelectorEvents
, ReceiverSelectorMediaType } from "./ReceiverSelector";
import { TypedEventTarget } from "../lib/typedEvents";
import { getWindowCenteredProps } from "../lib/utils"; import { getWindowCenteredProps } from "../lib/utils";
import { Message, Receiver } from "../types"; import { Message, Receiver } from "../types";
import { NativeReceiverSelectorCloseMessage
, NativeReceiverSelectorErrorMessage
, NativeReceiverSelectorSelectedMessage } from "../messageTypes";
const _ = browser.i18n.getMessage; const _ = browser.i18n.getMessage;
interface NativeReceiverSelectorSelectedMessage extends Message {
subject: "main:/receiverSelector/selected";
data: ReceiverSelection;
}
interface NativeReceiverSelectorCloseMessage extends Message {
subject: "main:/receiverSelector/error";
data: string;
}
interface NativeReceiverSelectorErrorMessage extends Message {
subject: "main:/receiverSelector/error";
data: string;
}
// TODO: Figure out lifetime properly
export default class NativeMacReceiverSelector export default class NativeMacReceiverSelector
extends EventTarget extends TypedEventTarget<ReceiverSelectorEvents>
implements ReceiverSelector { implements ReceiverSelector {
private bridgePort: browser.runtime.Port; private bridgePort: browser.runtime.Port;
private bridgePortDisconnected: boolean = false;
private wasReceiverSelected: boolean = false; private wasReceiverSelected: boolean = false;
private _isOpen: boolean = false;
get isOpen () {
return this._isOpen;
}
public async open ( public async open (
receivers: Receiver[] receivers: Receiver[]
, defaultMediaType: ReceiverSelectorMediaType , defaultMediaType: ReceiverSelectorMediaType
, availableMediaTypes: ReceiverSelectorMediaType): Promise<void> { , availableMediaTypes: ReceiverSelectorMediaType): Promise<void> {
const applicationName = await options.get("bridgeApplicationName"); this.bridgePort = await bridge.connect();
this.bridgePort = nativeMessaging.connectNative(applicationName);
this.bridgePort.onMessage.addListener((message: Message) => { this.bridgePort.onMessage.addListener((message: Message) => {
switch (message.subject) { switch (message.subject) {
@@ -56,7 +73,9 @@ export default class NativeMacReceiverSelector
}); });
this.bridgePort.onDisconnect.addListener(() => { this.bridgePort.onDisconnect.addListener(() => {
this.bridgePortDisconnected = true; this.bridgePort = null;
this.wasReceiverSelected = false;
this._isOpen = false;
}); });
@@ -84,14 +103,18 @@ export default class NativeMacReceiverSelector
, i18n_mediaSelectToLabel: _("popupMediaSelectToLabel") , i18n_mediaSelectToLabel: _("popupMediaSelectToLabel")
}) })
}); });
this._isOpen = true;
} }
public close (): void { public close (): void {
if (this.bridgePort && !this.bridgePortDisconnected) { if (this.bridgePort) {
this.bridgePort.postMessage({ this.bridgePort.postMessage({
subject: "bridge:/receiverSelector/close" subject: "bridge:/receiverSelector/close"
}); });
} }
this._isOpen = false;
} }
@@ -115,12 +138,12 @@ export default class NativeMacReceiverSelector
this.dispatchEvent(new CustomEvent("cancelled")); this.dispatchEvent(new CustomEvent("cancelled"));
} }
if (!this.bridgePortDisconnected) { if (this.bridgePort) {
this.bridgePort.disconnect(); this.bridgePort.disconnect();
} }
this.bridgePort = null; this.bridgePort = null;
this.bridgePortDisconnected = false;
this.wasReceiverSelected = false; this.wasReceiverSelected = false;
this._isOpen = false;
} }
} }

View File

@@ -1,19 +1,23 @@
"use strict"; "use strict";
import ReceiverSelector, { import ReceiverSelector, {
ReceiverSelectorMediaType } from "./ReceiverSelector"; ReceiverSelectorEvents
, ReceiverSelectorMediaType } from "./ReceiverSelector";
import { TypedEventTarget } from "../lib/typedEvents";
import { getWindowCenteredProps } from "../lib/utils"; import { getWindowCenteredProps } from "../lib/utils";
import { Message, Receiver } from "../types"; import { Message, Receiver } from "../types";
export default class PopupReceiverSelector export default class PopupReceiverSelector
extends EventTarget extends TypedEventTarget<ReceiverSelectorEvents>
implements ReceiverSelector { implements ReceiverSelector {
private windowId: number; private windowId: number;
private openerWindowId: number; private openerWindowId: number;
private messagePort: browser.runtime.Port; private messagePort: browser.runtime.Port;
private messagePortDisconnected: boolean;
private receivers: Receiver[]; private receivers: Receiver[];
private defaultMediaType: ReceiverSelectorMediaType; private defaultMediaType: ReceiverSelectorMediaType;
@@ -21,6 +25,8 @@ export default class PopupReceiverSelector
private wasReceiverSelected: boolean = false; private wasReceiverSelected: boolean = false;
private _isOpen: boolean = false;
constructor () { constructor () {
super(); super();
@@ -48,6 +54,9 @@ export default class PopupReceiverSelector
this.messagePort = port; this.messagePort = port;
this.messagePort.onMessage.addListener(this.onPopupMessage); this.messagePort.onMessage.addListener(this.onPopupMessage);
this.messagePort.onDisconnect.addListener(() => {
this.messagePortDisconnected = true;
});
this.messagePort.postMessage({ this.messagePort.postMessage({
subject: "popup:/populateReceiverList" subject: "popup:/populateReceiverList"
@@ -60,6 +69,9 @@ export default class PopupReceiverSelector
}); });
} }
get isOpen () {
return this._isOpen;
}
public async open ( public async open (
receivers: Receiver[] receivers: Receiver[]
@@ -85,6 +97,8 @@ export default class PopupReceiverSelector
, ...centeredProps , ...centeredProps
}); });
this._isOpen = true;
this.windowId = popup.id; this.windowId = popup.id;
this.openerWindowId = openerWindow.id; this.openerWindowId = openerWindow.id;
@@ -98,8 +112,16 @@ export default class PopupReceiverSelector
this.onWindowsFocusChanged); this.onWindowsFocusChanged);
} }
public close (): void { public async close (): Promise<void> {
browser.windows.remove(this.windowId); if (this.windowId) {
await browser.windows.remove(this.windowId);
}
this._isOpen = false;
if (this.messagePort && !this.messagePortDisconnected) {
this.messagePort.disconnect();
}
} }

View File

@@ -1,5 +1,6 @@
"use strict"; "use strict";
import { TypedEventTarget } from "../lib/typedEvents";
import { Receiver } from "../types"; import { Receiver } from "../types";
@@ -16,12 +17,18 @@ export interface ReceiverSelection {
filePath?: string; filePath?: string;
} }
export type ReceiverSelectorSelectedEvent = CustomEvent<ReceiverSelection>;
export type ReceiverSelectorErrorEvent = CustomEvent;
export type ReceiverSelectorCancelledEvent = CustomEvent;
export interface ReceiverSelectorEvents {
"selected": ReceiverSelection;
"error": void;
"cancelled": void;
}
export default interface ReceiverSelector
extends TypedEventTarget<ReceiverSelectorEvents> {
readonly isOpen: boolean;
export default interface ReceiverSelector extends EventTarget {
open (receivers: Receiver[] open (receivers: Receiver[]
, defaultMediaType: ReceiverSelectorMediaType , defaultMediaType: ReceiverSelectorMediaType
, availableMediaTypes: ReceiverSelectorMediaType): void; , availableMediaTypes: ReceiverSelectorMediaType): void;

View File

@@ -6,12 +6,16 @@ import PopupReceiverSelector
from "./PopupReceiverSelector"; from "./PopupReceiverSelector";
export { ReceiverSelection import { ReceiverSelection
, ReceiverSelectorCancelledEvent , ReceiverSelectorMediaType } from "./ReceiverSelector";
, ReceiverSelectorErrorEvent
, ReceiverSelectorMediaType
, ReceiverSelectorSelectedEvent } from "./ReceiverSelector";
type ReceiverSelector = ReturnType<typeof getReceiverSelector>;
export {
ReceiverSelector
, ReceiverSelection
, ReceiverSelectorMediaType
};
export enum ReceiverSelectorType { export enum ReceiverSelectorType {
Popup Popup

View File

@@ -1,17 +1,22 @@
"use strict"; "use strict";
import { Options } from "../defaultOptions"; import mediaCasting from "../lib/mediaCasting";
import cast, { init } from "../shim/export"; import options from "../lib/options";
import cast, { ensureInit } from "../shim/export";
import { Message, Receiver } from "../types";
// Variables passed from background // Variables passed from background
const { srcUrl const { selectedReceiver
, srcUrl
, targetElementId } , targetElementId }
: { srcUrl: string : { selectedReceiver: Receiver
, srcUrl: string
, targetElementId: number } = (window as any); , targetElementId: number } = (window as any);
let options: Options; let backgroundPort: browser.runtime.Port;
let session: cast.Session; let session: cast.Session;
let currentMedia: cast.media.Media; let currentMedia: cast.media.Media;
@@ -24,16 +29,13 @@ const isLocalFile = srcUrl.startsWith("file:");
const mediaElement = browser.menus.getTargetElement( const mediaElement = browser.menus.getTargetElement(
targetElementId) as HTMLMediaElement; targetElementId) as HTMLMediaElement;
window.addEventListener("beforeunload", () => { window.addEventListener("beforeunload", async () => {
browser.runtime.sendMessage({ backgroundPort.postMessage({
subject: "bridge:/mediaServer/stop" subject: "bridge:/mediaServer/stop"
}); });
if (options.mediaStopOnUnload) { if (await options.get("mediaStopOnUnload")) {
session.stop(null, null); session.stop(null, null);
/*currentMedia.stop(null
, onMediaStopSuccess
, onMediaStopError);*/
} }
}); });
@@ -55,42 +57,63 @@ function getLocalAddress () {
} }
async function onRequestSessionSuccess (newSession: cast.Session) { function startMediaServer (filePath: string, port: number) {
cast.logMessage("onRequestSessionSuccess"); return new Promise((resolve, reject) => {
backgroundPort.postMessage({
session = newSession; subject: "bridge:/mediaServer/start"
, data: {
let mediaUrl = new URL(srcUrl); filePath: decodeURI(filePath)
const port = options.localMediaServerPort; , port
}
if (isLocalFile) {
await new Promise((resolve, reject) => {
browser.runtime.sendMessage({
subject: "bridge:/mediaServer/start"
, data: {
filePath: decodeURI(mediaUrl.pathname)
, port
}
});
browser.runtime.onMessage.addListener(function onMessage (message) {
if (message.subject === "mediaCast:/mediaServer/started") {
browser.runtime.onMessage.removeListener(onMessage);
resolve();
}
});
}); });
// Address of local HTTP server backgroundPort.onMessage.addListener(
mediaUrl = new URL(`http://${await getLocalAddress()}:${port}/`); function onMessage (message: Message) {
switch (message.subject) {
case "mediaCast:/mediaServer/started": {
backgroundPort.onMessage.removeListener(onMessage);
resolve();
}
case "mediaCast:/mediaServer/error": {
backgroundPort.onMessage.removeListener(onMessage);
reject();
}
}
});
});
}
async function loadMedia () {
let mediaUrl = new URL(srcUrl);
const mediaTitle = mediaUrl.pathname;
/**
* If the media is a local file, start an HTTP media server
* and change the media URL to point to it.
*/
if (isLocalFile) {
const host = await getLocalAddress();
const port = await options.get("localMediaServerPort");
try {
// Wait until media server is listening
await startMediaServer(mediaUrl.pathname, port);
} catch (err) {
console.error("Failed to start media server");
return;
}
mediaUrl = new URL(`http://${host}:${port}/`);
} }
const mediaInfo = new cast.media.MediaInfo(mediaUrl.href, null); const mediaInfo = new cast.media.MediaInfo(mediaUrl.href, null);
// Media metadata (title/poster) // Media metadata (title/poster)
mediaInfo.metadata = new cast.media.GenericMediaMetadata(); mediaInfo.metadata = new cast.media.GenericMediaMetadata();
mediaInfo.metadata.metadataType = cast.media.MetadataType.GENERIC; mediaInfo.metadata.metadataType = cast.media.MetadataType.GENERIC;
mediaInfo.metadata.title = mediaUrl.pathname; mediaInfo.metadata.title = mediaTitle;
if (mediaElement instanceof HTMLVideoElement && mediaElement.poster) { if (mediaElement instanceof HTMLVideoElement && mediaElement.poster) {
mediaInfo.metadata.images = [ mediaInfo.metadata.images = [
@@ -164,41 +187,13 @@ async function onRequestSessionSuccess (newSession: cast.Session) {
, onLoadMediaError); , onLoadMediaError);
} }
function onRequestSessionError () {
cast.logMessage("onRequestSessionError");
}
async function onLoadMediaSuccess (media: cast.media.Media) {
function sessionListener (newSession: cast.Session) {
cast.logMessage("sessionListener");
}
function receiverListener (availability: string) {
cast.logMessage("receiverListener");
if (availability === cast.ReceiverAvailability.AVAILABLE) {
cast.requestSession(
onRequestSessionSuccess
, onRequestSessionError);
}
}
function onInitializeSuccess () {
cast.logMessage("onInitializeSuccess");
}
function onInitializeError () {
cast.logMessage("onInitializeError");
}
function onLoadMediaSuccess (media: cast.media.Media) {
cast.logMessage("onLoadMediaSuccess"); cast.logMessage("onLoadMediaSuccess");
currentMedia = media; currentMedia = media;
if (options.mediaSyncElement) { if (await options.get("mediaSyncElement")) {
mediaElement.addEventListener("play", () => { mediaElement.addEventListener("play", () => {
if (ignoreMediaEvents) { if (ignoreMediaEvents) {
ignoreMediaEvents = false; ignoreMediaEvents = false;
@@ -312,73 +307,57 @@ function onLoadMediaSuccess (media: cast.media.Media) {
} }
} }
function onRequestSessionError () {
cast.logMessage("onRequestSessionError");
}
function sessionListener (newSession: cast.Session) {
cast.logMessage("sessionListener");
}
function onInitializeSuccess () {
cast.logMessage("onInitializeSuccess");
}
function onInitializeError () {
cast.logMessage("onInitializeError");
}
function onLoadMediaError () { function onLoadMediaError () {
cast.logMessage("onLoadMediaError"); cast.logMessage("onLoadMediaError");
} }
/* play */
function onMediaPlaySuccess () { function onMediaPlaySuccess () {
cast.logMessage("onMediaPlaySuccess"); cast.logMessage("onMediaPlaySuccess");
} }
function onMediaPlayError (err: cast.Error) { function onMediaPlayError (err: cast.Error) {
cast.logMessage("onMediaPlayError"); cast.logMessage("onMediaPlayError");
} }
/* pause */
function onMediaPauseSuccess () { function onMediaPauseSuccess () {
cast.logMessage("onMediaPauseSuccess"); cast.logMessage("onMediaPauseSuccess");
} }
function onMediaPauseError (err: cast.Error) { function onMediaPauseError (err: cast.Error) {
cast.logMessage("onMediaPauseError"); cast.logMessage("onMediaPauseError");
} }
/* stop */
function onMediaStopSuccess () { function onMediaStopSuccess () {
cast.logMessage("onMediaStopSuccess"); cast.logMessage("onMediaStopSuccess");
} }
function onMediaStopError (err: cast.Error) { function onMediaStopError (err: cast.Error) {
cast.logMessage("onMediaStopError"); cast.logMessage("onMediaStopError");
} }
/* seek */
function onMediaSeekSuccess () { function onMediaSeekSuccess () {
cast.logMessage("onMediaSeekSuccess"); cast.logMessage("onMediaSeekSuccess");
} }
function onMediaSeekError (err: cast.Error) { function onMediaSeekError (err: cast.Error) {
cast.logMessage("onMediaSeekError"); cast.logMessage("onMediaSeekError");
} }
init().then(async bridgeInfo => { ensureInit().then(async (port) => {
if (!bridgeInfo.isVersionCompatible) { backgroundPort = port;
console.error("__onGCastApiAvailable error");
return;
}
options = (await browser.storage.sync.get("options")).options; const isLocalMediaEnabled = await options.get("localMediaEnabled");
if (isLocalFile && !isLocalMediaEnabled) {
if (isLocalFile && !options.localMediaEnabled) {
cast.logMessage("Local media casting not enabled"); cast.logMessage("Local media casting not enabled");
return; return;
} }
session = await mediaCasting.getMediaSession(selectedReceiver);
const sessionRequest = new cast.SessionRequest( loadMedia();
cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID);
const apiConfig = new cast.ApiConfig(sessionRequest
, sessionListener
, receiverListener);
cast.initialize(apiConfig
, onInitializeSuccess
, onInitializeError);
}); });

View File

@@ -1,15 +1,19 @@
"use strict"; "use strict";
import options from "../lib/options"; import options from "../lib/options";
import cast, { init } from "../shim/export"; import cast, { ensureInit } from "../shim/export";
import { ReceiverSelectorMediaType } import { ReceiverSelectorMediaType }
from "../receiver_selectors/ReceiverSelector"; from "../receiver_selectors/ReceiverSelector";
import { Receiver } from "../types";
// Variables passed from background // Variables passed from background
const { selectedMedia } const { selectedMedia
: { selectedMedia: ReceiverSelectorMediaType } = (window as any); , selectedReceiver }
: { selectedMedia: ReceiverSelectorMediaType
, selectedReceiver: Receiver } = (window as any);
const FX_CAST_RECEIVER_APP_NAMESPACE = "urn:x-cast:fx_cast"; const FX_CAST_RECEIVER_APP_NAMESPACE = "urn:x-cast:fx_cast";
@@ -42,6 +46,10 @@ if (typeof navigator.mediaDevices.getDisplayMedia === "undefined") {
* receiver device. * receiver device.
*/ */
function sendAppMessage (subject: string, data: any) { function sendAppMessage (subject: string, data: any) {
if (!session) {
return;
}
session.sendMessage(FX_CAST_RECEIVER_APP_NAMESPACE, { session.sendMessage(FX_CAST_RECEIVER_APP_NAMESPACE, {
subject subject
, data , data
@@ -54,9 +62,7 @@ window.addEventListener("beforeunload", () => {
}); });
async function onRequestSessionSuccess ( async function onRequestSessionSuccess (newSession: cast.Session) {
newSession: cast.Session
, newSelectedMedia: ReceiverSelectorMediaType) {
cast.logMessage("onRequestSessionSuccess"); cast.logMessage("onRequestSessionSuccess");
@@ -83,7 +89,7 @@ async function onRequestSessionSuccess (
sendAppMessage("iceCandidate", ev.candidate); sendAppMessage("iceCandidate", ev.candidate);
}); });
switch (newSelectedMedia) { switch (selectedMedia) {
case ReceiverSelectorMediaType.Tab: { case ReceiverSelectorMediaType.Tab: {
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
@@ -161,8 +167,9 @@ function receiverListener (availability: string) {
if (availability === cast.ReceiverAvailability.AVAILABLE) { if (availability === cast.ReceiverAvailability.AVAILABLE) {
wasSessionRequested = true; wasSessionRequested = true;
cast.requestSession( cast._requestSession(
onRequestSessionSuccess selectedReceiver
, onRequestSessionSuccess
, onRequestSessionError); , onRequestSessionError);
} }
} }
@@ -182,13 +189,7 @@ function onInitializeError () {
} }
init().then(async bridgeInfo => { ensureInit().then(async () => {
if (!bridgeInfo.isVersionCompatible) {
console.error("__onGCastApiAvailable error");
return;
}
const mirroringAppId = await options.get("mirroringAppId"); const mirroringAppId = await options.get("mirroringAppId");
const sessionRequest = new cast.SessionRequest(mirroringAppId); const sessionRequest = new cast.SessionRequest(mirroringAppId);
@@ -196,9 +197,7 @@ init().then(async bridgeInfo => {
sessionRequest sessionRequest
, sessionListener , sessionListener
, receiverListener , receiverListener
, undefined, undefined , undefined, undefined);
, selectedMedia
, availableMediaTypes);
cast.initialize(apiConfig cast.initialize(apiConfig
, onInitializeSuccess , onInitializeSuccess

View File

@@ -6,9 +6,6 @@ import SessionRequest from "./SessionRequest";
import { AutoJoinPolicy import { AutoJoinPolicy
, DefaultActionPolicy } from "../enums"; , DefaultActionPolicy } from "../enums";
import { ReceiverSelectorMediaType }
from "../../../receiver_selectors/ReceiverSelector";
export default class ApiConfig { export default class ApiConfig {
public additionalSessionRequests: any[] = []; public additionalSessionRequests: any[] = [];
@@ -23,15 +20,5 @@ export default class ApiConfig {
, public autoJoinPolicy: string , public autoJoinPolicy: string
= AutoJoinPolicy.TAB_AND_ORIGIN_SCOPED = AutoJoinPolicy.TAB_AND_ORIGIN_SCOPED
, public defaultActionPolicy: string , public defaultActionPolicy: string
= DefaultActionPolicy.CREATE_SESSION = DefaultActionPolicy.CREATE_SESSION) {}
// TODO: Remove awful hack for mirror casting
, public _defaultMediaType: ReceiverSelectorMediaType
= ReceiverSelectorMediaType.App
, public _availableMediaTypes: ReceiverSelectorMediaType
= ReceiverSelectorMediaType.App
| ReceiverSelectorMediaType.Tab
| ReceiverSelectorMediaType.Screen
| ReceiverSelectorMediaType.File) {
}
} }

View File

@@ -4,7 +4,7 @@ import ApiConfig from "./classes/ApiConfig";
import DialRequest from "./classes/DialRequest"; import DialRequest from "./classes/DialRequest";
import Error_ from "./classes/Error"; import Error_ from "./classes/Error";
import Image_ from "./classes/Image"; import Image_ from "./classes/Image";
import Receiver from "./classes/Receiver"; import Receiver_ from "./classes/Receiver";
import ReceiverDisplayStatus from "./classes/ReceiverDisplayStatus"; import ReceiverDisplayStatus from "./classes/ReceiverDisplayStatus";
import SenderApplication from "./classes/SenderApplication"; import SenderApplication from "./classes/SenderApplication";
import Session from "./classes/Session"; import Session from "./classes/Session";
@@ -26,18 +26,18 @@ import { AutoJoinPolicy
import * as media from "./media"; import * as media from "./media";
import { ReceiverSelectorMediaType } import { ReceiverSelectorMediaType }
from "../../receiver_selectors/ReceiverSelector"; from "../../receiver_selectors/ReceiverSelector";
import { Receiver } from "../../types";
import { onMessage, sendMessageResponse } from "../eventMessageChannel"; import { onMessage, sendMessageResponse } from "../eventMessageChannel";
type ReceiverActionListener = ( type ReceiverActionListener = (
receiver: Receiver receiver: Receiver_
, receiverAction: string) => void; , receiverAction: string) => void;
type RequestSessionSuccessCallback = ( type RequestSessionSuccessCallback = (session: Session) => void;
session: Session
, selectedMedia: ReceiverSelectorMediaType) => void;
type SuccessCallback = () => void; type SuccessCallback = () => void;
type ErrorCallback = (err: Error_) => void; type ErrorCallback = (err: Error_) => void;
@@ -61,12 +61,13 @@ export {
, SenderPlatform, SessionStatus, VolumeControlType , SenderPlatform, SessionStatus, VolumeControlType
// Classes // Classes
, ApiConfig, DialRequest, Receiver, ReceiverDisplayStatus , ApiConfig, DialRequest, ReceiverDisplayStatus
, SenderApplication, Session, SessionRequest, Timeout , SenderApplication, Session, SessionRequest, Timeout
, Volume , Volume
, Error_ as Error , Error_ as Error
, Image_ as Image , Image_ as Image
, Receiver_ as Receiver
, media , media
}; };
@@ -84,14 +85,17 @@ export function addReceiverActionListener (
export function initialize ( export function initialize (
newApiConfig: ApiConfig newApiConfig: ApiConfig
, successCallback: SuccessCallback , successCallback?: SuccessCallback
, errorCallback: ErrorCallback): void { , errorCallback?: ErrorCallback): void {
console.info("fx_cast (Debug): cast.initialize"); console.info("fx_cast (Debug): cast.initialize");
// Already initialized // Already initialized
if (apiConfig) { if (apiConfig) {
errorCallback(new Error_(ErrorCode.INVALID_PARAMETER)); if (errorCallback) {
errorCallback(new Error_(ErrorCode.INVALID_PARAMETER));
}
return; return;
} }
@@ -106,7 +110,9 @@ export function initialize (
? ReceiverAvailability.AVAILABLE ? ReceiverAvailability.AVAILABLE
: ReceiverAvailability.UNAVAILABLE); : ReceiverAvailability.UNAVAILABLE);
successCallback(); if (successCallback) {
successCallback();
}
} }
export function logMessage (message: string): void { export function logMessage (message: string): void {
@@ -125,29 +131,37 @@ export function removeReceiverActionListener (
} }
export function requestSession ( export function requestSession (
successCallback: RequestSessionSuccessCallback successCallback?: RequestSessionSuccessCallback
, errorCallback: ErrorCallback , errorCallback?: ErrorCallback
, sessionRequest: SessionRequest , sessionRequest: SessionRequest = apiConfig.sessionRequest): void {
= apiConfig.sessionRequest): void {
console.info("fx_cast (Debug): cast.requestSession"); console.info("fx_cast (Debug): cast.requestSession");
// Called before initialization // Called before initialization
if (!apiConfig) { if (!apiConfig) {
errorCallback(new Error_(ErrorCode.API_NOT_INITIALIZED)); if (errorCallback) {
errorCallback(new Error_(ErrorCode.API_NOT_INITIALIZED));
}
return; return;
} }
// Already requesting session // Already requesting session
if (sessionRequestInProgress) { if (sessionRequestInProgress) {
errorCallback(new Error_(ErrorCode.INVALID_PARAMETER if (errorCallback) {
, "Session request already in progress.")); errorCallback(new Error_(ErrorCode.INVALID_PARAMETER
, "Session request already in progress."));
}
return; return;
} }
// No available receivers // No available receivers
if (!receiverList.length) { if (!receiverList.length) {
errorCallback(new Error_(ErrorCode.RECEIVER_UNAVAILABLE)); if (errorCallback) {
errorCallback(new Error_(ErrorCode.RECEIVER_UNAVAILABLE));
}
return; return;
} }
@@ -159,21 +173,94 @@ export function requestSession (
// Open destination chooser // Open destination chooser
sendMessageResponse({ sendMessageResponse({
subject: "main:/selectReceiverBegin" subject: "main:/selectReceiverBegin"
, data: {
defaultMediaType: apiConfig._defaultMediaType
, availableMediaTypes: apiConfig._availableMediaTypes
}
}); });
} }
export function _requestSession (
_receiver: Receiver
, successCallback?: RequestSessionSuccessCallback
, errorCallback?: ErrorCallback): void {
console.info("fx_cast (Debug): cast._requestSession");
if (!apiConfig) {
if (errorCallback) {
errorCallback(new Error_(ErrorCode.API_NOT_INITIALIZED));
}
return;
}
if (sessionRequestInProgress) {
if (errorCallback) {
errorCallback(new Error_(ErrorCode.INVALID_PARAMETER
, "Session request already in progress."));
}
return;
}
if (!receiverList.length) {
if (errorCallback) {
errorCallback(new Error_(ErrorCode.RECEIVER_UNAVAILABLE));
}
return;
}
sessionRequestInProgress = true;
sessionSuccessCallback = successCallback;
sessionErrorCallback = errorCallback;
const selectedReceiver = new Receiver_(
_receiver.id
, _receiver.friendlyName);
(selectedReceiver as any)._address = _receiver.host;
(selectedReceiver as any)._port = _receiver.port;
function createSession () {
sessionList.push(new Session(
sessionList.length.toString() // sessionId
, apiConfig.sessionRequest.appId // appId
, _receiver.friendlyName // displayName
, [] // appImages
, selectedReceiver // receiver
, (session: Session) => {
sendMessageResponse({
subject: "main:/sessionCreated"
});
sessionRequestInProgress = false;
if (sessionSuccessCallback) {
sessionSuccessCallback(session);
}
}));
}
// If an existing session is active, stop it and start new one
if (sessionList.length) {
const lastSession = sessionList[sessionList.length - 1];
if (lastSession.status !== SessionStatus.STOPPED) {
lastSession.stop(createSession, null);
}
} else {
createSession();
}
}
export function requestSessionById (sessionId: string): void { export function requestSessionById (sessionId: string): void {
console.info("STUB :: cast.requestSessionById"); console.info("STUB :: cast.requestSessionById");
} }
export function setCustomReceivers ( export function setCustomReceivers (
receivers: Receiver[] receivers: Receiver_[]
, successCallback: SuccessCallback , successCallback?: SuccessCallback
, errorCallback: ErrorCallback): void { , errorCallback?: ErrorCallback): void {
console.info("STUB :: cast.setCustomReceivers"); console.info("STUB :: cast.setCustomReceivers");
} }
@@ -240,7 +327,7 @@ onMessage(async message => {
case "shim:/selectReceiverEnd": { case "shim:/selectReceiverEnd": {
console.info("fx_cast (Debug): Selected receiver"); console.info("fx_cast (Debug): Selected receiver");
const selectedReceiver = new Receiver( const selectedReceiver = new Receiver_(
message.data.receiver.id message.data.receiver.id
, message.data.receiver.friendlyName); , message.data.receiver.friendlyName);
@@ -261,9 +348,9 @@ onMessage(async message => {
sessionRequestInProgress = false; sessionRequestInProgress = false;
sessionSuccessCallback( if (sessionSuccessCallback) {
session sessionSuccessCallback(session);
, message.data.mediaType); }
})); }));
} }
@@ -287,7 +374,10 @@ onMessage(async message => {
case "shim:/selectReceiverCancelled": { case "shim:/selectReceiverCancelled": {
if (sessionRequestInProgress) { if (sessionRequestInProgress) {
sessionRequestInProgress = false; sessionRequestInProgress = false;
sessionErrorCallback(new Error_(ErrorCode.CANCEL));
if (sessionErrorCallback) {
sessionErrorCallback(new Error_(ErrorCode.CANCEL));
}
} }
break; break;

View File

@@ -1,7 +1,7 @@
"use strict"; "use strict";
import { CAST_LOADER_SCRIPT_URL import { CAST_LOADER_SCRIPT_URL
, CAST_SCRIPT_URLS } from "../endpoints"; , CAST_SCRIPT_URLS } from "../lib/endpoints";
(window.wrappedJSObject as any).chrome = cloneInto({}, window); (window.wrappedJSObject as any).chrome = cloneInto({}, window);

View File

@@ -1,6 +1,7 @@
"use strict"; "use strict";
import { loadScript } from "../lib/utils"; import { loadScript } from "../lib/utils";
import { Message } from "../types";
import { onMessageResponse, sendMessage } from "./eventMessageChannel"; import { onMessageResponse, sendMessage } from "./eventMessageChannel";
@@ -17,12 +18,17 @@ if (isFramework) {
// Message port to background script // Message port to background script
const backgroundPort = browser.runtime.connect({ name: "shim" }); export const backgroundPort = browser.runtime.connect({ name: "shim" });
// Forward background messages to shim const forwardToShim = (message: Message) => sendMessage(message);
backgroundPort.onMessage.addListener(sendMessage); const forwardToMain = (message: Message) => backgroundPort.postMessage(message);
// Forward shim messages to background // Add message listeners
onMessageResponse(message => { backgroundPort.onMessage.addListener(forwardToShim);
backgroundPort.postMessage(message); const listener = onMessageResponse(forwardToMain);
// Remove listeners
backgroundPort.onDisconnect.addListener(() => {
backgroundPort.onMessage.removeListener(forwardToShim);
listener.disconnect();
}); });

View File

@@ -2,11 +2,14 @@
import * as cast from "./cast"; import * as cast from "./cast";
import { BridgeInfo } from "../lib/getBridgeInfo"; import { BridgeInfo } from "../lib/bridge";
import { Message } from "../types"; import { Message } from "../types";
import { onMessage } from "./eventMessageChannel"; import { onMessage } from "./eventMessageChannel";
let initializedBridgeInfo: BridgeInfo;
let initializedBackgroundPort: browser.runtime.Port;
/** /**
* To support exporting an API from a module, we need to * To support exporting an API from a module, we need to
* retain the event-based message passing despite not * retain the event-based message passing despite not
@@ -14,17 +17,44 @@ import { onMessage } from "./eventMessageChannel";
* for and emits these messages, and changing that behavior * for and emits these messages, and changing that behavior
* is too messy. * is too messy.
*/ */
export function init (): Promise<BridgeInfo> { export function ensureInit (): Promise<browser.runtime.Port> {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
// Trigger message port setup side-effects // If already initialized, just return existing bridge info
import("./contentBridge"); if (initializedBridgeInfo) {
if (initializedBridgeInfo.isVersionCompatible) {
resolve(initializedBackgroundPort);
} else {
reject();
}
return;
}
/**
* If the module is imported into a background script
* context, the location will be the internal extension URL,
* whereas in a content script, it will be the content page
* URL.
*/
if (window.location.protocol === "moz-extension:") {
//
} else {
// Trigger message port setup side-effects
const { backgroundPort } = await import("./contentBridge");
initializedBackgroundPort = backgroundPort;
}
onMessage(message => { onMessage(message => {
switch (message.subject) { switch (message.subject) {
case "shim:/initialized": { case "shim:/initialized": {
const bridgeInfo: BridgeInfo = message.data; initializedBridgeInfo = message.data;
resolve(bridgeInfo);
if (initializedBridgeInfo.isVersionCompatible) {
resolve(initializedBackgroundPort);
} else {
reject();
}
} }
} }
}); });

View File

@@ -2,7 +2,7 @@
import * as cast from "./cast"; import * as cast from "./cast";
import { CAST_FRAMEWORK_SCRIPT_URL } from "../endpoints"; import { CAST_FRAMEWORK_SCRIPT_URL } from "../lib/endpoints";
import { loadScript } from "../lib/utils"; import { loadScript } from "../lib/utils";
import { onMessage } from "./eventMessageChannel"; import { onMessage } from "./eventMessageChannel";
@@ -13,6 +13,10 @@ if (!_window.chrome) {
_window.chrome = {}; _window.chrome = {};
} }
// Remove private APIs
delete cast._requestSession;
// Create page-accessible API object // Create page-accessible API object
_window.chrome.cast = cast; _window.chrome.cast = cast;
@@ -61,7 +65,6 @@ if (document.currentScript) {
} }
} }
onMessage(message => { onMessage(message => {
switch (message.subject) { switch (message.subject) {
case "shim:/initialized": { case "shim:/initialized": {

View File

@@ -26,20 +26,3 @@ export interface ReceiverStatus {
muted: boolean muted: boolean
}; };
} }
export interface DownloadDelta {
id: number;
url?: browser.downloads.StringDelta;
filename?: browser.downloads.StringDelta;
danger?: browser.downloads.StringDelta;
mime?: browser.downloads.StringDelta;
startTime?: browser.downloads.StringDelta;
endTime?: browser.downloads.StringDelta;
state?: browser.downloads.StringDelta;
canResume?: browser.downloads.BooleanDelta;
paused?: browser.downloads.BooleanDelta;
error?: browser.downloads.StringDelta;
totalBytes?: browser.downloads.DoubleDelta;
fileSize?: browser.downloads.DoubleDelta;
exists?: browser.downloads.BooleanDelta;
}

View File

@@ -7,7 +7,7 @@ import semver from "semver";
import { getNextEllipsis import { getNextEllipsis
, getWindowCenteredProps } from "../../lib/utils"; , getWindowCenteredProps } from "../../lib/utils";
import { BridgeInfo } from "../../lib/getBridgeInfo"; import { BridgeInfo } from "../../lib/bridge";
const _ = browser.i18n.getMessage; const _ = browser.i18n.getMessage;

View File

@@ -4,13 +4,13 @@
import React, { Component } from "react"; import React, { Component } from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import defaultOptions, { Options } from "../../defaultOptions"; import defaultOptions from "../../defaultOptions";
import Bridge from "./Bridge"; import Bridge from "./Bridge";
import EditableList from "./EditableList"; import EditableList from "./EditableList";
import getBridgeInfo, { BridgeInfo } from "../../lib/getBridgeInfo"; import bridge, { BridgeInfo } from "../../lib/bridge";
import options from "../../lib/options"; import options, { Options } from "../../lib/options";
import { REMOTE_MATCH_PATTERN_REGEX } from "../../lib/utils"; import { REMOTE_MATCH_PATTERN_REGEX } from "../../lib/utils";
import { ReceiverSelectorType } from "../../receiver_selectors"; import { ReceiverSelectorType } from "../../receiver_selectors";
@@ -88,7 +88,7 @@ class OptionsApp extends Component<{}, OptionsAppState> {
, options: await options.getAll() , options: await options.getAll()
}); });
const bridgeInfo = await getBridgeInfo(); const bridgeInfo = await bridge.getInfo();
const { os } = await browser.runtime.getPlatformInfo(); const { os } = await browser.runtime.getPlatformInfo();
this.setState({ this.setState({
@@ -350,17 +350,8 @@ class OptionsApp extends Component<{}, OptionsAppState> {
this.form.reportValidity(); this.form.reportValidity();
try { try {
const oldOpts = await options.getAll();
await options.setAll(this.state.options); await options.setAll(this.state.options);
const alteredOptions = [];
for (const [ key, val ] of Object.entries(this.state.options)) {
const oldVal = oldOpts[key];
if (oldVal !== val) {
alteredOptions.push(key);
}
}
this.setState({ this.setState({
hasSaved: true hasSaved: true
}, () => { }, () => {
@@ -370,12 +361,6 @@ class OptionsApp extends Component<{}, OptionsAppState> {
}); });
}, 1000); }, 1000);
}); });
// Send update message / event
browser.runtime.sendMessage({
subject: "optionsUpdated"
, data: { alteredOptions }
});
} catch (err) { } catch (err) {
console.error("Failed to save options"); console.error("Failed to save options");
} }
@@ -421,7 +406,7 @@ class OptionsApp extends Component<{}, OptionsAppState> {
bridgeLoading: true bridgeLoading: true
}); });
const bridgeInfo = await getBridgeInfo(); const bridgeInfo = await bridge.getInfo();
this.setState({ this.setState({
bridgeInfo bridgeInfo

View File

@@ -179,11 +179,6 @@ class PopupApp extends Component<{}, PopupAppState> {
try { try {
const filePath = window.prompt(); const filePath = window.prompt();
// Validate URL
const fileUrl = new URL(filePath.startsWith("file://")
? filePath
: `file://${filePath}`);
this.setState({ this.setState({
mediaType mediaType
, filePath , filePath

6
package-lock.json generated
View File

@@ -8,6 +8,12 @@
"integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==",
"dev": true "dev": true
}, },
"@types/firefox-webext-browser": {
"version": "67.0.2",
"resolved": "https://registry.npmjs.org/@types/firefox-webext-browser/-/firefox-webext-browser-67.0.2.tgz",
"integrity": "sha512-tXR5THtH5Fqwfmi4y/2dTxCTebqHsKCH2bif/VdE5CPcB/S5x5fu+N+wBuMENzN41agovHMU/LQbtWNV+Aux+w==",
"dev": true
},
"@types/minimist": { "@types/minimist": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz",

View File

@@ -20,6 +20,7 @@
"lint:ext": "npm run lint --prefix ./ext" "lint:ext": "npm run lint --prefix ./ext"
}, },
"devDependencies": { "devDependencies": {
"@types/firefox-webext-browser": "^67.0.2",
"@types/minimist": "^1.2.0", "@types/minimist": "^1.2.0",
"@types/semver": "^5.5.0", "@types/semver": "^5.5.0",
"@types/uuid": "^3.4.4", "@types/uuid": "^3.4.4",

View File

@@ -22,6 +22,10 @@ describe("chrome", () => {
expect(chrome.cast.unescape).toBeDefined(); expect(chrome.cast.unescape).toBeDefined();
}); });
it("should not have private api methods", () => {
expect(chrome.cast._requestSession).toBeUndefined();
});
it("should have all api classes", () => { it("should have all api classes", () => {
expect(chrome.cast.ApiConfig).toBeDefined(); expect(chrome.cast.ApiConfig).toBeDefined();
expect(chrome.cast.DialRequest).toBeDefined(); expect(chrome.cast.DialRequest).toBeDefined();