انتقل إلى المحتوى

Browser extension

The Vistilla Recorder extension (extension/) records a Google Meet call, or any tab, in Chrome or Brave and sends it to Vistilla. It is plain Manifest V3 JavaScript with no build step; the website offers it as a zip.

Pieces

File Runs as Does
manifest.json — MV3; permissions tabCapture, offscreen, activeTab, storage, contextMenus, downloads; hosts the live site and meet.google.com. Its key fixes the extension id (amkapkpnecmakbakcjgkleoobldejjhj) for everyone who loads the folder.
background.js service worker Starts a recording (popup, Alt+Shift+R, right-click menu), keeps the offscreen recorder, relays its state to the badge, the Meet bar and the popup, opens the finished recording.
offscreen.js offscreen document (USER_MEDIA) Captures the tab (chromeMediaSource: "tab", up to 1080p30) and the microphone (always taken when allowed; the switch only sets its gain to 0 or 1, so it can come on mid-recording), mixes their sound in a Web Audio graph (the tab's sound is also played back, since capture silences it), records VP9/Opus WebM at 1.5 Mb/s, and uploads.
popup.* toolbar popup Title, channel, audience, microphone switch, Record; then time, the microphone switch again (set-mic, routed to the offscreen page and, for a Meet-bar recording, to the Meet tab), Pause, Stop, Discard; then progress and Open. A popup cannot show a permission prompt, so when the microphone has not been asked yet it opens welcome.html#mic.
content.js content script on Meet The bar at the top of the call (shadow DOM), and the recorder for a recording started from it (see below). Follows Meet's mute button ([data-is-muted]) and mutes the microphone in the recording with it (each press; at the start, an unmuted Meet leaves the popup's switch as it is).
welcome.* extension page Opened on install: pin, allow the microphone (an offscreen document and a popup cannot ask; the page asks on load, and as #mic closes itself once allowed), sign in.
lib.js module Site address, API calls, i18n, helpers.
updater.js, update.* module, extension page One-click updates (see below).
_locales/en, _locales/ar — Every string, English and Arabic (chrome.i18n, by the browser's language).

Signing in

There is no separate login. Requests go to the site with credentials: "include"; because the extension has host permission for the site, Chrome sends its SameSite=Lax session cookie with them. Signed in on the website means signed in in the extension. No server change was needed.

Uploading

The same as the website's recorder (web/assets/record.js):

  1. POST /api/videos with live: true and visibility: "private" when recording starts (not awaited, so recording never waits on the network).
  2. Every 5 MB of recording, PUT {part_url}{n}, in order, one at a time. While recording, a failed part is retried indefinitely with backoff (up to 30 s apart); after Stop, six more times.
  3. On Stop: the last part, PATCH /api/videos/{id} (title, audience), PUT …/thumbnail (a frame grabbed 2.5 s in), POST …/complete with the length (pauses left out), size and dimensions. Then the recording page opens in a new tab.

The whole recording also stays in memory until it is complete; if the site cannot be reached at the end, it is handed to chrome.downloads as a .webm, so nothing is lost.

Two ways to record

  • From the extension (toolbar Record, Alt+Shift+R, right-click menu): these grant activeTab, so chrome.tabCapture.getMediaStreamId hands the tab to the offscreen recorder, which records it (mode: "tab").
  • From the Meet bar's Record: a click in a page does not grant activeTab, so the content script records the page itself with getDisplayMedia({ preferCurrentTab: true }) (Chrome's one "Share this tab" question), mixes in the microphone (Meet's own permission), and sends each second to the offscreen page as base64 (page-start, page-chunk, page-poster), which uploads it the same way (mode: "page"). Pause, Stop and Discard from the popup or the shortcut are routed by the service worker to the Meet tab (control), which acts and then tells the offscreen page. Closing the tab or Chrome's "Stop sharing" ends it with what has arrived.

Updating itself

updater.js, update.html: Chrome runs an unpacked extension from its folder, so an update writes the new files there and calls chrome.runtime.reload(). The folder is picked once with showDirectoryPicker (welcome page, or the first update), checked to hold this extension's manifest (same key), and its handle kept in IndexedDB. Update now in the popup downloads the zip, unzips it (a small reader using DecompressionStream("deflate-raw")), checks the new manifest has the same key, writes every file (the manifest last), and reloads. When the folder's permission has lapsed (queryPermission is not granted), the update page asks for it with a click. A running recording blocks updating. Brave ships with showDirectoryPicker switched off (flag #file-system-access-api); the welcome and update pages detect that and open the flag instead. Reload works only with Developer mode on (Chrome otherwise disables the reloaded extension as unsupportedDeveloperExtension), which Load unpacked needs anyway.

Releasing

  1. Raise version in extension/manifest.json.
  2. scripts/extension-zip.sh writes web/assets/extension/vistilla-recorder.zip (one vistilla-recorder/ folder) and web/assets/extension/version.json.
  3. Deploy the site. The website's button shows the new version, and the extension's popup tells people who have an older one.

It is not in the Chrome Web Store: publishing there needs a developer account with a paid registration, which the project's free-hosting rule rules out. People install it with Load unpacked.

Testing

Headless Chromium can drive it end to end (--headless=new, --load-extension, and --allowlisted-extension-id=<id> so tabCapture works without a toolbar click, or --auto-accept-this-tab-capture for the Meet bar's "Share this tab"). Setting a page's viewport resizes the one headless window, and with it what a capture records. --use-fake-device-for-media-stream and --use-fake-ui-for-media-stream break tab capture there; leave them out (the microphone is then missing, which the recorder handles).