Use extension popup instead of content script for mirroring sender

This commit is contained in:
hensm
2022-09-08 12:43:47 +01:00
parent bd5c1c8da8
commit b897cbb64b
12 changed files with 269 additions and 117 deletions

View File

@@ -75,6 +75,7 @@ const buildOpts = {
path.join(srcPath, "/cast/senders/mirroring.ts"), path.join(srcPath, "/cast/senders/mirroring.ts"),
// UI // UI
path.join(srcPath, "ui/popup/index.ts"), path.join(srcPath, "ui/popup/index.ts"),
path.join(srcPath, "ui/mirroring/index.ts"),
path.join(srcPath, "ui/options/index.ts") path.join(srcPath, "ui/options/index.ts")
], ],
define: { define: {

View File

@@ -559,5 +559,40 @@
"optionsShowAdvancedOptions": { "optionsShowAdvancedOptions": {
"message": "Show advanced options", "message": "Show advanced options",
"description": "Show advanced options checkbox label." "description": "Show advanced options checkbox label."
},
"mirroringPopupTitle": {
"message": "Mirroring",
"description": "Mirroring popup window title."
},
"mirroringPopupWaitingForConnection": {
"message": "Waiting for connection",
"description": "Mirroring popup loading text."
},
"mirroringPopupConnectedTo": {
"message": "Connected to $deviceName$",
"description": "Mirroring popup label displayed whilst session connected before mirroring.",
"placeholders": {
"deviceName": {
"content": "$1"
}
}
},
"mirroringPopupMirroringTo": {
"message": "Mirroring to $deviceName$",
"description": "Mirroring popup label displayed whilst mirroring.",
"placeholders": {
"deviceName": {
"content": "$1"
}
}
},
"mirroringPopupChooseSource": {
"message": "Choose Source",
"description": "Mirroring popup choose media source button label."
},
"mirroringPopupStopMirroring": {
"message": "Stop Mirroring",
"description": "Mirroring popup stop mirroring button label."
} }
} }

View File

@@ -7,10 +7,10 @@ import {
import logger from "../lib/logger"; import logger from "../lib/logger";
import messaging, { Message, Port } from "../messaging"; import messaging, { Message, Port } from "../messaging";
import options from "../lib/options"; import options from "../lib/options";
import { getMediaTypesForPageUrl, stringify } from "../lib/utils";
import type { TypedMessagePort } from "../lib/TypedMessagePort"; import type { TypedMessagePort } from "../lib/TypedMessagePort";
import { import {
ReceiverDevice,
ReceiverSelectorAppInfo, ReceiverSelectorAppInfo,
ReceiverSelectorMediaType, ReceiverSelectorMediaType,
ReceiverSelectorPageInfo ReceiverSelectorPageInfo
@@ -515,19 +515,7 @@ const castManager = new (class {
} }
case ReceiverSelectorMediaType.Screen: case ReceiverSelectorMediaType.Screen:
await browser.tabs.executeScript(contentContext.tabId, { await createMirroringPopup(selection.device);
code: stringify`
window.receiverDevice = ${selection.device};
window.contextTabId = ${contentContext.tabId};
`,
frameId: contentContext.frameId
});
await browser.tabs.executeScript(contentContext.tabId, {
file: "cast/senders/mirroring.js",
frameId: contentContext.frameId
});
break; break;
} }
} }
@@ -560,7 +548,7 @@ async function getReceiverSelection(selectionOpts: {
} }
let defaultMediaType = ReceiverSelectorMediaType.Screen; let defaultMediaType = ReceiverSelectorMediaType.Screen;
let availableMediaTypes = ReceiverSelectorMediaType.None; let availableMediaTypes = ReceiverSelectorMediaType.Screen;
// Default frame ID // Default frame ID
if (selectionOpts.frameId === undefined) selectionOpts.frameId = 0; if (selectionOpts.frameId === undefined) selectionOpts.frameId = 0;
@@ -616,12 +604,8 @@ async function getReceiverSelection(selectionOpts: {
}) })
).url ).url
}; };
} catch (err) {
availableMediaTypes = getMediaTypesForPageUrl(pageInfo.url); logger.error("Failed to locate frame!", err);
} catch {
logger.error(
"Failed to locate frame, falling back to default available media types."
);
} }
} }
@@ -791,4 +775,38 @@ function createSelector() {
return selector; return selector;
} }
async function createMirroringPopup(device: ReceiverDevice) {
let popup: browser.windows.Window;
try {
popup = await browser.windows.create({
url: browser.runtime.getURL("ui/mirroring/index.html"),
type: "popup",
width: 400,
height: 150
});
} catch (err) {
logger.error("Failed to create mirroring popup!", err);
return;
}
const onMirroringPopupMessage = (port: Port) => {
if (
port.sender?.tab?.windowId !== popup.id ||
port.name !== "mirroring"
) {
return;
}
port.postMessage({ subject: "mirroringPopup:init", data: { device } });
};
messaging.onConnect.addListener(onMirroringPopupMessage);
browser.windows.onRemoved.addListener(function onWindowRemoved(windowId) {
if (windowId !== popup.id) return;
messaging.onConnect.removeListener(onMirroringPopupMessage);
browser.windows.onRemoved.removeListener(onWindowRemoved);
});
}
export default castManager; export default castManager;

