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):
POST /api/videoswithlive: trueandvisibility: "private"when recording starts (not awaited, so recording never waits on the network).- 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. - On Stop: the last part,
PATCH /api/videos/{id}(title, audience),PUT …/thumbnail(a frame grabbed 2.5 s in),POST …/completewith 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, sochrome.tabCapture.getMediaStreamIdhands 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 withgetDisplayMedia({ 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¶
- Raise
versioninextension/manifest.json. scripts/extension-zip.shwritesweb/assets/extension/vistilla-recorder.zip(onevistilla-recorder/folder) andweb/assets/extension/version.json.- 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).