mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-12 02:29:59 +00:00
Allow mediaCast sender to run in background context
This commit is contained in:
@@ -276,11 +276,9 @@ function handleMediaServerMessage (message: Message) {
|
|||||||
sendMessage("mediaCast:/mediaServer/started");
|
sendMessage("mediaCast:/mediaServer/started");
|
||||||
});
|
});
|
||||||
mediaServer.on("close", () => {
|
mediaServer.on("close", () => {
|
||||||
console.error("mediaServer close");
|
|
||||||
sendMessage("mediaCast:/mediaServer/stopped");
|
sendMessage("mediaCast:/mediaServer/stopped");
|
||||||
});
|
});
|
||||||
mediaServer.on("error", (a) => {
|
mediaServer.on("error", (a) => {
|
||||||
console.error("mediaServer error", a);
|
|
||||||
sendMessage("mediaCast:/mediaServer/error");
|
sendMessage("mediaCast:/mediaServer/error");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ async function getSelection (
|
|||||||
ReceiverSelectorMediaType.Tab
|
ReceiverSelectorMediaType.Tab
|
||||||
, availableMediaTypes =
|
, availableMediaTypes =
|
||||||
ReceiverSelectorMediaType.Tab
|
ReceiverSelectorMediaType.Tab
|
||||||
| ReceiverSelectorMediaType.Screen)
|
| ReceiverSelectorMediaType.Screen
|
||||||
// | ReceiverSelectorMediaType.File)
|
| ReceiverSelectorMediaType.File)
|
||||||
: Promise<ReceiverSelection> {
|
: Promise<ReceiverSelection> {
|
||||||
|
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
|
|||||||
@@ -13,151 +13,218 @@ import SelectorManager from "./SelectorManager";
|
|||||||
import StatusManager from "./StatusManager";
|
import StatusManager from "./StatusManager";
|
||||||
|
|
||||||
|
|
||||||
|
type Port = browser.runtime.Port | MessagePort;
|
||||||
|
|
||||||
export interface Shim {
|
export interface Shim {
|
||||||
bridgePort: browser.runtime.Port;
|
bridgePort: browser.runtime.Port;
|
||||||
contentPort?: browser.runtime.Port;
|
contentPort: Port;
|
||||||
contentTabId?: number;
|
contentTabId?: number;
|
||||||
contentFrameId?: number;
|
contentFrameId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function createShim (
|
|
||||||
port: browser.runtime.Port): Promise<Shim> {
|
|
||||||
|
|
||||||
const contentPort = port;
|
const activeShims = new Set<Shim>();
|
||||||
const contentTabId = port.sender.tab.id;
|
|
||||||
const contentFrameId = port.sender.frameId;
|
|
||||||
|
|
||||||
const bridgePort = await bridge.connect();
|
StatusManager.addEventListener("serviceUp", ev => {
|
||||||
|
for (const shim of activeShims) {
|
||||||
|
shim.contentPort.postMessage({
|
||||||
/**
|
subject: "shim:/serviceUp"
|
||||||
* If either the bridge port or the content port disconnects,
|
, data: { id: ev.detail.id }
|
||||||
* 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);
|
StatusManager.addEventListener("serviceDown", ev => {
|
||||||
contentPort.onDisconnect.addListener(onDisconnect);
|
for (const shim of activeShims) {
|
||||||
|
shim.contentPort.postMessage({
|
||||||
|
subject: "shim:/serviceDown"
|
||||||
// Add listeners
|
, data: { id: ev.detail.id }
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: If we're closing a selector, make sure it's the
|
|
||||||
* same one that caused the session creation.
|
|
||||||
*/
|
|
||||||
case "main:/sessionCreated": {
|
|
||||||
const selector = await SelectorManager.getSharedSelector();
|
|
||||||
|
|
||||||
const shouldClose = await options.get(
|
|
||||||
"receiverSelectorWaitForConnection");
|
|
||||||
|
|
||||||
if (selector.isOpen && shouldClose) {
|
|
||||||
selector.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
contentPort.postMessage({
|
async function createShim (port: Port): Promise<void> {
|
||||||
|
const shim = await (port instanceof MessagePort
|
||||||
|
? createShimFromBackground(port)
|
||||||
|
: createShimFromContent(port));
|
||||||
|
|
||||||
|
shim.contentPort.postMessage({
|
||||||
subject: "shim:/initialized"
|
subject: "shim:/initialized"
|
||||||
, data: await bridge.getInfo()
|
, data: await bridge.getInfo()
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
activeShims.add(shim);
|
||||||
bridgePort
|
|
||||||
, contentPort
|
|
||||||
, contentTabId
|
|
||||||
, contentFrameId
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function createShimFromBackground (
|
||||||
|
contentPort: MessagePort): Promise<Shim> {
|
||||||
|
|
||||||
|
const shim: Shim = {
|
||||||
|
bridgePort: await bridge.connect()
|
||||||
|
, contentPort
|
||||||
|
};
|
||||||
|
|
||||||
|
shim.bridgePort.onDisconnect.addListener(() => {
|
||||||
|
contentPort.close();
|
||||||
|
activeShims.delete(shim);
|
||||||
|
});
|
||||||
|
|
||||||
|
shim.bridgePort.onMessage.addListener((message: Message) => {
|
||||||
|
contentPort.postMessage(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
contentPort.onmessage = ev => {
|
||||||
|
const message = ev.data as Message;
|
||||||
|
handleContentMessage(shim, message);
|
||||||
|
};
|
||||||
|
|
||||||
|
return shim;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function createShimFromContent (
|
||||||
|
contentPort: browser.runtime.Port): Promise<Shim> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there's already an active shim for the sender
|
||||||
|
* tab/frame ID, disconnect it.
|
||||||
|
*/
|
||||||
|
for (const activeShim of activeShims) {
|
||||||
|
if (activeShim.contentTabId === contentPort.sender.tab.id
|
||||||
|
&& activeShim.contentFrameId === contentPort.sender.frameId) {
|
||||||
|
activeShim.bridgePort.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const shim: Shim = {
|
||||||
|
bridgePort: await bridge.connect()
|
||||||
|
, contentPort
|
||||||
|
, contentTabId: contentPort.sender.tab.id
|
||||||
|
, contentFrameId: contentPort.sender.frameId
|
||||||
|
};
|
||||||
|
|
||||||
|
function onContentPortMessage (message: Message) {
|
||||||
|
handleContentMessage(shim, message);
|
||||||
|
}
|
||||||
|
function onBridgePortMessage (message: Message) {
|
||||||
|
contentPort.postMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDisconnect () {
|
||||||
|
shim.bridgePort.onMessage.removeListener(onBridgePortMessage);
|
||||||
|
contentPort.onMessage.removeListener(onContentPortMessage);
|
||||||
|
|
||||||
|
shim.bridgePort.disconnect();
|
||||||
|
contentPort.disconnect();
|
||||||
|
|
||||||
|
activeShims.delete(shim);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
shim.bridgePort.onDisconnect.addListener(onDisconnect);
|
||||||
|
shim.bridgePort.onMessage.addListener(onBridgePortMessage);
|
||||||
|
|
||||||
|
contentPort.onDisconnect.addListener(onDisconnect);
|
||||||
|
contentPort.onMessage.addListener(onContentPortMessage);
|
||||||
|
|
||||||
|
|
||||||
|
return shim;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function handleContentMessage (shim: Shim, message: Message) {
|
||||||
|
const [ destination ] = message.subject.split(":/");
|
||||||
|
if (destination === "bridge") {
|
||||||
|
shim.bridgePort.postMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (message.subject) {
|
||||||
|
case "main:/shimInitialized": {
|
||||||
|
for (const receiver of StatusManager.getReceivers()) {
|
||||||
|
shim.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) {
|
||||||
|
shim.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) {
|
||||||
|
shim.contentPort.postMessage({
|
||||||
|
subject: "shim:/selectReceiverCancelled"
|
||||||
|
});
|
||||||
|
|
||||||
|
loadSender({
|
||||||
|
tabId: shim.contentTabId
|
||||||
|
, frameId: shim.contentFrameId
|
||||||
|
, selection
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass selection back to shim
|
||||||
|
shim.contentPort.postMessage({
|
||||||
|
subject: "shim:/selectReceiverEnd"
|
||||||
|
, data: selection
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
// TODO: Report errors properly
|
||||||
|
shim.contentPort.postMessage({
|
||||||
|
subject: "shim:/selectReceiverCancelled"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: If we're closing a selector, make sure it's the
|
||||||
|
* same one that caused the session creation.
|
||||||
|
*/
|
||||||
|
case "main:/sessionCreated": {
|
||||||
|
const selector = await SelectorManager.getSharedSelector();
|
||||||
|
|
||||||
|
const shouldClose = await options.get(
|
||||||
|
"receiverSelectorWaitForConnection");
|
||||||
|
|
||||||
|
if (selector.isOpen && shouldClose) {
|
||||||
|
selector.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createShim;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default {
|
|||||||
, mirroringAppId: MIRRORING_APP_ID
|
, mirroringAppId: MIRRORING_APP_ID
|
||||||
, receiverSelectorType: ReceiverSelectorType.Popup
|
, receiverSelectorType: ReceiverSelectorType.Popup
|
||||||
, receiverSelectorCloseIfFocusLost: true
|
, receiverSelectorCloseIfFocusLost: true
|
||||||
, receiverSelectorWaitForConnection: false
|
, receiverSelectorWaitForConnection: true
|
||||||
, userAgentWhitelistEnabled: true
|
, userAgentWhitelistEnabled: true
|
||||||
, userAgentWhitelist: [
|
, userAgentWhitelist: [
|
||||||
"https://www.netflix.com/*"
|
"https://www.netflix.com/*"
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
import mediaCasting from "./mediaCasting";
|
|
||||||
|
|
||||||
import { stringify } from "./utils";
|
import { stringify } from "./utils";
|
||||||
|
|
||||||
import { ReceiverSelection
|
import { ReceiverSelection
|
||||||
@@ -45,8 +43,12 @@ export default async function loadSender (opts: LoadSenderOptions) {
|
|||||||
|
|
||||||
case ReceiverSelectorMediaType.File: {
|
case ReceiverSelectorMediaType.File: {
|
||||||
const fileUrl = new URL(`file://${opts.selection.filePath}`);
|
const fileUrl = new URL(`file://${opts.selection.filePath}`);
|
||||||
const mediaSession = await mediaCasting.loadMediaUrl(
|
const { init } = await import("../senders/mediaCast");
|
||||||
fileUrl.href, opts.selection.receiver);
|
|
||||||
|
init({
|
||||||
|
mediaUrl: fileUrl.href
|
||||||
|
, receiver: opts.selection.receiver
|
||||||
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
"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
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
import defaultOptions from "./defaultOptions";
|
import defaultOptions from "./defaultOptions";
|
||||||
import bridge from "./lib/bridge";
|
import bridge from "./lib/bridge";
|
||||||
import loadSender from "./lib/loadSender";
|
import loadSender from "./lib/loadSender";
|
||||||
import mediaCasting from "./lib/mediaCasting";
|
|
||||||
import options, { Options } from "./lib/options";
|
import options, { Options } from "./lib/options";
|
||||||
|
|
||||||
import { getChromeUserAgent } from "./lib/userAgents";
|
import { getChromeUserAgent } from "./lib/userAgents";
|
||||||
@@ -49,6 +48,17 @@ browser.runtime.onInstalled.addListener(async details => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a message port connection with the name "shim" is
|
||||||
|
* established, pass it to createShim to handle the setup
|
||||||
|
* and maintenance.
|
||||||
|
*/
|
||||||
|
browser.runtime.onConnect.addListener(async port => {
|
||||||
|
if (port.name === "shim") {
|
||||||
|
createShim(port);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When the browser action is clicked, open a receiver
|
* When the browser action is clicked, open a receiver
|
||||||
* selector and load a sender for the response. The
|
* selector and load a sender for the response. The
|
||||||
@@ -66,61 +76,6 @@ browser.browserAction.onClicked.addListener(async tab => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const activeShims = new Set<Shim>();
|
|
||||||
|
|
||||||
browser.runtime.onConnect.addListener(async port => {
|
|
||||||
if (port.name === "shim") {
|
|
||||||
/**
|
|
||||||
* If there's already an active shim for the sender
|
|
||||||
* tab/frame ID, disconnect it.
|
|
||||||
*/
|
|
||||||
for (const activeShim of activeShims) {
|
|
||||||
if (activeShim.contentTabId === port.sender.tab.id
|
|
||||||
&& activeShim.contentFrameId === port.sender.frameId) {
|
|
||||||
|
|
||||||
activeShim.contentPort.disconnect();
|
|
||||||
activeShim.bridgePort.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const shim = await createShim(port);
|
|
||||||
|
|
||||||
shim.bridgePort.onDisconnect.addListener(() => {
|
|
||||||
activeShims.delete(shim);
|
|
||||||
});
|
|
||||||
shim.contentPort.onDisconnect.addListener(() => {
|
|
||||||
activeShims.delete(shim);
|
|
||||||
});
|
|
||||||
|
|
||||||
activeShims.add(shim);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
StatusManager.addEventListener("serviceUp", ev => {
|
|
||||||
for (const shim of activeShims) {
|
|
||||||
shim.contentPort.postMessage({
|
|
||||||
subject: "shim:/serviceUp"
|
|
||||||
, data: { id: ev.detail.id }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
StatusManager.addEventListener("serviceDown", ev => {
|
|
||||||
for (const shim of activeShims) {
|
|
||||||
shim.contentPort.postMessage({
|
|
||||||
subject: "shim:/serviceDown"
|
|
||||||
, data: { id: ev.detail.id }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let mediaCastTabId: number;
|
|
||||||
let mediaCastFrameId: number;
|
|
||||||
|
|
||||||
async function initMenus () {
|
async function initMenus () {
|
||||||
console.info("fx_cast (Debug): init (menus)");
|
console.info("fx_cast (Debug): init (menus)");
|
||||||
|
|
||||||
@@ -135,8 +90,8 @@ async function initMenus () {
|
|||||||
const allMediaTypes =
|
const allMediaTypes =
|
||||||
ReceiverSelectorMediaType.App
|
ReceiverSelectorMediaType.App
|
||||||
| ReceiverSelectorMediaType.Tab
|
| ReceiverSelectorMediaType.Tab
|
||||||
| ReceiverSelectorMediaType.Screen;
|
| ReceiverSelectorMediaType.Screen
|
||||||
// | ReceiverSelectorMediaType.File;
|
| ReceiverSelectorMediaType.File;
|
||||||
|
|
||||||
const selection = await SelectorManager.getSelection(
|
const selection = await SelectorManager.getSelection(
|
||||||
ReceiverSelectorMediaType.App
|
ReceiverSelectorMediaType.App
|
||||||
@@ -154,8 +109,8 @@ async function initMenus () {
|
|||||||
if (selection.mediaType === ReceiverSelectorMediaType.App) {
|
if (selection.mediaType === ReceiverSelectorMediaType.App) {
|
||||||
await browser.tabs.executeScript(tab.id, {
|
await browser.tabs.executeScript(tab.id, {
|
||||||
code: stringify`
|
code: stringify`
|
||||||
window.selectedReceiver = ${selection.receiver};
|
window.receiver = ${selection.receiver};
|
||||||
window.srcUrl = ${info.srcUrl};
|
window.mediaUrl = ${info.srcUrl};
|
||||||
window.targetElementId = ${info.targetElementId};
|
window.targetElementId = ${info.targetElementId};
|
||||||
`
|
`
|
||||||
, frameId: info.frameId
|
, frameId: info.frameId
|
||||||
@@ -165,10 +120,6 @@ async function initMenus () {
|
|||||||
file: "senders/mediaCast.js"
|
file: "senders/mediaCast.js"
|
||||||
, frameId: info.frameId
|
, frameId: info.frameId
|
||||||
});
|
});
|
||||||
|
|
||||||
// Store for later
|
|
||||||
mediaCastTabId = tab.id;
|
|
||||||
mediaCastFrameId = info.frameId;
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// Handle other responses
|
// Handle other responses
|
||||||
|
|||||||
@@ -1,44 +1,11 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
import mediaCasting from "../lib/mediaCasting";
|
|
||||||
import options from "../lib/options";
|
import options from "../lib/options";
|
||||||
import cast, { ensureInit } from "../shim/export";
|
import cast, { ensureInit } from "../shim/export";
|
||||||
|
|
||||||
import { Message, Receiver } from "../types";
|
import { Message, Receiver } from "../types";
|
||||||
|
|
||||||
|
|
||||||
// Variables passed from background
|
|
||||||
const { selectedReceiver
|
|
||||||
, srcUrl
|
|
||||||
, targetElementId }
|
|
||||||
: { selectedReceiver: Receiver
|
|
||||||
, srcUrl: string
|
|
||||||
, targetElementId: number } = (window as any);
|
|
||||||
|
|
||||||
|
|
||||||
let backgroundPort: browser.runtime.Port;
|
|
||||||
|
|
||||||
let session: cast.Session;
|
|
||||||
let currentMedia: cast.media.Media;
|
|
||||||
|
|
||||||
let ignoreMediaEvents = false;
|
|
||||||
|
|
||||||
|
|
||||||
const isLocalFile = srcUrl.startsWith("file:");
|
|
||||||
|
|
||||||
const mediaElement = browser.menus.getTargetElement(
|
|
||||||
targetElementId) as HTMLMediaElement;
|
|
||||||
|
|
||||||
window.addEventListener("beforeunload", async () => {
|
|
||||||
backgroundPort.postMessage({
|
|
||||||
subject: "bridge:/mediaServer/stop"
|
|
||||||
});
|
|
||||||
|
|
||||||
if (await options.get("mediaStopOnUnload")) {
|
|
||||||
session.stop(null, null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function getLocalAddress () {
|
function getLocalAddress () {
|
||||||
const pc = new RTCPeerConnection();
|
const pc = new RTCPeerConnection();
|
||||||
pc.createDataChannel(null);
|
pc.createDataChannel(null);
|
||||||
@@ -56,7 +23,6 @@ function getLocalAddress () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function startMediaServer (filePath: string, port: number) {
|
function startMediaServer (filePath: string, port: number) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
backgroundPort.postMessage({
|
backgroundPort.postMessage({
|
||||||
@@ -67,177 +33,235 @@ function startMediaServer (filePath: string, port: number) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
backgroundPort.onMessage.addListener(
|
backgroundPort.addEventListener("message", function onMessage (ev) {
|
||||||
function onMessage (message: Message) {
|
const message = ev.data as Message;
|
||||||
|
|
||||||
|
if (message.subject.startsWith("mediaCast:/mediaServer/")) {
|
||||||
|
backgroundPort.removeEventListener("message", onMessage);
|
||||||
|
}
|
||||||
|
|
||||||
switch (message.subject) {
|
switch (message.subject) {
|
||||||
case "mediaCast:/mediaServer/started": {
|
case "mediaCast:/mediaServer/started": {
|
||||||
backgroundPort.onMessage.removeListener(onMessage);
|
|
||||||
resolve();
|
resolve();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
case "mediaCast:/mediaServer/error": {
|
case "mediaCast:/mediaServer/error": {
|
||||||
backgroundPort.onMessage.removeListener(onMessage);
|
|
||||||
reject();
|
reject();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
backgroundPort.start();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadMedia () {
|
|
||||||
let mediaUrl = new URL(srcUrl);
|
|
||||||
const mediaTitle = mediaUrl.pathname;
|
|
||||||
|
|
||||||
/**
|
let backgroundPort: MessagePort;
|
||||||
* 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 {
|
let currentSession: cast.Session;
|
||||||
// Wait until media server is listening
|
let currentMedia: cast.media.Media;
|
||||||
await startMediaServer(mediaUrl.pathname, port);
|
|
||||||
} catch (err) {
|
let mediaElement: HTMLMediaElement;
|
||||||
console.error("Failed to start media server");
|
|
||||||
return;
|
|
||||||
|
function getSession (opts: InitOptions): Promise<cast.Session> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
/**
|
||||||
|
* 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 (opts.receiver) {
|
||||||
|
cast._requestSession(
|
||||||
|
opts.receiver
|
||||||
|
, onRequestSessionSuccess
|
||||||
|
, onRequestSessionError);
|
||||||
|
} else {
|
||||||
|
cast.requestSession(
|
||||||
|
onRequestSessionSuccess
|
||||||
|
, onRequestSessionError);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaUrl = new URL(`http://${host}:${port}/`);
|
function onRequestSessionSuccess (session: cast.Session) {
|
||||||
}
|
resolve(session);
|
||||||
|
}
|
||||||
|
function onRequestSessionError (err: cast.Error) {
|
||||||
const mediaInfo = new cast.media.MediaInfo(mediaUrl.href, null);
|
reject(err.description);
|
||||||
|
|
||||||
// Media metadata (title/poster)
|
|
||||||
mediaInfo.metadata = new cast.media.GenericMediaMetadata();
|
|
||||||
mediaInfo.metadata.metadataType = cast.media.MetadataType.GENERIC;
|
|
||||||
mediaInfo.metadata.title = mediaTitle;
|
|
||||||
|
|
||||||
if (mediaElement instanceof HTMLVideoElement && mediaElement.poster) {
|
|
||||||
mediaInfo.metadata.images = [
|
|
||||||
new cast.Image(mediaElement.poster)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const activeTrackIds = [];
|
|
||||||
|
|
||||||
if (mediaElement.textTracks.length) {
|
|
||||||
const trackElements = mediaElement.querySelectorAll("track");
|
|
||||||
|
|
||||||
let index = 0;
|
|
||||||
for (const textTrack of Array.from(mediaElement.textTracks)) {
|
|
||||||
const trackElement = trackElements[index];
|
|
||||||
|
|
||||||
// Create Track object
|
|
||||||
const track = new cast.media.Track(
|
|
||||||
index // trackId
|
|
||||||
, cast.media.TrackType.TEXT); // trackType
|
|
||||||
|
|
||||||
// Copy TextTrack properties to Track
|
|
||||||
track.name = textTrack.label;
|
|
||||||
track.language = textTrack.language;
|
|
||||||
track.trackContentId = trackElement.src;
|
|
||||||
track.trackContentType = "text/vtt";
|
|
||||||
|
|
||||||
const { TextTrackType } = cast.media;
|
|
||||||
|
|
||||||
switch (textTrack.kind) {
|
|
||||||
case "subtitles":
|
|
||||||
track.subtype = TextTrackType.SUBTITLES;
|
|
||||||
break;
|
|
||||||
case "captions":
|
|
||||||
track.subtype = TextTrackType.CAPTIONS;
|
|
||||||
break;
|
|
||||||
case "descriptions":
|
|
||||||
track.subtype = TextTrackType.DESCRIPTIONS;
|
|
||||||
break;
|
|
||||||
case "chapters":
|
|
||||||
track.subtype = TextTrackType.CHAPTERS;
|
|
||||||
break;
|
|
||||||
case "metadata":
|
|
||||||
track.subtype = TextTrackType.METADATA;
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Default to subtitles
|
|
||||||
default:
|
|
||||||
track.subtype = TextTrackType.SUBTITLES;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add track to mediaInfo
|
|
||||||
mediaInfo.tracks.push(track);
|
|
||||||
|
|
||||||
// If enabled, set as active track for load request
|
|
||||||
if (textTrack.mode === "showing" || trackElement.default) {
|
|
||||||
activeTrackIds.push(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
index++;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const loadRequest = new cast.media.LoadRequest(mediaInfo);
|
|
||||||
loadRequest.autoplay = false;
|
|
||||||
loadRequest.activeTrackIds = activeTrackIds;
|
|
||||||
|
|
||||||
session.loadMedia(loadRequest
|
const sessionRequest = new cast.SessionRequest(
|
||||||
, onLoadMediaSuccess
|
cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID);
|
||||||
, onLoadMediaError);
|
|
||||||
|
const apiConfig = new cast.ApiConfig(
|
||||||
|
sessionRequest
|
||||||
|
, null // sessionListener
|
||||||
|
, receiverListener); // receiverListener
|
||||||
|
|
||||||
|
|
||||||
|
cast.initialize(apiConfig);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMedia (opts: InitOptions): Promise<cast.media.Media> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
let mediaUrlObject = new URL(opts.mediaUrl);
|
||||||
|
const mediaTitle = mediaUrlObject.pathname;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the media is a local file, start an HTTP media server
|
||||||
|
* and change the media URL to point to it.
|
||||||
|
*/
|
||||||
|
if (opts.mediaUrl.startsWith("file://")) {
|
||||||
|
const host = await getLocalAddress();
|
||||||
|
const port = await options.get("localMediaServerPort");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Wait until media server is listening
|
||||||
|
await startMediaServer(mediaUrlObject.pathname, port);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to start media server");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaUrlObject = new URL(`http://${host}:${port}/`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const activeTrackIds: number[] = [];
|
||||||
|
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 = mediaTitle;
|
||||||
|
mediaInfo.tracks = [];
|
||||||
|
|
||||||
|
|
||||||
|
if (mediaElement) {
|
||||||
|
if (mediaElement instanceof HTMLVideoElement) {
|
||||||
|
if (mediaElement.poster) {
|
||||||
|
mediaInfo.metadata.images = [
|
||||||
|
new cast.Image(mediaElement.poster)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaElement.textTracks.length) {
|
||||||
|
const tracks = Array.from(mediaElement.textTracks);
|
||||||
|
const trackElements = mediaElement.querySelectorAll("track");
|
||||||
|
|
||||||
|
tracks.forEach((track, index) => {
|
||||||
|
const trackElement = trackElements[index];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create media.Track object with the index as the track ID
|
||||||
|
* and type as TrackType.TEXT.
|
||||||
|
*/
|
||||||
|
const castTrack = new cast.media.Track(
|
||||||
|
index, cast.media.TrackType.TEXT);
|
||||||
|
|
||||||
|
// Copy TextTrack properties
|
||||||
|
castTrack.name = track.label;
|
||||||
|
castTrack.language = track.language;
|
||||||
|
castTrack.trackContentId = trackElement.src;
|
||||||
|
castTrack.trackContentType = "text/vtt";
|
||||||
|
|
||||||
|
switch (track.kind) {
|
||||||
|
case "subtitles":
|
||||||
|
castTrack.subtype =
|
||||||
|
cast.media.TextTrackType.SUBTITLES;
|
||||||
|
break;
|
||||||
|
case "captions":
|
||||||
|
castTrack.subtype =
|
||||||
|
cast.media.TextTrackType.CAPTIONS;
|
||||||
|
break;
|
||||||
|
case "descriptions":
|
||||||
|
castTrack.subtype =
|
||||||
|
cast.media.TextTrackType.DESCRIPTIONS;
|
||||||
|
break;
|
||||||
|
case "chapters":
|
||||||
|
castTrack.subtype =
|
||||||
|
cast.media.TextTrackType.CHAPTERS;
|
||||||
|
break;
|
||||||
|
case "metadata":
|
||||||
|
castTrack.subtype =
|
||||||
|
cast.media.TextTrackType.METADATA;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Default to subtitles
|
||||||
|
default:
|
||||||
|
castTrack.subtype =
|
||||||
|
cast.media.TextTrackType.SUBTITLES;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add track to mediaInfo
|
||||||
|
mediaInfo.tracks.push(castTrack);
|
||||||
|
|
||||||
|
// If enabled, mark as active track for load request
|
||||||
|
if (track.mode === "showing" || trackElement.default) {
|
||||||
|
activeTrackIds.push(index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadRequest = new cast.media.LoadRequest(mediaInfo);
|
||||||
|
loadRequest.autoplay = false;
|
||||||
|
loadRequest.activeTrackIds = activeTrackIds;
|
||||||
|
|
||||||
|
currentSession.loadMedia(loadRequest
|
||||||
|
, (media) => resolve(media)
|
||||||
|
, null);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function onLoadMediaSuccess (media: cast.media.Media) {
|
let ignoreMediaEvents = false;
|
||||||
cast.logMessage("onLoadMediaSuccess");
|
|
||||||
|
|
||||||
currentMedia = media;
|
|
||||||
|
|
||||||
|
async function registerMediaElementListeners () {
|
||||||
if (await options.get("mediaSyncElement")) {
|
if (await options.get("mediaSyncElement")) {
|
||||||
mediaElement.addEventListener("play", () => {
|
|
||||||
|
function checkIgnore (ev: Event) {
|
||||||
if (ignoreMediaEvents) {
|
if (ignoreMediaEvents) {
|
||||||
ignoreMediaEvents = false;
|
ignoreMediaEvents = false;
|
||||||
return;
|
ev.stopImmediatePropagation();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
currentMedia.play(null
|
mediaElement.addEventListener("play", checkIgnore, true);
|
||||||
, onMediaPlaySuccess
|
mediaElement.addEventListener("pause", checkIgnore, true);
|
||||||
, onMediaPlayError);
|
mediaElement.addEventListener("suspend", checkIgnore, true);
|
||||||
|
mediaElement.addEventListener("seeking", checkIgnore, true);
|
||||||
|
mediaElement.addEventListener("ratechange", checkIgnore, true);
|
||||||
|
mediaElement.addEventListener("volumechange", checkIgnore, true);
|
||||||
|
|
||||||
|
|
||||||
|
mediaElement.addEventListener("play", () => {
|
||||||
|
currentMedia.play(null, null, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
mediaElement.addEventListener("pause", () => {
|
mediaElement.addEventListener("pause", () => {
|
||||||
if (ignoreMediaEvents) {
|
currentMedia.pause(null, null, null);
|
||||||
ignoreMediaEvents = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentMedia.pause(null
|
|
||||||
, onMediaPauseSuccess
|
|
||||||
, onMediaPauseError);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
mediaElement.addEventListener("suspend", () => {
|
mediaElement.addEventListener("suspend", () => {
|
||||||
/*currentMedia.stop(null
|
// currentMedia.stop(null, null, null);
|
||||||
, onMediaStopSuccess
|
|
||||||
, onMediaStopError);*/
|
|
||||||
});
|
});
|
||||||
|
|
||||||
mediaElement.addEventListener("seeking", () => {
|
mediaElement.addEventListener("seeked", () => {
|
||||||
if (ignoreMediaEvents) {
|
|
||||||
ignoreMediaEvents = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const seekRequest = new cast.media.SeekRequest();
|
const seekRequest = new cast.media.SeekRequest();
|
||||||
seekRequest.currentTime = mediaElement.currentTime;
|
seekRequest.currentTime = mediaElement.currentTime;
|
||||||
|
|
||||||
currentMedia.seek(seekRequest
|
currentMedia.seek(seekRequest, null, null);
|
||||||
, onMediaSeekSuccess
|
|
||||||
, onMediaSeekError);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
mediaElement.addEventListener("ratechange", () => {
|
mediaElement.addEventListener("ratechange", () => {
|
||||||
(currentMedia as any)._sendMediaMessage({
|
currentMedia._sendMediaMessage({
|
||||||
type: "SET_PLAYBACK_RATE"
|
type: "SET_PLAYBACK_RATE"
|
||||||
, playbackRate: mediaElement.playbackRate
|
, playbackRate: mediaElement.playbackRate
|
||||||
});
|
});
|
||||||
@@ -248,10 +272,8 @@ async function onLoadMediaSuccess (media: cast.media.Media) {
|
|||||||
currentMedia.volume.level
|
currentMedia.volume.level
|
||||||
, currentMedia.volume.muted);
|
, currentMedia.volume.muted);
|
||||||
|
|
||||||
const volumeRequest =
|
const volumeRequest = new cast.media.VolumeRequest(newVolume);
|
||||||
new cast.media.VolumeRequest(newVolume);
|
|
||||||
|
|
||||||
cast.logMessage("Volume change");
|
|
||||||
currentMedia.setVolume(volumeRequest);
|
currentMedia.setVolume(volumeRequest);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -261,44 +283,46 @@ async function onLoadMediaSuccess (media: cast.media.Media) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlayerState
|
|
||||||
const localPlayerState = mediaElement.paused
|
const localPlayerState = mediaElement.paused
|
||||||
? cast.media.PlayerState.PAUSED
|
? cast.media.PlayerState.PAUSED
|
||||||
: cast.media.PlayerState.PLAYING;
|
: cast.media.PlayerState.PLAYING;
|
||||||
|
|
||||||
if (localPlayerState !== currentMedia.playerState) {
|
if (localPlayerState !== currentMedia.playerState) {
|
||||||
ignoreMediaEvents = true;
|
ignoreMediaEvents = true;
|
||||||
|
|
||||||
switch (currentMedia.playerState) {
|
switch (currentMedia.playerState) {
|
||||||
case cast.media.PlayerState.PLAYING:
|
case cast.media.PlayerState.PLAYING: {
|
||||||
mediaElement.play();
|
mediaElement.play();
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case cast.media.PlayerState.PAUSED:
|
case cast.media.PlayerState.PAUSED: {
|
||||||
mediaElement.pause();
|
mediaElement.pause();
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RepeatMode
|
|
||||||
const localRepeatMode = mediaElement.loop
|
const localRepeatMode = mediaElement.loop
|
||||||
? cast.media.RepeatMode.SINGLE
|
? cast.media.RepeatMode.SINGLE
|
||||||
: cast.media.RepeatMode.OFF;
|
: cast.media.RepeatMode.OFF;
|
||||||
|
|
||||||
if (localRepeatMode !== currentMedia.repeatMode) {
|
if (localRepeatMode !== currentMedia.repeatMode) {
|
||||||
ignoreMediaEvents = true;
|
ignoreMediaEvents = true;
|
||||||
|
|
||||||
switch (currentMedia.repeatMode) {
|
switch (currentMedia.repeatMode) {
|
||||||
case cast.media.RepeatMode.SINGLE:
|
case cast.media.RepeatMode.SINGLE: {
|
||||||
mediaElement.loop = true;
|
mediaElement.loop = true;
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case cast.media.RepeatMode.OFF:
|
case cast.media.RepeatMode.OFF: {
|
||||||
mediaElement.loop = false;
|
mediaElement.loop = false;
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// currentTime
|
|
||||||
if (currentMedia.currentTime !== mediaElement.currentTime) {
|
if (currentMedia.currentTime !== mediaElement.currentTime) {
|
||||||
ignoreMediaEvents = true;
|
ignoreMediaEvents = true;
|
||||||
mediaElement.currentTime = currentMedia.currentTime;
|
mediaElement.currentTime = currentMedia.currentTime;
|
||||||
@@ -307,57 +331,57 @@ async function onLoadMediaSuccess (media: cast.media.Media) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onRequestSessionError () {
|
|
||||||
cast.logMessage("onRequestSessionError");
|
interface InitOptions {
|
||||||
}
|
mediaUrl: string;
|
||||||
function sessionListener (newSession: cast.Session) {
|
receiver: Receiver;
|
||||||
cast.logMessage("sessionListener");
|
targetElementId?: number;
|
||||||
}
|
|
||||||
function onInitializeSuccess () {
|
|
||||||
cast.logMessage("onInitializeSuccess");
|
|
||||||
}
|
|
||||||
function onInitializeError () {
|
|
||||||
cast.logMessage("onInitializeError");
|
|
||||||
}
|
|
||||||
function onLoadMediaError () {
|
|
||||||
cast.logMessage("onLoadMediaError");
|
|
||||||
}
|
|
||||||
function onMediaPlaySuccess () {
|
|
||||||
cast.logMessage("onMediaPlaySuccess");
|
|
||||||
}
|
|
||||||
function onMediaPlayError (err: cast.Error) {
|
|
||||||
cast.logMessage("onMediaPlayError");
|
|
||||||
}
|
|
||||||
function onMediaPauseSuccess () {
|
|
||||||
cast.logMessage("onMediaPauseSuccess");
|
|
||||||
}
|
|
||||||
function onMediaPauseError (err: cast.Error) {
|
|
||||||
cast.logMessage("onMediaPauseError");
|
|
||||||
}
|
|
||||||
function onMediaStopSuccess () {
|
|
||||||
cast.logMessage("onMediaStopSuccess");
|
|
||||||
}
|
|
||||||
function onMediaStopError (err: cast.Error) {
|
|
||||||
cast.logMessage("onMediaStopError");
|
|
||||||
}
|
|
||||||
function onMediaSeekSuccess () {
|
|
||||||
cast.logMessage("onMediaSeekSuccess");
|
|
||||||
}
|
|
||||||
function onMediaSeekError (err: cast.Error) {
|
|
||||||
cast.logMessage("onMediaSeekError");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function init (opts: InitOptions) {
|
||||||
|
backgroundPort = await ensureInit();
|
||||||
|
|
||||||
ensureInit().then(async (port) => {
|
const isLocalMedia = opts.mediaUrl.startsWith("file://");
|
||||||
backgroundPort = port;
|
|
||||||
|
|
||||||
const isLocalMediaEnabled = await options.get("localMediaEnabled");
|
const isLocalMediaEnabled = await options.get("localMediaEnabled");
|
||||||
if (isLocalFile && !isLocalMediaEnabled) {
|
|
||||||
|
if (isLocalMedia && !isLocalMediaEnabled) {
|
||||||
cast.logMessage("Local media casting not enabled");
|
cast.logMessage("Local media casting not enabled");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
session = await mediaCasting.getMediaSession(selectedReceiver);
|
if (opts.targetElementId) {
|
||||||
|
mediaElement = browser.menus.getTargetElement(
|
||||||
|
opts.targetElementId) as HTMLMediaElement;
|
||||||
|
}
|
||||||
|
|
||||||
loadMedia();
|
currentSession = await getSession(opts);
|
||||||
});
|
currentMedia = await getMedia(opts);
|
||||||
|
|
||||||
|
if (opts.targetElementId) {
|
||||||
|
registerMediaElementListeners();
|
||||||
|
|
||||||
|
window.addEventListener("beforeunload", async () => {
|
||||||
|
backgroundPort.postMessage({
|
||||||
|
subject: "bridge:/mediaServer/stop"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (await options.get("mediaStopOnUnload")) {
|
||||||
|
currentSession.stop(null, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If loaded as a content script, the init values are
|
||||||
|
* provided on the window object.
|
||||||
|
*/
|
||||||
|
if (window.location.protocol !== "moz-extension:") {
|
||||||
|
const _window = (window as any);
|
||||||
|
|
||||||
|
init({
|
||||||
|
mediaUrl: _window.mediaUrl
|
||||||
|
, receiver: _window.receiver
|
||||||
|
, targetElementId: _window.targetElementId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import { ErrorCode
|
|||||||
, SessionStatus
|
, SessionStatus
|
||||||
, VolumeControlType } from "../enums";
|
, VolumeControlType } from "../enums";
|
||||||
|
|
||||||
|
import { RepeatMode } from "../media/enums";
|
||||||
|
|
||||||
import { ListenerObject
|
import { ListenerObject
|
||||||
, onMessage
|
, onMessage
|
||||||
, sendMessageResponse } from "../../eventMessageChannel";
|
, sendMessageResponse } from "../../eventMessageChannel";
|
||||||
@@ -305,9 +307,10 @@ export default class Session {
|
|||||||
, autoplay: loadRequest.autoplay || false
|
, autoplay: loadRequest.autoplay || false
|
||||||
, currentTime: loadRequest.currentTime || 0
|
, currentTime: loadRequest.currentTime || 0
|
||||||
, customData: loadRequest.customData || {}
|
, customData: loadRequest.customData || {}
|
||||||
, repeatMode: "REPEAT_OFF"
|
, repeatMode: RepeatMode.OFF
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
let hasResponded = false;
|
let hasResponded = false;
|
||||||
|
|
||||||
this.addMessageListener(
|
this.addMessageListener(
|
||||||
@@ -318,23 +321,28 @@ export default class Session {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mediaObject = JSON.parse(data);
|
const message = JSON.parse(data);
|
||||||
|
|
||||||
if (mediaObject.status && mediaObject.status.length > 0) {
|
if (message.status && message.status.length > 0) {
|
||||||
hasResponded = true;
|
hasResponded = true;
|
||||||
|
|
||||||
const media = new Media(
|
const media = new Media(
|
||||||
this.sessionId
|
this.sessionId
|
||||||
, mediaObject.status[0].mediaSessionId
|
, message.status[0].mediaSessionId
|
||||||
, _id.get(this));
|
, _id.get(this));
|
||||||
|
|
||||||
media.media = loadRequest.media;
|
media.media = loadRequest.media;
|
||||||
this.media = [ media ];
|
this.media = [ media ];
|
||||||
|
|
||||||
media.play();
|
media.play();
|
||||||
successCallback(media);
|
|
||||||
|
if (successCallback) {
|
||||||
|
successCallback(media);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
errorCallback(new _Error(ErrorCode.SESSION_ERROR));
|
if (errorCallback) {
|
||||||
|
errorCallback(new _Error(ErrorCode.SESSION_ERROR));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,9 +210,6 @@ export function _requestSession (
|
|||||||
|
|
||||||
sessionRequestInProgress = true;
|
sessionRequestInProgress = true;
|
||||||
|
|
||||||
sessionSuccessCallback = successCallback;
|
|
||||||
sessionErrorCallback = errorCallback;
|
|
||||||
|
|
||||||
|
|
||||||
const selectedReceiver = new Receiver_(
|
const selectedReceiver = new Receiver_(
|
||||||
_receiver.id
|
_receiver.id
|
||||||
@@ -235,8 +232,8 @@ export function _requestSession (
|
|||||||
|
|
||||||
sessionRequestInProgress = false;
|
sessionRequestInProgress = false;
|
||||||
|
|
||||||
if (sessionSuccessCallback) {
|
if (successCallback) {
|
||||||
sessionSuccessCallback(session);
|
successCallback(session);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -293,7 +293,7 @@ export default class Media {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private _sendMediaMessage (
|
public _sendMediaMessage (
|
||||||
message: any
|
message: any
|
||||||
, successCallback?: SuccessCallback
|
, successCallback?: SuccessCallback
|
||||||
, errorCallback?: ErrorCallback) {
|
, errorCallback?: ErrorCallback) {
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import * as cast from "./cast";
|
|||||||
|
|
||||||
import { BridgeInfo } from "../lib/bridge";
|
import { BridgeInfo } from "../lib/bridge";
|
||||||
import { Message } from "../types";
|
import { Message } from "../types";
|
||||||
import { onMessage } from "./eventMessageChannel";
|
|
||||||
|
import { onMessage, onMessageResponse, sendMessage } from "./eventMessageChannel";
|
||||||
|
|
||||||
|
|
||||||
let initializedBridgeInfo: BridgeInfo;
|
let initializedBridgeInfo: BridgeInfo;
|
||||||
let initializedBackgroundPort: browser.runtime.Port;
|
let initializedBackgroundPort: MessagePort;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To support exporting an API from a module, we need to
|
* To support exporting an API from a module, we need to
|
||||||
@@ -17,7 +18,7 @@ let initializedBackgroundPort: browser.runtime.Port;
|
|||||||
* 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 ensureInit (): Promise<browser.runtime.Port> {
|
export function ensureInit (): Promise<MessagePort> {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
|
|
||||||
// If already initialized, just return existing bridge info
|
// If already initialized, just return existing bridge info
|
||||||
@@ -31,6 +32,9 @@ export function ensureInit (): Promise<browser.runtime.Port> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const channel = new MessageChannel();
|
||||||
|
initializedBackgroundPort = channel.port1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the module is imported into a background script
|
* If the module is imported into a background script
|
||||||
* context, the location will be the internal extension URL,
|
* context, the location will be the internal extension URL,
|
||||||
@@ -38,14 +42,48 @@ export function ensureInit (): Promise<browser.runtime.Port> {
|
|||||||
* URL.
|
* URL.
|
||||||
*/
|
*/
|
||||||
if (window.location.protocol === "moz-extension:") {
|
if (window.location.protocol === "moz-extension:") {
|
||||||
//
|
const { default: createShim } = await import("../createShim");
|
||||||
|
|
||||||
|
// port2 will post bridge messages to port 1
|
||||||
|
await createShim(channel.port2);
|
||||||
|
|
||||||
|
// bridge -> shim
|
||||||
|
channel.port1.onmessage = ev => {
|
||||||
|
const message = ev.data as Message;
|
||||||
|
|
||||||
|
// Send message to shim
|
||||||
|
sendMessage(message);
|
||||||
|
handleIncomingMessageToShim(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
// shim -> bridge
|
||||||
|
onMessageResponse(message => {
|
||||||
|
channel.port1.postMessage(message);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// Trigger message port setup side-effects
|
/**
|
||||||
|
* Import reference to message port created by contentBridge.
|
||||||
|
* Creation of the port triggers side-effects in the
|
||||||
|
* background script.
|
||||||
|
*/
|
||||||
const { backgroundPort } = await import("./contentBridge");
|
const { backgroundPort } = await import("./contentBridge");
|
||||||
initializedBackgroundPort = backgroundPort;
|
|
||||||
|
// backgroundPort -> channel.port2
|
||||||
|
backgroundPort.onMessage.addListener((message: Message) => {
|
||||||
|
channel.port2.postMessage(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
// channel.port2 -> backgroundPort
|
||||||
|
channel.port2.onmessage = ev => {
|
||||||
|
const message = ev.data as Message;
|
||||||
|
backgroundPort.postMessage(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle shim messages
|
||||||
|
onMessage(handleIncomingMessageToShim);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMessage(message => {
|
function handleIncomingMessageToShim (message: Message) {
|
||||||
switch (message.subject) {
|
switch (message.subject) {
|
||||||
case "shim:/initialized": {
|
case "shim:/initialized": {
|
||||||
initializedBridgeInfo = message.data;
|
initializedBridgeInfo = message.data;
|
||||||
@@ -57,7 +95,7 @@ export function ensureInit (): Promise<browser.runtime.Port> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user