mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-08 08:39:59 +00:00
Use extension popup instead of content script for mirroring sender
This commit is contained in:
@@ -75,6 +75,7 @@ const buildOpts = {
|
||||
path.join(srcPath, "/cast/senders/mirroring.ts"),
|
||||
// UI
|
||||
path.join(srcPath, "ui/popup/index.ts"),
|
||||
path.join(srcPath, "ui/mirroring/index.ts"),
|
||||
path.join(srcPath, "ui/options/index.ts")
|
||||
],
|
||||
define: {
|
||||
|
||||
@@ -559,5 +559,40 @@
|
||||
"optionsShowAdvancedOptions": {
|
||||
"message": "Show advanced options",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@ import {
|
||||
import logger from "../lib/logger";
|
||||
import messaging, { Message, Port } from "../messaging";
|
||||
import options from "../lib/options";
|
||||
import { getMediaTypesForPageUrl, stringify } from "../lib/utils";
|
||||
import type { TypedMessagePort } from "../lib/TypedMessagePort";
|
||||
|
||||
import {
|
||||
ReceiverDevice,
|
||||
ReceiverSelectorAppInfo,
|
||||
ReceiverSelectorMediaType,
|
||||
ReceiverSelectorPageInfo
|
||||
@@ -515,19 +515,7 @@ const castManager = new (class {
|
||||
}
|
||||
|
||||
case ReceiverSelectorMediaType.Screen:
|
||||
await browser.tabs.executeScript(contentContext.tabId, {
|
||||
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
|
||||
});
|
||||
|
||||
await createMirroringPopup(selection.device);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -560,7 +548,7 @@ async function getReceiverSelection(selectionOpts: {
|
||||
}
|
||||
|
||||
let defaultMediaType = ReceiverSelectorMediaType.Screen;
|
||||
let availableMediaTypes = ReceiverSelectorMediaType.None;
|
||||
let availableMediaTypes = ReceiverSelectorMediaType.Screen;
|
||||
|
||||
// Default frame ID
|
||||
if (selectionOpts.frameId === undefined) selectionOpts.frameId = 0;
|
||||
@@ -616,12 +604,8 @@ async function getReceiverSelection(selectionOpts: {
|
||||
})
|
||||
).url
|
||||
};
|
||||
|
||||
availableMediaTypes = getMediaTypesForPageUrl(pageInfo.url);
|
||||
} catch {
|
||||
logger.error(
|
||||
"Failed to locate frame, falling back to default available media types."
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error("Failed to locate frame!", err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -791,4 +775,38 @@ function createSelector() {
|
||||
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;
|
||||
|
||||
@@ -3,6 +3,10 @@ import messaging, { Message } from "../messaging";
|
||||
import type { ReceiverDevice } from "../types";
|
||||
|
||||
import pageMessenging from "./pageMessenging";
|
||||
|
||||
// Ensure extension-side is initialized first
|
||||
void pageMessenging.extension;
|
||||
|
||||
import CastSDK from "./sdk";
|
||||
|
||||
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
|
||||
* 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(
|
||||
"../background/castManager"
|
||||
);
|
||||
|
||||
@@ -19,13 +19,17 @@ type MirroringAppMessage =
|
||||
| { subject: "close" };
|
||||
|
||||
interface MirroringSenderOpts {
|
||||
contextTabId?: number;
|
||||
receiverDevice?: ReceiverDevice;
|
||||
receiverDevice: ReceiverDevice;
|
||||
onSessionCreated: () => void;
|
||||
onMirroringConnected: () => void;
|
||||
onMirroringStopped: () => void;
|
||||
}
|
||||
|
||||
class MirroringSender {
|
||||
private contextTabId?: number;
|
||||
private receiverDevice?: ReceiverDevice;
|
||||
export default class MirroringSender {
|
||||
private receiverDevice: ReceiverDevice;
|
||||
private sessionCreatedCallback: () => void;
|
||||
private mirroringConnectedCallback: () => void;
|
||||
private mirroringStoppedCallback: () => void;
|
||||
|
||||
private session?: Session;
|
||||
private wasSessionRequested = false;
|
||||
@@ -40,18 +44,17 @@ class MirroringSender {
|
||||
private streamMaxResolution: { width?: number; height?: number } = {};
|
||||
|
||||
constructor(opts: MirroringSenderOpts) {
|
||||
this.contextTabId = opts.contextTabId;
|
||||
this.receiverDevice = opts.receiverDevice;
|
||||
this.sessionCreatedCallback = opts.onSessionCreated;
|
||||
this.mirroringConnectedCallback = opts.onMirroringConnected;
|
||||
this.mirroringStoppedCallback = opts.onMirroringStopped;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
private async init() {
|
||||
try {
|
||||
await ensureInit({
|
||||
contextTabId: this.contextTabId,
|
||||
receiverDevice: this.receiverDevice
|
||||
});
|
||||
await ensureInit({ receiverDevice: this.receiverDevice });
|
||||
} catch (err) {
|
||||
logger.error("Failed to initialize cast API", err);
|
||||
}
|
||||
@@ -82,11 +85,6 @@ class MirroringSender {
|
||||
cast.initialize(apiConfig);
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.peerConnection?.close();
|
||||
this.session?.stop();
|
||||
}
|
||||
|
||||
private sessionListener() {
|
||||
// Unused
|
||||
}
|
||||
@@ -98,7 +96,7 @@ class MirroringSender {
|
||||
cast.requestSession(
|
||||
session => {
|
||||
this.session = session;
|
||||
this.createMirroringConnection();
|
||||
this.sessionCreatedCallback();
|
||||
},
|
||||
err => {
|
||||
logger.error("Session request failed", err);
|
||||
@@ -112,7 +110,14 @@ class MirroringSender {
|
||||
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();
|
||||
this.peerConnection = pc;
|
||||
|
||||
@@ -152,6 +157,7 @@ class MirroringSender {
|
||||
return;
|
||||
}
|
||||
|
||||
this.mirroringConnectedCallback();
|
||||
applyParameters();
|
||||
});
|
||||
|
||||
@@ -200,26 +206,9 @@ class MirroringSender {
|
||||
await sender.setParameters(params);
|
||||
};
|
||||
|
||||
let stream: MediaStream;
|
||||
try {
|
||||
// Add screen media stream
|
||||
|
||||
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;
|
||||
}
|
||||
const [track] = stream.getVideoTracks();
|
||||
pc.addTrack(track, stream);
|
||||
track.addEventListener("ended", () => this.stop());
|
||||
|
||||
/**
|
||||
* Use a video element to get stream resize events and update
|
||||
@@ -231,19 +220,3 @@ class MirroringSender {
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,47 +29,6 @@ export function stringify(
|
||||
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(
|
||||
scriptUrl: string,
|
||||
doc: Document = document
|
||||
|
||||
@@ -108,6 +108,8 @@ type ExtMessageDefinitions = {
|
||||
* to the bridge.
|
||||
*/
|
||||
"main:refreshDeviceManager": void;
|
||||
|
||||
"mirroringPopup:init": { device: ReceiverDevice };
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,4 +26,6 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<span class="ellipsis">{ellipsis}</span>
|
||||
<span class="indicator">
|
||||
<slot />{ellipsis}
|
||||
</span>
|
||||
|
||||
101
ext/src/ui/mirroring/MirroringPopup.svelte
Normal file
101
ext/src/ui/mirroring/MirroringPopup.svelte
Normal 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}
|
||||
15
ext/src/ui/mirroring/index.html
Normal file
15
ext/src/ui/mirroring/index.html
Normal 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>
|
||||
9
ext/src/ui/mirroring/index.ts
Normal file
9
ext/src/ui/mirroring/index.ts
Normal 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();
|
||||
});
|
||||
}
|
||||
30
ext/src/ui/mirroring/style.css
Normal file
30
ext/src/ui/mirroring/style.css
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user