mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-12 18:39:58 +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 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;
|
||||||
|
|||||||
@@ -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
2
ext/package-lock.json
generated
@@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
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";
|
"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
4
ext/src/global.d.ts
vendored
@@ -37,8 +37,8 @@ declare interface RTCPeerConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
declare interface MediaDevices {
|
declare interface MediaDevices {
|
||||||
getDisplayMedia (constraints: MediaStreamConstraints)
|
getDisplayMedia (constraints: MediaStreamConstraints)
|
||||||
: Promise<MediaStream>;
|
: Promise<MediaStream>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
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";
|
"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
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
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 */
|
/* 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;
|
||||||
|
|||||||
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" ]
|
"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
|
||||||
|
|||||||
@@ -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";
|
"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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
6
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user