mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-08 08:39: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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import mime from "mime-types";
|
||||
import path from "path";
|
||||
|
||||
import Media from "./Media";
|
||||
import MediaServer from "./MediaServer";
|
||||
import Session from "./Session";
|
||||
import StatusListener from "./StatusListener";
|
||||
|
||||
@@ -32,11 +31,11 @@ events.EventEmitter.defaultMaxListeners = 50;
|
||||
const browser = new dnssd.Browser(dnssd.tcp("googlecast"));
|
||||
|
||||
// Local media server
|
||||
let mediaServer: MediaServer;
|
||||
let mediaServer: http.Server;
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
if (mediaServer) {
|
||||
mediaServer.stop();
|
||||
if (mediaServer && mediaServer.listening) {
|
||||
mediaServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -47,18 +46,28 @@ const encodeTransform = new EncodeTransform();
|
||||
// stdin -> stdout
|
||||
process.stdin
|
||||
.pipe(decodeTransform)
|
||||
.pipe(new ResponseTransform(handleMessage))
|
||||
.pipe(encodeTransform)
|
||||
|
||||
decodeTransform.on("data", handleMessage);
|
||||
|
||||
encodeTransform
|
||||
.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 {
|
||||
encodeTransform.write(message);
|
||||
if (typeof message === "string") {
|
||||
encodeTransform.write({
|
||||
subject: message
|
||||
});
|
||||
} else {
|
||||
encodeTransform.write(message);
|
||||
}
|
||||
} 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();
|
||||
|
||||
let receiverSelectorApp: child_process.ChildProcess;
|
||||
let receiverSelectorAppClosed = true;
|
||||
|
||||
/**
|
||||
* Handle incoming messages from the extension and forward
|
||||
@@ -130,10 +140,18 @@ async function handleMessage (message: Message) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.subject.startsWith("bridge:/receiverSelector/")) {
|
||||
handleReceiverSelectorMessage(message);
|
||||
}
|
||||
|
||||
if (message.subject.startsWith("bridge:/mediaServer/")) {
|
||||
handleMediaServerMessage(message);
|
||||
}
|
||||
|
||||
switch (message.subject) {
|
||||
case "bridge:/getInfo": {
|
||||
const extensionVersion = message.data;
|
||||
return __applicationVersion;
|
||||
encodeTransform.write(__applicationVersion);
|
||||
}
|
||||
|
||||
case "bridge:/initialize": {
|
||||
@@ -142,8 +160,11 @@ async function handleMessage (message: Message) {
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function handleReceiverSelectorMessage (message: Message) {
|
||||
switch (message.subject) {
|
||||
case "bridge:/receiverSelector/open": {
|
||||
const receiverSelectorData = message.data;
|
||||
|
||||
@@ -167,6 +188,8 @@ async function handleMessage (message: Message) {
|
||||
path.join(process.cwd(), "selector")
|
||||
, [ receiverSelectorData ]);
|
||||
|
||||
receiverSelectorAppClosed = false;
|
||||
|
||||
receiverSelectorApp.stdout!.setEncoding("utf8");
|
||||
receiverSelectorApp.stdout!.on("data", data => {
|
||||
sendMessage({
|
||||
@@ -175,7 +198,7 @@ async function handleMessage (message: Message) {
|
||||
});
|
||||
});
|
||||
|
||||
receiverSelectorApp.addListener("error", err => {
|
||||
receiverSelectorApp.on("error", err => {
|
||||
sendMessage({
|
||||
subject: "main:/receiverSelector/error"
|
||||
, data: err.message
|
||||
@@ -183,9 +206,13 @@ async function handleMessage (message: Message) {
|
||||
});
|
||||
|
||||
receiverSelectorApp.on("close", () => {
|
||||
sendMessage({
|
||||
subject: "main:/receiverSelector/close"
|
||||
});
|
||||
if (!receiverSelectorAppClosed) {
|
||||
receiverSelectorAppClosed = true;
|
||||
|
||||
sendMessage({
|
||||
subject: "main:/receiverSelector/close"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
break;
|
||||
@@ -193,34 +220,78 @@ async function handleMessage (message: Message) {
|
||||
|
||||
case "bridge:/receiverSelector/close": {
|
||||
receiverSelectorApp.kill();
|
||||
receiverSelectorAppClosed = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function handleMediaServerMessage (message: Message) {
|
||||
switch (message.subject) {
|
||||
case "bridge:/mediaServer/start": {
|
||||
const { filePath, port } = message.data;
|
||||
const { filePath, port }
|
||||
: { filePath: string, port: number } = message.data;
|
||||
|
||||
mediaServer = new MediaServer(filePath, port);
|
||||
mediaServer.start();
|
||||
const contentType = mime.lookup(filePath);
|
||||
|
||||
mediaServer.on("started", () => {
|
||||
sendMessage({
|
||||
subject: "mediaCast:/mediaServer/started"
|
||||
});
|
||||
if (!contentType) {
|
||||
sendMessage("mediaCast:/mediaServer/error");
|
||||
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", () => {
|
||||
sendMessage({
|
||||
subject: "mediaCast:/mediaServer/stopped"
|
||||
});
|
||||
mediaServer.on("listening", () => {
|
||||
sendMessage("mediaCast:/mediaServer/started");
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
case "bridge:/mediaServer/stop": {
|
||||
if (mediaServer) {
|
||||
mediaServer.stop();
|
||||
if (mediaServer && mediaServer.listening) {
|
||||
mediaServer.close();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
@@ -70,7 +70,7 @@ const webpackConfig = require(`${ROOT}/webpack.config.js`)({
|
||||
webpackConfig.mode = argv.mode;
|
||||
webpackConfig.devtool = argv.mode === "production"
|
||||
? "none"
|
||||
: "eval";
|
||||
: "source-map";
|
||||
|
||||
|
||||
// Clean
|
||||
|
||||
2
ext/package-lock.json
generated
2
ext/package-lock.json
generated
@@ -6377,7 +6377,7 @@
|
||||
},
|
||||
"pretty-format": {
|
||||
"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=",
|
||||
"dev": true
|
||||
},
|
||||
|
||||
85
ext/src/SelectorManager.ts
Normal file
85
ext/src/SelectorManager.ts
Normal 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
154
ext/src/StatusManager.ts
Normal 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
278
ext/src/createMenus.ts
Normal 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
155
ext/src/createShim.ts
Normal 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
|
||||
};
|
||||
}
|
||||
@@ -1,29 +1,10 @@
|
||||
"use strict";
|
||||
|
||||
import { Options } from "./lib/options";
|
||||
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
|
||||
receiverSelectorCloseIfFocusLost: boolean;
|
||||
receiverSelectorWaitForConnection: boolean;
|
||||
|
||||
userAgentWhitelistEnabled: boolean;
|
||||
userAgentWhitelist: string[];
|
||||
|
||||
[key: string]: Options[keyof Options];
|
||||
}
|
||||
|
||||
const options: Options = {
|
||||
export default {
|
||||
bridgeApplicationName: APPLICATION_NAME
|
||||
, mediaEnabled: true
|
||||
, mediaSyncElement: false
|
||||
@@ -39,6 +20,4 @@ const options: Options = {
|
||||
, userAgentWhitelist: [
|
||||
"https://www.netflix.com/*"
|
||||
]
|
||||
};
|
||||
|
||||
export default options;
|
||||
} as Options;
|
||||
|
||||
4
ext/src/global.d.ts
vendored
4
ext/src/global.d.ts
vendored
@@ -37,8 +37,8 @@ declare interface RTCPeerConnection {
|
||||
}
|
||||
|
||||
declare interface MediaDevices {
|
||||
getDisplayMedia (constraints: MediaStreamConstraints)
|
||||
: Promise<MediaStream>;
|
||||
getDisplayMedia (constraints: MediaStreamConstraints)
|
||||
: Promise<MediaStream>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
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;
|
||||
|
||||
984
ext/src/main.ts
984
ext/src/main.ts
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,15 @@
|
||||
"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"
|
||||
, "default_locale": "en"
|
||||
, "manifest_version": 2
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,39 +1,56 @@
|
||||
"use strict";
|
||||
|
||||
import nativeMessaging from "../lib/nativeMessaging";
|
||||
import bridge from "../lib/bridge";
|
||||
import options from "../lib/options";
|
||||
|
||||
import ReceiverSelector, {
|
||||
ReceiverSelectorMediaType } from "./ReceiverSelector";
|
||||
ReceiverSelection
|
||||
, ReceiverSelectorEvents
|
||||
, ReceiverSelectorMediaType } from "./ReceiverSelector";
|
||||
|
||||
import { TypedEventTarget } from "../lib/typedEvents";
|
||||
import { getWindowCenteredProps } from "../lib/utils";
|
||||
import { Message, Receiver } from "../types";
|
||||
|
||||
import { NativeReceiverSelectorCloseMessage
|
||||
, NativeReceiverSelectorErrorMessage
|
||||
, NativeReceiverSelectorSelectedMessage } from "../messageTypes";
|
||||
|
||||
|
||||
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
|
||||
extends EventTarget
|
||||
extends TypedEventTarget<ReceiverSelectorEvents>
|
||||
implements ReceiverSelector {
|
||||
|
||||
private bridgePort: browser.runtime.Port;
|
||||
private bridgePortDisconnected: boolean = false;
|
||||
|
||||
private wasReceiverSelected: boolean = false;
|
||||
private _isOpen: boolean = false;
|
||||
|
||||
get isOpen () {
|
||||
return this._isOpen;
|
||||
}
|
||||
|
||||
public async open (
|
||||
receivers: Receiver[]
|
||||
, defaultMediaType: ReceiverSelectorMediaType
|
||||
, availableMediaTypes: ReceiverSelectorMediaType): Promise<void> {
|
||||
|
||||
const applicationName = await options.get("bridgeApplicationName");
|
||||
this.bridgePort = nativeMessaging.connectNative(applicationName);
|
||||
this.bridgePort = await bridge.connect();
|
||||
|
||||
this.bridgePort.onMessage.addListener((message: Message) => {
|
||||
switch (message.subject) {
|
||||
@@ -56,7 +73,9 @@ export default class NativeMacReceiverSelector
|
||||
});
|
||||
|
||||
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")
|
||||
})
|
||||
});
|
||||
|
||||
this._isOpen = true;
|
||||
}
|
||||
|
||||
public close (): void {
|
||||
if (this.bridgePort && !this.bridgePortDisconnected) {
|
||||
if (this.bridgePort) {
|
||||
this.bridgePort.postMessage({
|
||||
subject: "bridge:/receiverSelector/close"
|
||||
});
|
||||
}
|
||||
|
||||
this._isOpen = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -115,12 +138,12 @@ export default class NativeMacReceiverSelector
|
||||
this.dispatchEvent(new CustomEvent("cancelled"));
|
||||
}
|
||||
|
||||
if (!this.bridgePortDisconnected) {
|
||||
if (this.bridgePort) {
|
||||
this.bridgePort.disconnect();
|
||||
}
|
||||
|
||||
this.bridgePort = null;
|
||||
this.bridgePortDisconnected = false;
|
||||
this.wasReceiverSelected = false;
|
||||
this._isOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
"use strict";
|
||||
|
||||
import ReceiverSelector, {
|
||||
ReceiverSelectorMediaType } from "./ReceiverSelector";
|
||||
ReceiverSelectorEvents
|
||||
, ReceiverSelectorMediaType } from "./ReceiverSelector";
|
||||
|
||||
import { TypedEventTarget } from "../lib/typedEvents";
|
||||
import { getWindowCenteredProps } from "../lib/utils";
|
||||
import { Message, Receiver } from "../types";
|
||||
|
||||
|
||||
export default class PopupReceiverSelector
|
||||
extends EventTarget
|
||||
extends TypedEventTarget<ReceiverSelectorEvents>
|
||||
implements ReceiverSelector {
|
||||
|
||||
private windowId: number;
|
||||
private openerWindowId: number;
|
||||
|
||||
private messagePort: browser.runtime.Port;
|
||||
private messagePortDisconnected: boolean;
|
||||
|
||||
private receivers: Receiver[];
|
||||
private defaultMediaType: ReceiverSelectorMediaType;
|
||||
@@ -21,6 +25,8 @@ export default class PopupReceiverSelector
|
||||
|
||||
private wasReceiverSelected: boolean = false;
|
||||
|
||||
private _isOpen: boolean = false;
|
||||
|
||||
|
||||
constructor () {
|
||||
super();
|
||||
@@ -48,6 +54,9 @@ export default class PopupReceiverSelector
|
||||
|
||||
this.messagePort = port;
|
||||
this.messagePort.onMessage.addListener(this.onPopupMessage);
|
||||
this.messagePort.onDisconnect.addListener(() => {
|
||||
this.messagePortDisconnected = true;
|
||||
});
|
||||
|
||||
this.messagePort.postMessage({
|
||||
subject: "popup:/populateReceiverList"
|
||||
@@ -60,6 +69,9 @@ export default class PopupReceiverSelector
|
||||
});
|
||||
}
|
||||
|
||||
get isOpen () {
|
||||
return this._isOpen;
|
||||
}
|
||||
|
||||
public async open (
|
||||
receivers: Receiver[]
|
||||
@@ -85,6 +97,8 @@ export default class PopupReceiverSelector
|
||||
, ...centeredProps
|
||||
});
|
||||
|
||||
this._isOpen = true;
|
||||
|
||||
this.windowId = popup.id;
|
||||
this.openerWindowId = openerWindow.id;
|
||||
|
||||
@@ -98,8 +112,16 @@ export default class PopupReceiverSelector
|
||||
this.onWindowsFocusChanged);
|
||||
}
|
||||
|
||||
public close (): void {
|
||||
browser.windows.remove(this.windowId);
|
||||
public async close (): Promise<void> {
|
||||
if (this.windowId) {
|
||||
await browser.windows.remove(this.windowId);
|
||||
}
|
||||
|
||||
this._isOpen = false;
|
||||
|
||||
if (this.messagePort && !this.messagePortDisconnected) {
|
||||
this.messagePort.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use strict";
|
||||
|
||||
import { TypedEventTarget } from "../lib/typedEvents";
|
||||
import { Receiver } from "../types";
|
||||
|
||||
|
||||
@@ -16,12 +17,18 @@ export interface ReceiverSelection {
|
||||
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[]
|
||||
, defaultMediaType: ReceiverSelectorMediaType
|
||||
, availableMediaTypes: ReceiverSelectorMediaType): void;
|
||||
|
||||
@@ -6,12 +6,16 @@ import PopupReceiverSelector
|
||||
from "./PopupReceiverSelector";
|
||||
|
||||
|
||||
export { ReceiverSelection
|
||||
, ReceiverSelectorCancelledEvent
|
||||
, ReceiverSelectorErrorEvent
|
||||
, ReceiverSelectorMediaType
|
||||
, ReceiverSelectorSelectedEvent } from "./ReceiverSelector";
|
||||
import { ReceiverSelection
|
||||
, ReceiverSelectorMediaType } from "./ReceiverSelector";
|
||||
|
||||
type ReceiverSelector = ReturnType<typeof getReceiverSelector>;
|
||||
|
||||
export {
|
||||
ReceiverSelector
|
||||
, ReceiverSelection
|
||||
, ReceiverSelectorMediaType
|
||||
};
|
||||
|
||||
export enum ReceiverSelectorType {
|
||||
Popup
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
"use strict";
|
||||
|
||||
import { Options } from "../defaultOptions";
|
||||
import cast, { init } from "../shim/export";
|
||||
import mediaCasting from "../lib/mediaCasting";
|
||||
import options from "../lib/options";
|
||||
import cast, { ensureInit } from "../shim/export";
|
||||
|
||||
import { Message, Receiver } from "../types";
|
||||
|
||||
|
||||
// Variables passed from background
|
||||
const { srcUrl
|
||||
const { selectedReceiver
|
||||
, srcUrl
|
||||
, targetElementId }
|
||||
: { srcUrl: string
|
||||
: { selectedReceiver: Receiver
|
||||
, srcUrl: string
|
||||
, targetElementId: number } = (window as any);
|
||||
|
||||
|
||||
let options: Options;
|
||||
let backgroundPort: browser.runtime.Port;
|
||||
|
||||
let session: cast.Session;
|
||||
let currentMedia: cast.media.Media;
|
||||
@@ -24,16 +29,13 @@ const isLocalFile = srcUrl.startsWith("file:");
|
||||
const mediaElement = browser.menus.getTargetElement(
|
||||
targetElementId) as HTMLMediaElement;
|
||||
|
||||
window.addEventListener("beforeunload", () => {
|
||||
browser.runtime.sendMessage({
|
||||
window.addEventListener("beforeunload", async () => {
|
||||
backgroundPort.postMessage({
|
||||
subject: "bridge:/mediaServer/stop"
|
||||
});
|
||||
|
||||
if (options.mediaStopOnUnload) {
|
||||
if (await options.get("mediaStopOnUnload")) {
|
||||
session.stop(null, null);
|
||||
/*currentMedia.stop(null
|
||||
, onMediaStopSuccess
|
||||
, onMediaStopError);*/
|
||||
}
|
||||
});
|
||||
|
||||
@@ -55,42 +57,63 @@ function getLocalAddress () {
|
||||
}
|
||||
|
||||
|
||||
async function onRequestSessionSuccess (newSession: cast.Session) {
|
||||
cast.logMessage("onRequestSessionSuccess");
|
||||
|
||||
session = newSession;
|
||||
|
||||
let mediaUrl = new URL(srcUrl);
|
||||
const port = options.localMediaServerPort;
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
function startMediaServer (filePath: string, port: number) {
|
||||
return new Promise((resolve, reject) => {
|
||||
backgroundPort.postMessage({
|
||||
subject: "bridge:/mediaServer/start"
|
||||
, data: {
|
||||
filePath: decodeURI(filePath)
|
||||
, port
|
||||
}
|
||||
});
|
||||
|
||||
// Address of local HTTP server
|
||||
mediaUrl = new URL(`http://${await getLocalAddress()}:${port}/`);
|
||||
backgroundPort.onMessage.addListener(
|
||||
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);
|
||||
|
||||
// Media metadata (title/poster)
|
||||
mediaInfo.metadata = new cast.media.GenericMediaMetadata();
|
||||
mediaInfo.metadata.metadataType = cast.media.MetadataType.GENERIC;
|
||||
mediaInfo.metadata.title = mediaUrl.pathname;
|
||||
mediaInfo.metadata.title = mediaTitle;
|
||||
|
||||
if (mediaElement instanceof HTMLVideoElement && mediaElement.poster) {
|
||||
mediaInfo.metadata.images = [
|
||||
@@ -164,41 +187,13 @@ async function onRequestSessionSuccess (newSession: cast.Session) {
|
||||
, onLoadMediaError);
|
||||
}
|
||||
|
||||
function onRequestSessionError () {
|
||||
cast.logMessage("onRequestSessionError");
|
||||
}
|
||||
|
||||
|
||||
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) {
|
||||
async function onLoadMediaSuccess (media: cast.media.Media) {
|
||||
cast.logMessage("onLoadMediaSuccess");
|
||||
|
||||
currentMedia = media;
|
||||
|
||||
if (options.mediaSyncElement) {
|
||||
if (await options.get("mediaSyncElement")) {
|
||||
mediaElement.addEventListener("play", () => {
|
||||
if (ignoreMediaEvents) {
|
||||
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 () {
|
||||
cast.logMessage("onLoadMediaError");
|
||||
}
|
||||
|
||||
|
||||
/* play */
|
||||
function onMediaPlaySuccess () {
|
||||
cast.logMessage("onMediaPlaySuccess");
|
||||
}
|
||||
|
||||
function onMediaPlayError (err: cast.Error) {
|
||||
cast.logMessage("onMediaPlayError");
|
||||
}
|
||||
|
||||
|
||||
/* pause */
|
||||
function onMediaPauseSuccess () {
|
||||
cast.logMessage("onMediaPauseSuccess");
|
||||
}
|
||||
|
||||
function onMediaPauseError (err: cast.Error) {
|
||||
cast.logMessage("onMediaPauseError");
|
||||
}
|
||||
|
||||
|
||||
/* stop */
|
||||
function onMediaStopSuccess () {
|
||||
cast.logMessage("onMediaStopSuccess");
|
||||
}
|
||||
|
||||
function onMediaStopError (err: cast.Error) {
|
||||
cast.logMessage("onMediaStopError");
|
||||
}
|
||||
|
||||
|
||||
/* seek */
|
||||
function onMediaSeekSuccess () {
|
||||
cast.logMessage("onMediaSeekSuccess");
|
||||
}
|
||||
|
||||
function onMediaSeekError (err: cast.Error) {
|
||||
cast.logMessage("onMediaSeekError");
|
||||
}
|
||||
|
||||
|
||||
init().then(async bridgeInfo => {
|
||||
if (!bridgeInfo.isVersionCompatible) {
|
||||
console.error("__onGCastApiAvailable error");
|
||||
return;
|
||||
}
|
||||
ensureInit().then(async (port) => {
|
||||
backgroundPort = port;
|
||||
|
||||
options = (await browser.storage.sync.get("options")).options;
|
||||
|
||||
if (isLocalFile && !options.localMediaEnabled) {
|
||||
const isLocalMediaEnabled = await options.get("localMediaEnabled");
|
||||
if (isLocalFile && !isLocalMediaEnabled) {
|
||||
cast.logMessage("Local media casting not enabled");
|
||||
return;
|
||||
}
|
||||
|
||||
session = await mediaCasting.getMediaSession(selectedReceiver);
|
||||
|
||||
const sessionRequest = new cast.SessionRequest(
|
||||
cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID);
|
||||
|
||||
const apiConfig = new cast.ApiConfig(sessionRequest
|
||||
, sessionListener
|
||||
, receiverListener);
|
||||
|
||||
cast.initialize(apiConfig
|
||||
, onInitializeSuccess
|
||||
, onInitializeError);
|
||||
loadMedia();
|
||||
});
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
"use strict";
|
||||
|
||||
import options from "../lib/options";
|
||||
import cast, { init } from "../shim/export";
|
||||
import cast, { ensureInit } from "../shim/export";
|
||||
|
||||
import { ReceiverSelectorMediaType }
|
||||
from "../receiver_selectors/ReceiverSelector";
|
||||
|
||||
import { Receiver } from "../types";
|
||||
|
||||
|
||||
// Variables passed from background
|
||||
const { selectedMedia }
|
||||
: { selectedMedia: ReceiverSelectorMediaType } = (window as any);
|
||||
const { selectedMedia
|
||||
, selectedReceiver }
|
||||
: { selectedMedia: ReceiverSelectorMediaType
|
||||
, selectedReceiver: Receiver } = (window as any);
|
||||
|
||||
|
||||
const FX_CAST_RECEIVER_APP_NAMESPACE = "urn:x-cast:fx_cast";
|
||||
@@ -42,6 +46,10 @@ if (typeof navigator.mediaDevices.getDisplayMedia === "undefined") {
|
||||
* receiver device.
|
||||
*/
|
||||
function sendAppMessage (subject: string, data: any) {
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
session.sendMessage(FX_CAST_RECEIVER_APP_NAMESPACE, {
|
||||
subject
|
||||
, data
|
||||
@@ -54,9 +62,7 @@ window.addEventListener("beforeunload", () => {
|
||||
});
|
||||
|
||||
|
||||
async function onRequestSessionSuccess (
|
||||
newSession: cast.Session
|
||||
, newSelectedMedia: ReceiverSelectorMediaType) {
|
||||
async function onRequestSessionSuccess (newSession: cast.Session) {
|
||||
|
||||
cast.logMessage("onRequestSessionSuccess");
|
||||
|
||||
@@ -83,7 +89,7 @@ async function onRequestSessionSuccess (
|
||||
sendAppMessage("iceCandidate", ev.candidate);
|
||||
});
|
||||
|
||||
switch (newSelectedMedia) {
|
||||
switch (selectedMedia) {
|
||||
case ReceiverSelectorMediaType.Tab: {
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
@@ -161,8 +167,9 @@ function receiverListener (availability: string) {
|
||||
|
||||
if (availability === cast.ReceiverAvailability.AVAILABLE) {
|
||||
wasSessionRequested = true;
|
||||
cast.requestSession(
|
||||
onRequestSessionSuccess
|
||||
cast._requestSession(
|
||||
selectedReceiver
|
||||
, onRequestSessionSuccess
|
||||
, onRequestSessionError);
|
||||
}
|
||||
}
|
||||
@@ -182,13 +189,7 @@ function onInitializeError () {
|
||||
}
|
||||
|
||||
|
||||
init().then(async bridgeInfo => {
|
||||
if (!bridgeInfo.isVersionCompatible) {
|
||||
console.error("__onGCastApiAvailable error");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
ensureInit().then(async () => {
|
||||
const mirroringAppId = await options.get("mirroringAppId");
|
||||
const sessionRequest = new cast.SessionRequest(mirroringAppId);
|
||||
|
||||
@@ -196,9 +197,7 @@ init().then(async bridgeInfo => {
|
||||
sessionRequest
|
||||
, sessionListener
|
||||
, receiverListener
|
||||
, undefined, undefined
|
||||
, selectedMedia
|
||||
, availableMediaTypes);
|
||||
, undefined, undefined);
|
||||
|
||||
cast.initialize(apiConfig
|
||||
, onInitializeSuccess
|
||||
|
||||
@@ -6,9 +6,6 @@ import SessionRequest from "./SessionRequest";
|
||||
import { AutoJoinPolicy
|
||||
, DefaultActionPolicy } from "../enums";
|
||||
|
||||
import { ReceiverSelectorMediaType }
|
||||
from "../../../receiver_selectors/ReceiverSelector";
|
||||
|
||||
|
||||
export default class ApiConfig {
|
||||
public additionalSessionRequests: any[] = [];
|
||||
@@ -23,15 +20,5 @@ export default class ApiConfig {
|
||||
, public autoJoinPolicy: string
|
||||
= AutoJoinPolicy.TAB_AND_ORIGIN_SCOPED
|
||||
, public defaultActionPolicy: string
|
||||
= 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) {
|
||||
}
|
||||
= DefaultActionPolicy.CREATE_SESSION) {}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import ApiConfig from "./classes/ApiConfig";
|
||||
import DialRequest from "./classes/DialRequest";
|
||||
import Error_ from "./classes/Error";
|
||||
import Image_ from "./classes/Image";
|
||||
import Receiver from "./classes/Receiver";
|
||||
import Receiver_ from "./classes/Receiver";
|
||||
import ReceiverDisplayStatus from "./classes/ReceiverDisplayStatus";
|
||||
import SenderApplication from "./classes/SenderApplication";
|
||||
import Session from "./classes/Session";
|
||||
@@ -26,18 +26,18 @@ import { AutoJoinPolicy
|
||||
|
||||
import * as media from "./media";
|
||||
|
||||
|
||||
import { ReceiverSelectorMediaType }
|
||||
from "../../receiver_selectors/ReceiverSelector";
|
||||
from "../../receiver_selectors/ReceiverSelector";
|
||||
import { Receiver } from "../../types";
|
||||
import { onMessage, sendMessageResponse } from "../eventMessageChannel";
|
||||
|
||||
|
||||
type ReceiverActionListener = (
|
||||
receiver: Receiver
|
||||
receiver: Receiver_
|
||||
, receiverAction: string) => void;
|
||||
|
||||
type RequestSessionSuccessCallback = (
|
||||
session: Session
|
||||
, selectedMedia: ReceiverSelectorMediaType) => void;
|
||||
type RequestSessionSuccessCallback = (session: Session) => void;
|
||||
|
||||
type SuccessCallback = () => void;
|
||||
type ErrorCallback = (err: Error_) => void;
|
||||
@@ -61,12 +61,13 @@ export {
|
||||
, SenderPlatform, SessionStatus, VolumeControlType
|
||||
|
||||
// Classes
|
||||
, ApiConfig, DialRequest, Receiver, ReceiverDisplayStatus
|
||||
, ApiConfig, DialRequest, ReceiverDisplayStatus
|
||||
, SenderApplication, Session, SessionRequest, Timeout
|
||||
, Volume
|
||||
|
||||
, Error_ as Error
|
||||
, Image_ as Image
|
||||
, Receiver_ as Receiver
|
||||
|
||||
, media
|
||||
};
|
||||
@@ -84,14 +85,17 @@ export function addReceiverActionListener (
|
||||
|
||||
export function initialize (
|
||||
newApiConfig: ApiConfig
|
||||
, successCallback: SuccessCallback
|
||||
, errorCallback: ErrorCallback): void {
|
||||
, successCallback?: SuccessCallback
|
||||
, errorCallback?: ErrorCallback): void {
|
||||
|
||||
console.info("fx_cast (Debug): cast.initialize");
|
||||
|
||||
// Already initialized
|
||||
if (apiConfig) {
|
||||
errorCallback(new Error_(ErrorCode.INVALID_PARAMETER));
|
||||
if (errorCallback) {
|
||||
errorCallback(new Error_(ErrorCode.INVALID_PARAMETER));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -106,7 +110,9 @@ export function initialize (
|
||||
? ReceiverAvailability.AVAILABLE
|
||||
: ReceiverAvailability.UNAVAILABLE);
|
||||
|
||||
successCallback();
|
||||
if (successCallback) {
|
||||
successCallback();
|
||||
}
|
||||
}
|
||||
|
||||
export function logMessage (message: string): void {
|
||||
@@ -125,29 +131,37 @@ export function removeReceiverActionListener (
|
||||
}
|
||||
|
||||
export function requestSession (
|
||||
successCallback: RequestSessionSuccessCallback
|
||||
, errorCallback: ErrorCallback
|
||||
, sessionRequest: SessionRequest
|
||||
= apiConfig.sessionRequest): void {
|
||||
successCallback?: RequestSessionSuccessCallback
|
||||
, errorCallback?: ErrorCallback
|
||||
, sessionRequest: SessionRequest = apiConfig.sessionRequest): void {
|
||||
|
||||
console.info("fx_cast (Debug): cast.requestSession");
|
||||
|
||||
// Called before initialization
|
||||
if (!apiConfig) {
|
||||
errorCallback(new Error_(ErrorCode.API_NOT_INITIALIZED));
|
||||
if (errorCallback) {
|
||||
errorCallback(new Error_(ErrorCode.API_NOT_INITIALIZED));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Already requesting session
|
||||
if (sessionRequestInProgress) {
|
||||
errorCallback(new Error_(ErrorCode.INVALID_PARAMETER
|
||||
, "Session request already in progress."));
|
||||
if (errorCallback) {
|
||||
errorCallback(new Error_(ErrorCode.INVALID_PARAMETER
|
||||
, "Session request already in progress."));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// No available receivers
|
||||
if (!receiverList.length) {
|
||||
errorCallback(new Error_(ErrorCode.RECEIVER_UNAVAILABLE));
|
||||
if (errorCallback) {
|
||||
errorCallback(new Error_(ErrorCode.RECEIVER_UNAVAILABLE));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -159,21 +173,94 @@ export function requestSession (
|
||||
// Open destination chooser
|
||||
sendMessageResponse({
|
||||
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 {
|
||||
console.info("STUB :: cast.requestSessionById");
|
||||
}
|
||||
|
||||
export function setCustomReceivers (
|
||||
receivers: Receiver[]
|
||||
, successCallback: SuccessCallback
|
||||
, errorCallback: ErrorCallback): void {
|
||||
receivers: Receiver_[]
|
||||
, successCallback?: SuccessCallback
|
||||
, errorCallback?: ErrorCallback): void {
|
||||
|
||||
console.info("STUB :: cast.setCustomReceivers");
|
||||
}
|
||||
@@ -240,7 +327,7 @@ onMessage(async message => {
|
||||
case "shim:/selectReceiverEnd": {
|
||||
console.info("fx_cast (Debug): Selected receiver");
|
||||
|
||||
const selectedReceiver = new Receiver(
|
||||
const selectedReceiver = new Receiver_(
|
||||
message.data.receiver.id
|
||||
, message.data.receiver.friendlyName);
|
||||
|
||||
@@ -261,9 +348,9 @@ onMessage(async message => {
|
||||
|
||||
sessionRequestInProgress = false;
|
||||
|
||||
sessionSuccessCallback(
|
||||
session
|
||||
, message.data.mediaType);
|
||||
if (sessionSuccessCallback) {
|
||||
sessionSuccessCallback(session);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -287,7 +374,10 @@ onMessage(async message => {
|
||||
case "shim:/selectReceiverCancelled": {
|
||||
if (sessionRequestInProgress) {
|
||||
sessionRequestInProgress = false;
|
||||
sessionErrorCallback(new Error_(ErrorCode.CANCEL));
|
||||
|
||||
if (sessionErrorCallback) {
|
||||
sessionErrorCallback(new Error_(ErrorCode.CANCEL));
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
import { CAST_LOADER_SCRIPT_URL
|
||||
, CAST_SCRIPT_URLS } from "../endpoints";
|
||||
, CAST_SCRIPT_URLS } from "../lib/endpoints";
|
||||
|
||||
|
||||
(window.wrappedJSObject as any).chrome = cloneInto({}, window);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
import { loadScript } from "../lib/utils";
|
||||
import { Message } from "../types";
|
||||
import { onMessageResponse, sendMessage } from "./eventMessageChannel";
|
||||
|
||||
|
||||
@@ -17,12 +18,17 @@ if (isFramework) {
|
||||
|
||||
|
||||
// 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
|
||||
backgroundPort.onMessage.addListener(sendMessage);
|
||||
const forwardToShim = (message: Message) => sendMessage(message);
|
||||
const forwardToMain = (message: Message) => backgroundPort.postMessage(message);
|
||||
|
||||
// Forward shim messages to background
|
||||
onMessageResponse(message => {
|
||||
backgroundPort.postMessage(message);
|
||||
// Add message listeners
|
||||
backgroundPort.onMessage.addListener(forwardToShim);
|
||||
const listener = onMessageResponse(forwardToMain);
|
||||
|
||||
// Remove listeners
|
||||
backgroundPort.onDisconnect.addListener(() => {
|
||||
backgroundPort.onMessage.removeListener(forwardToShim);
|
||||
listener.disconnect();
|
||||
});
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
import * as cast from "./cast";
|
||||
|
||||
import { BridgeInfo } from "../lib/getBridgeInfo";
|
||||
import { BridgeInfo } from "../lib/bridge";
|
||||
import { Message } from "../types";
|
||||
import { onMessage } from "./eventMessageChannel";
|
||||
|
||||
|
||||
let initializedBridgeInfo: BridgeInfo;
|
||||
let initializedBackgroundPort: browser.runtime.Port;
|
||||
|
||||
/**
|
||||
* To support exporting an API from a module, we need to
|
||||
* 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
|
||||
* is too messy.
|
||||
*/
|
||||
export function init (): Promise<BridgeInfo> {
|
||||
export function ensureInit (): Promise<browser.runtime.Port> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
|
||||
// Trigger message port setup side-effects
|
||||
import("./contentBridge");
|
||||
// If already initialized, just return existing bridge info
|
||||
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 => {
|
||||
switch (message.subject) {
|
||||
case "shim:/initialized": {
|
||||
const bridgeInfo: BridgeInfo = message.data;
|
||||
resolve(bridgeInfo);
|
||||
initializedBridgeInfo = message.data;
|
||||
|
||||
if (initializedBridgeInfo.isVersionCompatible) {
|
||||
resolve(initializedBackgroundPort);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
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 { onMessage } from "./eventMessageChannel";
|
||||
|
||||
@@ -13,6 +13,10 @@ if (!_window.chrome) {
|
||||
_window.chrome = {};
|
||||
}
|
||||
|
||||
|
||||
// Remove private APIs
|
||||
delete cast._requestSession;
|
||||
|
||||
// Create page-accessible API object
|
||||
_window.chrome.cast = cast;
|
||||
|
||||
@@ -61,7 +65,6 @@ if (document.currentScript) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onMessage(message => {
|
||||
switch (message.subject) {
|
||||
case "shim:/initialized": {
|
||||
|
||||
@@ -26,20 +26,3 @@ export interface ReceiverStatus {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import semver from "semver";
|
||||
import { getNextEllipsis
|
||||
, getWindowCenteredProps } from "../../lib/utils";
|
||||
|
||||
import { BridgeInfo } from "../../lib/getBridgeInfo";
|
||||
import { BridgeInfo } from "../../lib/bridge";
|
||||
|
||||
const _ = browser.i18n.getMessage;
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
import React, { Component } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import defaultOptions, { Options } from "../../defaultOptions";
|
||||
import defaultOptions from "../../defaultOptions";
|
||||
|
||||
import Bridge from "./Bridge";
|
||||
import EditableList from "./EditableList";
|
||||
|
||||
import getBridgeInfo, { BridgeInfo } from "../../lib/getBridgeInfo";
|
||||
import options from "../../lib/options";
|
||||
import bridge, { BridgeInfo } from "../../lib/bridge";
|
||||
import options, { Options } from "../../lib/options";
|
||||
import { REMOTE_MATCH_PATTERN_REGEX } from "../../lib/utils";
|
||||
|
||||
import { ReceiverSelectorType } from "../../receiver_selectors";
|
||||
@@ -88,7 +88,7 @@ class OptionsApp extends Component<{}, OptionsAppState> {
|
||||
, options: await options.getAll()
|
||||
});
|
||||
|
||||
const bridgeInfo = await getBridgeInfo();
|
||||
const bridgeInfo = await bridge.getInfo();
|
||||
const { os } = await browser.runtime.getPlatformInfo();
|
||||
|
||||
this.setState({
|
||||
@@ -350,17 +350,8 @@ class OptionsApp extends Component<{}, OptionsAppState> {
|
||||
this.form.reportValidity();
|
||||
|
||||
try {
|
||||
const oldOpts = await options.getAll();
|
||||
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({
|
||||
hasSaved: true
|
||||
}, () => {
|
||||
@@ -370,12 +361,6 @@ class OptionsApp extends Component<{}, OptionsAppState> {
|
||||
});
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
// Send update message / event
|
||||
browser.runtime.sendMessage({
|
||||
subject: "optionsUpdated"
|
||||
, data: { alteredOptions }
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to save options");
|
||||
}
|
||||
@@ -421,7 +406,7 @@ class OptionsApp extends Component<{}, OptionsAppState> {
|
||||
bridgeLoading: true
|
||||
});
|
||||
|
||||
const bridgeInfo = await getBridgeInfo();
|
||||
const bridgeInfo = await bridge.getInfo();
|
||||
|
||||
this.setState({
|
||||
bridgeInfo
|
||||
|
||||
@@ -179,11 +179,6 @@ class PopupApp extends Component<{}, PopupAppState> {
|
||||
try {
|
||||
const filePath = window.prompt();
|
||||
|
||||
// Validate URL
|
||||
const fileUrl = new URL(filePath.startsWith("file://")
|
||||
? filePath
|
||||
: `file://${filePath}`);
|
||||
|
||||
this.setState({
|
||||
mediaType
|
||||
, filePath
|
||||
|
||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -8,6 +8,12 @@
|
||||
"integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==",
|
||||
"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": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz",
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"lint:ext": "npm run lint --prefix ./ext"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/firefox-webext-browser": "^67.0.2",
|
||||
"@types/minimist": "^1.2.0",
|
||||
"@types/semver": "^5.5.0",
|
||||
"@types/uuid": "^3.4.4",
|
||||
|
||||
@@ -22,6 +22,10 @@ describe("chrome", () => {
|
||||
expect(chrome.cast.unescape).toBeDefined();
|
||||
});
|
||||
|
||||
it("should not have private api methods", () => {
|
||||
expect(chrome.cast._requestSession).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should have all api classes", () => {
|
||||
expect(chrome.cast.ApiConfig).toBeDefined();
|
||||
expect(chrome.cast.DialRequest).toBeDefined();
|
||||
|
||||
Reference in New Issue
Block a user