diff --git a/ext/bin/build.js b/ext/bin/build.js index 30928db..f3458c7 100644 --- a/ext/bin/build.js +++ b/ext/bin/build.js @@ -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: { diff --git a/ext/src/_locales/en/messages.json b/ext/src/_locales/en/messages.json index e72c8e9..f39fe64 100755 --- a/ext/src/_locales/en/messages.json +++ b/ext/src/_locales/en/messages.json @@ -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." } } diff --git a/ext/src/background/castManager.ts b/ext/src/background/castManager.ts index 8e51780..c877c49 100644 --- a/ext/src/background/castManager.ts +++ b/ext/src/background/castManager.ts @@ -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; diff --git a/ext/src/cast/export.ts b/ext/src/cast/export.ts index 0bc165d..567723d 100644 --- a/ext/src/cast/export.ts +++ b/ext/src/cast/export.ts @@ -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; @@ -41,7 +45,10 @@ export function ensureInit(opts: EnsureInitOpts): Promise { * 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" ); diff --git a/ext/src/cast/senders/mirroring.ts b/ext/src/cast/senders/mirroring.ts index ba6f692..8f49175 100644 --- a/ext/src/cast/senders/mirroring.ts +++ b/ext/src/cast/senders/mirroring.ts @@ -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(); - }); -} diff --git a/ext/src/lib/utils.ts b/ext/src/lib/utils.ts index d10eed2..1229b51 100644 --- a/ext/src/lib/utils.ts +++ b/ext/src/lib/utils.ts @@ -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 diff --git a/ext/src/messaging.ts b/ext/src/messaging.ts index b59f84c..f357aa4 100644 --- a/ext/src/messaging.ts +++ b/ext/src/messaging.ts @@ -108,6 +108,8 @@ type ExtMessageDefinitions = { * to the bridge. */ "main:refreshDeviceManager": void; + + "mirroringPopup:init": { device: ReceiverDevice }; }; /** diff --git a/ext/src/ui/LoadingIndicator.svelte b/ext/src/ui/LoadingIndicator.svelte index c54c405..8e70a14 100644 --- a/ext/src/ui/LoadingIndicator.svelte +++ b/ext/src/ui/LoadingIndicator.svelte @@ -26,4 +26,6 @@ }); -{ellipsis} + + {ellipsis} + diff --git a/ext/src/ui/mirroring/MirroringPopup.svelte b/ext/src/ui/mirroring/MirroringPopup.svelte new file mode 100644 index 0000000..01a460e --- /dev/null +++ b/ext/src/ui/mirroring/MirroringPopup.svelte @@ -0,0 +1,101 @@ + + +{#if isSessionCreated} +
+ {#if isMirroringConnected} +

+ {_("mirroringPopupMirroringTo", device.friendlyName)} +

+ + {:else} +

+ {_("mirroringPopupConnectedTo", device.friendlyName)} +

+ + {/if} +
+{:else} +

+ {_("mirroringPopupWaitingForConnection")} +

+{/if} diff --git a/ext/src/ui/mirroring/index.html b/ext/src/ui/mirroring/index.html new file mode 100644 index 0000000..7d2eeca --- /dev/null +++ b/ext/src/ui/mirroring/index.html @@ -0,0 +1,15 @@ + + + + + + + + + + + + +
+ + diff --git a/ext/src/ui/mirroring/index.ts b/ext/src/ui/mirroring/index.ts new file mode 100644 index 0000000..88533b6 --- /dev/null +++ b/ext/src/ui/mirroring/index.ts @@ -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(); + }); +} diff --git a/ext/src/ui/mirroring/style.css b/ext/src/ui/mirroring/style.css new file mode 100644 index 0000000..2fb2728 --- /dev/null +++ b/ext/src/ui/mirroring/style.css @@ -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; +}