View File

@@ -3,6 +3,10 @@ import messaging, { Message } from "../messaging";
import type { ReceiverDevice } from "../types"; import type { ReceiverDevice } from "../types";
import pageMessenging from "./pageMessenging"; import pageMessenging from "./pageMessenging";
// Ensure extension-side is initialized first
void pageMessenging.extension;
import CastSDK from "./sdk"; import CastSDK from "./sdk";
export type CastPort = TypedMessagePort<Message>; export type CastPort = TypedMessagePort<Message>;
@@ -41,7 +45,10 @@ export function ensureInit(opts: EnsureInitOpts): Promise<CastPort> {
* will be the internal extension URL, whereas in a content * will be the internal extension URL, whereas in a content
* script, it will be the content page URL. * script, it will be the content page URL.
*/ */
if (window.location.protocol === "moz-extension:") { if (
window.location.protocol === "moz-extension:" &&
window.location.pathname === "_generated_background_page.html"
) {
const { default: castManager } = await import( const { default: castManager } = await import(
"../background/castManager" "../background/castManager"
); );

View File

@@ -19,13 +19,17 @@ type MirroringAppMessage =
| { subject: "close" }; | { subject: "close" };
interface MirroringSenderOpts { interface MirroringSenderOpts {
contextTabId?: number; receiverDevice: ReceiverDevice;
receiverDevice?: ReceiverDevice; onSessionCreated: () => void;
onMirroringConnected: () => void;
onMirroringStopped: () => void;
} }
class MirroringSender { export default class MirroringSender {
private contextTabId?: number; private receiverDevice: ReceiverDevice;
private receiverDevice?: ReceiverDevice; private sessionCreatedCallback: () => void;
private mirroringConnectedCallback: () => void;
private mirroringStoppedCallback: () => void;
private session?: Session; private session?: Session;
private wasSessionRequested = false; private wasSessionRequested = false;
@@ -40,18 +44,17 @@ class MirroringSender {
private streamMaxResolution: { width?: number; height?: number } = {}; private streamMaxResolution: { width?: number; height?: number } = {};
constructor(opts: MirroringSenderOpts) { constructor(opts: MirroringSenderOpts) {
this.contextTabId = opts.contextTabId;
this.receiverDevice = opts.receiverDevice; this.receiverDevice = opts.receiverDevice;
this.sessionCreatedCallback = opts.onSessionCreated;
this.mirroringConnectedCallback = opts.onMirroringConnected;
this.mirroringStoppedCallback = opts.onMirroringStopped;
this.init(); this.init();
} }
private async init() { private async init() {
try { try {
await ensureInit({ await ensureInit({ receiverDevice: this.receiverDevice });
contextTabId: this.contextTabId,
receiverDevice: this.receiverDevice
});
} catch (err) { } catch (err) {
logger.error("Failed to initialize cast API", err); logger.error("Failed to initialize cast API", err);
} }
@@ -82,11 +85,6 @@ class MirroringSender {
cast.initialize(apiConfig); cast.initialize(apiConfig);
} }
stop() {
this.peerConnection?.close();
this.session?.stop();
}
private sessionListener() { private sessionListener() {
// Unused // Unused
} }
@@ -98,7 +96,7 @@ class MirroringSender {
cast.requestSession( cast.requestSession(
session => { session => {
this.session = session; this.session = session;
this.createMirroringConnection(); this.sessionCreatedCallback();
}, },
err => { err => {
logger.error("Session request failed", err); logger.error("Session request failed", err);
@@ -112,7 +110,14 @@ class MirroringSender {
this.session.sendMessage(NS_FX_CAST, message); this.session.sendMessage(NS_FX_CAST, message);
} }
private async createMirroringConnection() { stop() {
this.peerConnection?.close();
this.session?.stop();
this.mirroringStoppedCallback();
}
async createMirroringConnection(stream: MediaStream) {
const pc = new RTCPeerConnection(); const pc = new RTCPeerConnection();
this.peerConnection = pc; this.peerConnection = pc;
@@ -152,6 +157,7 @@ class MirroringSender {
return; return;
} }
this.mirroringConnectedCallback();
applyParameters(); applyParameters();
}); });
@@ -200,26 +206,9 @@ class MirroringSender {
await sender.setParameters(params); await sender.setParameters(params);
}; };
let stream: MediaStream; const [track] = stream.getVideoTracks();
try { pc.addTrack(track, stream);
// Add screen media stream track.addEventListener("ended", () => this.stop());
stream = await navigator.mediaDevices.getDisplayMedia({
video: {
cursor: "motion",
frameRate: this.streamMaxFrameRate
},
audio: false
});
const [track] = stream.getVideoTracks();
pc.addTrack(track, stream);
track.addEventListener("ended", () => this.stop());
} catch (err) {
logger.error("Failed to add display media stream!", err);
this.stop();
return;
}
/** /**
* Use a video element to get stream resize events and update * Use a video element to get stream resize events and update
@@ -231,19 +220,3 @@ class MirroringSender {
video.play(); video.play();
} }
} }
/**
* If loaded as a content script, opts are stored on the window object.
*/
if (window.location.protocol !== "moz-extension:") {
const window_ = window as any;
const sender = new MirroringSender({
contextTabId: window_.contextTabId,
receiverDevice: window_.receiverDevice
});
window.addEventListener("beforeunload", () => {
sender.stop();
});
}

View File

@@ -29,47 +29,6 @@ export function stringify(
return formattedString; return formattedString;
} }
export function getMediaTypesForPageUrl(
pageUrl: string
): ReceiverSelectorMediaType {
const url = new URL(pageUrl);
let availableMediaTypes = ReceiverSelectorMediaType.None;
/**
* Content scripts are prohibited from running on some
* Mozilla domains.
*/
const blockedHosts = [
"accounts-static.cdn.mozilla.net",
"accounts.firefox.com",
"addons.cdn.mozilla.net",
"addons.mozilla.org",
"api.accounts.firefox.com",
"content.cdn.mozilla.net",
"discovery.addons.mozilla.org",
"install.mozilla.org",
"oauth.accounts.firefox.com",
"profile.accounts.firefox.com",
"support.mozilla.org",
"sync.services.mozilla.com"
];
if (blockedHosts.includes(url.host)) {
return availableMediaTypes;
}
/**
* When on an insecure origin, MediaDevices.getDisplayMedia
* will not exist (and legacy MediaDevices.getUserMedia
* mediaSource constraint will fail).
*/
if (url.protocol === "https:") {
availableMediaTypes |= ReceiverSelectorMediaType.Screen;
}
return availableMediaTypes;
}
export function loadScript( export function loadScript(
scriptUrl: string, scriptUrl: string,
doc: Document = document doc: Document = document

View File

@@ -108,6 +108,8 @@ type ExtMessageDefinitions = {
* to the bridge. * to the bridge.
*/ */
"main:refreshDeviceManager": void; "main:refreshDeviceManager": void;
"mirroringPopup:init": { device: ReceiverDevice };
}; };
/** /**

View File

@@ -26,4 +26,6 @@
}); });
</script> </script>
<span class="ellipsis">{ellipsis}</span> <span class="indicator">
<slot />{ellipsis}
</span>

View File

@@ -0,0 +1,101 @@
<script lang="ts">
import { onDestroy, onMount } from "svelte";
import MirroringSender from "../../cast/senders/mirroring";
import logger from "../../lib/logger";
import options, { Options } from "../../lib/options";
import messaging, { Port } from "../../messaging";
import type { ReceiverDevice } from "../../types";
import LoadingIndicator from "../LoadingIndicator.svelte";
const _ = browser.i18n.getMessage;
document.title = _("mirroringPopupTitle");
let port: Optional<Port>;
let opts: Optional<Options>;
let device: ReceiverDevice;
let mirroringSender: Optional<MirroringSender>;
let isSessionCreated = false;
let isMirroringConnected = false;
onMount(async () => {
port = messaging.connect({ name: "mirroring" });
port.onMessage.addListener(message => {
switch (message.subject) {
case "mirroringPopup:init":
device = message.data.device;
mirroringSender = new MirroringSender({
receiverDevice: device,
onSessionCreated() {
isSessionCreated = true;
},
onMirroringConnected() {
isMirroringConnected = true;
},
onMirroringStopped() {
window.close();
}
});
break;
}
});
opts = await options.getAll();
});
onDestroy(() => {
mirroringSender?.stop();
port?.disconnect();
});
async function requestDisplayMedia() {
if (!mirroringSender) return;
try {
mirroringSender.createMirroringConnection(
await navigator.mediaDevices.getDisplayMedia({
video: {
cursor: "motion",
frameRate: opts?.mirroringStreamMaxFrameRate
},
// Currently not implemented in Firefox
audio: true
})
);
} catch (err) {
logger.error("Failed to create mirroring connection!", err);
}
}
</script>
{#if isSessionCreated}
<div class="mirroring-status">
{#if isMirroringConnected}
<p>
{_("mirroringPopupMirroringTo", device.friendlyName)}
</p>
<button on:click={() => window.close()}>
{_("mirroringPopupStopMirroring")}
</button>
{:else}
<p>
{_("mirroringPopupConnectedTo", device.friendlyName)}
</p>
<button on:click={requestDisplayMedia} class="button">
{_("mirroringPopupChooseSource")}
</button>
{/if}
</div>
{:else}
<p>
<LoadingIndicator
>{_("mirroringPopupWaitingForConnection")}</LoadingIndicator
>
</p>
{/if}

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="../photon-colors.css" />
<link rel="stylesheet" href="../photon-widgets.css" />
<link rel="stylesheet" href="style.css" />
<script src="index.js" defer></script>
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@@ -0,0 +1,9 @@
import MirroringPopup from "./MirroringPopup.svelte";
const target = document.getElementById("root");
if (target) {
const mirroringPopup = new MirroringPopup({ target });
window.addEventListener("beforeunload", () => {
mirroringPopup.$destroy();
});
}

View File

@@ -0,0 +1,30 @@
body,
html {
height: 100%;
width: 100%;
}
body {
background: var(--box-background);
color: var(--box-color);
font: message-box;
font-size: 15px;
margin: initial;
}
#root {
align-items: center;
display: flex;
height: 100%;
justify-content: center;
}
.mirroring-status {
align-items: center;
display: flex;
flex-direction: column;
gap: 15px;
}
.mirroring-status > p {
margin: initial;
}