Skip to content

v1.5.6 — receiver MSE+SourceBuffer (live-edge seek)

Choose a tag to compare

@Kepners Kepners released this 11 May 15:14

PC Nest Speaker 1.5.6

The receiver is the change. Desktop bytes are unchanged from 1.5.5 except the version string — the work all lives in receiver-audio.html on GitHub Pages, which the Cast SDK reloads on every cast (force-relaunch from 1.5.0 onwards).

What changed — receiver-side

MSE + SourceBuffer path (preferred, with <audio> fallback)

Problem. Plain <audio src=stream> on Cast Audio runs an opaque internal jitter buffer we can't read or write. Steady-state latency floored at ~1–1.5 s and currentTime writes against a live HTTP-chunked source are likely silent no-ops (per MDN, and confirmed by absence of a seek-probe in v1.5.4/1.5.5 logs because we never had a chance to fire one).

Fix. New connectHttpPcmMse() is now the preferred path:

  • MediaSource + addSourceBuffer('audio/webm; codecs="opus"') with mode='sequence'.
  • fetch().body.getReader() pumps WebM clusters into the SourceBuffer.
  • A 250 ms drift loop calls sb.remove(0, currentTime - 0.5s) to trim history and audio.fastSeek(buffered.end - 0.2s) to push the playhead toward the live edge when we lag.
  • One-shot mse-seek-probe reports back whether the seek inside the MSE buffer actually moves the playhead.

Watchdog + automatic fallback

Cast Audio Chromium has historically had fetch().body.getReader() stalls (the existing CLAUDE.md notes this). 1.5.6's MSE path arms a 3.5 s first-byte watchdog. If no bytes arrive — or any of MediaSource.isTypeSupported, sourceopen, addSourceBuffer, fetch, or appendBuffer fail — the receiver tears down MSE and falls through to the plain <audio src> path that's been working since 1.5.0. So this is strictly additive: best case we cut 300–700 ms off latency, worst case we land on the same path as 1.5.5.

Why this approach (research summary)

Inspired by:

  • shaka-player #9872 — production live SourceBuffer.remove() + seek-to-live pattern
  • w3c/media-source #291 — documents the seekable window after remove()
  • owntone-server cast.c — the only open-source path proven to hit ~400 ms total latency on Cast Audio. They use Google's built-in mirroring receiver 85CDB22F with Cast RTP / Opus / targetDelay=400. If MSE doesn't crack it, that's where 1.5.7 goes — but RTP is a 1–2 week rewrite, so we test cheaper options first.

What you'll see in the desktop log

Where 1.5.5 only emitted audio-src-set / audio-loadedmetadata / audio-canplay / audio-playing, 1.5.6 will additionally emit (when MSE works):

mse-attempt
mse-src-set
mse-sourceopen
mse-sb-added
mse-fetch-headers
mse-first-byte
mse-canplay
mse-playing
mse-seek-probe { seek_works: true|false, ... }   ← the answer we've been waiting for
mse-drift-resync / mse-drift-catchup              ← drift management firing

If MSE fails for any reason you'll see mse-fallback-to-audio reason=... followed by the same audio-* events as 1.5.5.

Files changed

  • pcnestspeaker-receiver/receiver-audio.htmlconnectHttpPcmMse() + dispatcher + capabilities probe extended with mse_isTypeSupported_webm_opus
  • package.json — version bump

Desktop binaries are unchanged from 1.5.5 functionally; rebuilding only so the version string and "About" dialog match the receiver.