Skip to content

Build1 publisher3 min readPublished

Capturing a Meet tab under MV3 mutes the call until you wire the stream back to output

A solo developer's live-caption extension needs an offscreen document because MV3's service worker has no Web Audio, and his own measurements price what he paid in seconds of delay to cut the hourly bill 17 times.

The Engineer · Build desk

What happened

  • Chrome's MV3 service worker has no Web Audio and no getUserMedia, so the extension only fetches a stream id there and opens an invisible offscreen document with reasons USER_MEDIA to hold the stream.
  • Capturing the tab stops its sound reaching the speakers, and the meeting becomes audible again only once the stream is fed back through an AudioContext to its destination.
  • The error "Cannot capture a tab with an active stream" survives both a page reload and chrome.runtime.reload(), because the offscreen document holds the stream until that document is closed.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint Because activeTab dies at the next navigation, a reload has to be handled as a prompt telling the user to click the icon, not as an error state someone will debug.
  • decision Retrying a 400 or a 403 buys nearly twenty seconds of blank captions, so shipping this design means holding a second provider's key and a slower 3-second-block path behind it.
  • cost The saving is charged to whoever reads the captions, since the developer's own framing is that the price is paid in delay.
  • capability With recognition and translation in one stream, showing partial results no longer multiplies a per-character translation bill, so live-growing captions stop being a pricing decision.

Chrome frees a captured tab asynchronously, so a timeout is a guess. The extension closes the offscreen document, then calls `chrome.tabCapture.getCapturedTabs()` every 50 ms for up to 500 ms and checks whether the target tabId still comes back with status `active` [10]. Reloading the extension is the first reflex and it achieves nothing, because the stream belongs to the offscreen document [9].

The audio path is plain. An AudioWorklet downmixes the channels to mono and emits PCM Int16 in blocks of 4096 samples [7]. At 48 kHz, 4096 samples work out to 85.3 ms per block [8]. The session configuration goes out as the first websocket message, before the first audio byte: `pcm_s16le`, 48000 Hz, one channel, an English language hint, endpoint detection on, and one-way translation to Portuguese [19].

DeepL charges on the length of the source text, and a sentence that is still open grows and gets resent in full with every partial result [14]. How much that costs depends on how the recogniser cuts the sentence, so the developer measured it on the wire: a test track into a real Deepgram socket, the partials through the extension's own engine, and the translation call replaced [15]. On 29 August that harness reported 2.53 times as many characters as translating only final sentences, and about US$2.55 per hour of call [16]. The multiplier is a property of his test track and Deepgram's segmentation, so it transfers to your workload only if your speech density and your partial-result frequency look like his.

Soniox's usage summary reports US$31.23 for 202.6 hours of audio on stt-rt-v5 between 30 August and 17 September, which the post works out to US$0.154 per hour [23]. Against US$2.55, the post on dev.to calls that 17 times less [24]; 2.55 divided by 0.154 is 16.6 [25], and the absolute saving is about US$2.40 per hour of call [26]. Latency went the other way. The August Deepgram bench put a final sentence a little over 2 s behind the speech, and on 8 September Soniox produced 4.3 s and 7.6 s [27], up to 5.6 s worse [28]. The developer writes that the price is paid in delay, not in money [29].

The post also lists behaviour the documentation did not cover. The server announces the end of a sentence with a token, but the translation runs a few tokens behind the speech, so closing the caption line on that token glues the tail of one translation onto the next sentence; he holds a 700 ms window instead [20]. Language codes come back without region, so pt-BR and pt-PT both arrive as pt, and zh-Hans and zh-Hant both arrive as zh [21]. The temporary key's TTL limits how long the key may open streams, not how long an already-open stream may run, so a call of any length fits inside a TTL of a few minutes [18].

One more constraint sits under all of it. `getMediaStreamId` requires that the extension was invoked in that tab, and activeTab lasts only until the next navigation [11]. Tab audio is all there is: the Zoom and Teams desktop apps and the phone are out of reach [12].

What to watch

  • Whether Soniox's stt-rt-v5 holds near US$0.154 per audio hour beyond the 202.6 hours already billed.
  • Whether the 700 ms hold after the end-of-sentence token still works if the translation lag changes.
  • Whether activeTab keeps expiring at the next navigation, which is what forces the click-per-reload step.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories