-
Notifications
You must be signed in to change notification settings - Fork 1.7k
build(extension): add TARGET-driven dual builds with Firefox manifest #2000
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1f21d68
218f715
09780ad
f5a9f1f
4077348
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| { | ||
| "manifest_version": 3, | ||
| "name": "Cap - Screen Recorder & Screen Capture", | ||
| "short_name": "Cap", | ||
| "description": "Free, open source screen recorder. Capture your screen, camera & mic in Firefox and share a video link the moment you stop.", | ||
| "version": "1.0.3", | ||
| "homepage_url": "https://cap.so", | ||
| "browser_specific_settings": { | ||
| "gecko": { | ||
| "id": "extension@cap.so", | ||
| "strict_min_version": "128.0", | ||
| "data_collection_permissions": { | ||
| "required": ["authenticationInfo", "websiteContent"] | ||
| } | ||
| } | ||
| }, | ||
| "icons": { | ||
| "16": "icons/icon-16.png", | ||
| "32": "icons/icon-32.png", | ||
| "48": "icons/icon-48.png", | ||
| "128": "icons/icon-128.png" | ||
| }, | ||
| "action": { | ||
| "default_title": "Record your screen with Cap", | ||
| "default_icon": { | ||
| "16": "icons/icon-16.png", | ||
| "32": "icons/icon-32.png", | ||
| "48": "icons/icon-48.png", | ||
| "128": "icons/icon-128.png" | ||
| } | ||
| }, | ||
| "background": { | ||
| "scripts": ["assets/service-worker.js"], | ||
| "type": "module" | ||
| }, | ||
| "options_ui": { | ||
| "page": "options.html", | ||
| "open_in_tab": true | ||
| }, | ||
| "permissions": ["activeTab", "identity", "scripting", "storage"], | ||
| "host_permissions": ["http://*/*", "https://*/*", "file:///*"], | ||
| "content_scripts": [ | ||
| { | ||
| "matches": ["http://*/*", "https://*/*", "file:///*"], | ||
| "js": ["assets/content-bootstrap.js"], | ||
| "run_at": "document_idle" | ||
| } | ||
| ], | ||
| "web_accessible_resources": [ | ||
| { | ||
| "resources": ["icons/*", "content/overlay.js", "welcome.html"], | ||
| "matches": ["http://*/*", "https://*/*", "file:///*"] | ||
| }, | ||
| { | ||
| "resources": ["camera-preview.html", "popup.html"], | ||
| "matches": ["http://*/*", "https://*/*", "file:///*"] | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| // The recorder document (recorder.html) hosts capture, upload, device | ||
| // enumeration, the mic probe and the camera-preview relay. On Chrome it runs | ||
| // as an offscreen document; this module owns its lifecycle so the rest of the | ||
| // service worker never touches chrome.offscreen directly. | ||
| export const RECORDER_URL = "recorder.html"; | ||
|
|
||
| let recorderDocumentCreation: Promise<void> | null = null; | ||
|
|
||
| const getRecorderContexts = async () => { | ||
| const recorderUrl = chrome.runtime.getURL(RECORDER_URL); | ||
| return new Promise<Array<{ documentUrl?: string }>>((resolve) => { | ||
| chrome.runtime.getContexts( | ||
| { | ||
| contextTypes: [chrome.runtime.ContextType.OFFSCREEN_DOCUMENT], | ||
| documentUrls: [recorderUrl], | ||
| }, | ||
|
Comment on lines
+9
to
+16
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The shared background script imports this helper for both targets, but Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/chrome-extension/src/background/recorder-host.ts
Line: 9-16
Comment:
**Firefox Reaches Offscreen APIs**
The shared background script imports this helper for both targets, but `hasRecorderHost()` and `ensureRecorderHost()` call Chrome-only offscreen APIs without checking the target. In the Firefox build, normal paths like starting a recording, probing the mic, enumerating devices, or connecting camera preview can reach `chrome.runtime.getContexts` or `chrome.offscreen.createDocument`, producing a runtime error instead of opening a Firefox recorder page.
How can I resolve this? If you propose a fix, please make it concise. |
||
| (contexts) => resolve(contexts), | ||
| ); | ||
| }); | ||
| }; | ||
|
|
||
| export const hasRecorderHost = async () => | ||
| (await getRecorderContexts()).length > 0; | ||
|
|
||
| const createOffscreenDocument = () => | ||
| new Promise<void>((resolve, reject) => { | ||
| chrome.offscreen.createDocument( | ||
| { | ||
| url: RECORDER_URL, | ||
| reasons: ["USER_MEDIA", "DISPLAY_MEDIA", "BLOBS", "AUDIO_PLAYBACK"], | ||
| justification: "Record and upload Cap videos from an extension page.", | ||
| }, | ||
| () => { | ||
| const error = chrome.runtime.lastError; | ||
| if (!error) { | ||
| resolve(); | ||
| return; | ||
| } | ||
|
|
||
| const message = error.message ?? "Failed to create offscreen document"; | ||
| if (message.toLowerCase().includes("single offscreen document")) { | ||
| resolve(); | ||
| return; | ||
| } | ||
|
|
||
| reject(new Error(message)); | ||
| }, | ||
| ); | ||
| }); | ||
|
|
||
| export const ensureRecorderHost = async () => { | ||
| const contexts = await getRecorderContexts(); | ||
| if (contexts.length > 0) return; | ||
|
|
||
| recorderDocumentCreation ??= createOffscreenDocument().finally(() => { | ||
| recorderDocumentCreation = null; | ||
| }); | ||
| await recorderDocumentCreation; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import { TARGET } from "./target"; | ||
|
|
||
| // Per-target feature availability. Firefox has no chrome.offscreen or | ||
| // chrome.tabCapture, its getDisplayMedia exposes no system audio or per-tab | ||
| // surface, MV3 host permissions are user-grantable rather than granted at | ||
| // install, and getDisplayMedia requires transient user activation so capture | ||
| // cannot start without a click inside the recorder document. | ||
| export const capabilities = { | ||
| supportsTabCapture: TARGET === "chrome", | ||
| supportsOffscreen: TARGET === "chrome", | ||
| supportsSystemAudioCapture: TARGET === "chrome", | ||
| hostPermissionsGrantedAtInstall: TARGET === "chrome", | ||
| recorderNeedsUserGesture: TARGET === "firefox", | ||
| } as const; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| // "chrome-extension:" on Chromium, "moz-extension:" on Firefox. Sender checks | ||
| // must use this instead of a hardcoded literal or Firefox extension pages get | ||
| // misclassified as web pages. | ||
| export const EXTENSION_PROTOCOL = new URL(chrome.runtime.getURL("")).protocol; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| // Injected by vite `define` per build target; undefined under vitest, which | ||
| // runs without a define and must behave like the Chrome build. | ||
| declare const __TARGET__: "chrome" | "firefox" | undefined; | ||
|
|
||
| export type ExtensionTarget = "chrome" | "firefox"; | ||
|
|
||
| export const TARGET: ExtensionTarget = | ||
| typeof __TARGET__ === "undefined" ? "chrome" : __TARGET__; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file assumes
chrome.runtime.getContexts+chrome.offscreen.createDocumentexist. Since this PR adds a Firefox build (and the manifest drops theoffscreenpermission), it might be worth adding a Firefox-specific implementation here (or at least a feature-detect + clearer error) sosendOffscreen(...)doesn’t end up throwing aTypeErrorat runtime.