Releases: mormegil6/ambisonic-box
Release list
v1.3.5
Two defects that arrived with the keep-alive track in v1.3.4 are fixed, and the second of them silently cost a viewer their spatial audio.
A second audio AdaptationSet made video.js offer an audio track selector. Both entries render unlabelled, and choosing the wrong one points the video element at the silent keep-alive, which on the native decode path takes the programme away from the feed. The selector is no longer shown: there is exactly one correct track and the feed already selects it, so the control offered nothing but a way to break playback.
Worse, dash.js persists the last selected track to local storage and restores it on every later visit, so a viewer who touched that menu once stayed silent afterwards, across reloads, until they cleared site data. That cache is now disabled, which stops both the write and the restore, so an affected client recovers on its next load.
Picture-in-Picture is now offered. Both engines pause a backgrounded video by policy and both exempt PiP, so it is the sanctioned way to keep listening after leaving the browser, and the control falls back to webkitSetPresentationMode where the standard call is missing. Closing the window pauses playback; returning to the tab does not. Automatic Picture-in-Picture is not available here, since both engines gate it on the video element being the fullscreen presentation and this one is a texture source for the sphere rather than the picture on screen.
v1.3.4
The SRT gateway probed only the audio channel layout, so an H.265 push was admitted as first order and then died at earshot's muxer, with the reason reaching nobody but a container log. The gateway now probes the video codec as well and refuses what it cannot carry, and the refusal arrives as a sentence on the player page the pusher is already watching. An unresolved probe falls back to the previous behaviour rather than refusing.
H.265 is carried end to end. The MP4 codec tag differs per codec and cannot be deferred to ffmpeg, so the direct listener port now encodes the video codec alongside the track count and the gateway dials the matching one. H.264 keeps ports 9100 and 9101 unchanged, so a version-skewed deployment loses H.265 rather than breaking H.264.
Carrying H.265 is not the same as it playing. A browser without an HEVC decoder reports no support, the player drops the video track, and the viewer gets no picture while the clock advances and every check reports healthy. This stack passes video through and cannot transcode a fallback rung, so a guest pushing H.265 narrows the audience for that session.
The silent stereo keep-alive AdaptationSet ships on every live path, asserted by the pipeline test with the codec the effective FFMPEG_FLAGS implies. docs/IOS-SAFARI.md is corrected: measured on an iPhone Xs, the track does not stop iOS suspending a backgrounded element, since iOS restricts background playback by media type. It makes the session resumable instead, the Now Playing entry producing sound rather than silence. On macOS it prevents the loss outright, unchanged.
v1.3.3
The two live-playback failures on iPhone that v1.3.2 left open are closed, and they turned out to share one root. The player's media session was being torn down by its ManagedMediaSource getting detached: WebKit's load algorithm closes a detached managed source permanently, the dying source emits its streaming events after it is already closed, and dash.js latches its scheduler on one of them with no way back, so a session died with zero requests and no error anywhere. The player now notices the detach on the element's own emptied event and recovers in place, re-attaching the source and resuming within about three seconds instead of reloading the page, with the old reload kept as a fallback. The WASM audio feed learns about the recovery explicitly, since the re-attach rebases the media clock before any event the feed can see, and re-anchors itself on the new timeline.
The silence that outlived those teardowns was the second failure, and it was not the player's at all. The same silent GPU-process relaunch that detaches the source also strands the tab's audio-session category: WebKit's web process caches the category and never re-sends it to the relaunched GPU process, so the tab collapses into the Ambient category, which the iPhone's Ring/Silent switch mutes in hardware while every API keeps reporting success, the analyser included. The player now cycles navigator.audioSession.type away and back on every pageshow and after every recovery, which forces the category past the stale cache and lets a wedged tab heal itself. The full mechanism, the on-device confirmation and the workaround are filed as WebKit bug 323104; the dash.js half is reported as issue 5127 with the recovery offered upstream as PR 5128.
The player also ships the field forensics that found all of this: per-session resync and audio-context state trails in its beacons, plus a backgrounded-tab check so the teardown detector runs even when the tab is not visible. The full account, with device screenshots, is in docs/IOS-SAFARI.md.
Separately, the player's nginx base image is bumped to 1.31.4-alpine, taking the base OS's security updates with it.
v1.3.2
A bug in last release's own audio fix: closing the AudioContext on pagehide did not check whether the page was actually unloading. WebKit fires that event with persisted: true when it is caching a page for a likely back or forward restore, not destroying it, and the whole JavaScript state survives that including the context object itself. Switching tabs and back, or a back-arrow navigation and back, both left a page that had never truly unloaded with a context closed for the rest of its life. No reload recovered it; only quitting Safari did.
The fix checks event.persisted and skips the close when the page is being cached rather than destroyed. Verified in both directions: a persisted pagehide now leaves a running context running, and a genuine one still closes it as before.
While reproducing that bug, live captures also confirmed a separate dash.js defect independent of it: a healthy session can silently tear down mid-playback, with no ERROR event ever firing on the public event bus, on both MediaSource and ManagedMediaSource. That is not fixed here. It has been reported upstream, and this player's existing recovery watchdog still catches the resulting stall and reloads within about 30 seconds.
v1.3.1
Live audio on iPhone works, contrary to what v1.3.0's release notes said. Investigating why it looked broken turned up three real defects, all fixed here.
The feed treated an ordinary manifest refresh as an encoder restart, because a live window's own segment numbering and timing advance on every update. Each false restart stranded its entire encoded ring instead of reclaiming it, so the ring grew without bound over a session, tens of megabytes an hour on a phone. A muted <video> element's own state was written back into the player's volume control on every change, which could re-enter itself and pin the audio gain at zero while the control bar still showed full volume. On a touchscreen the volume slider applies whatever value a tap lands on, with no drag required, over a range about 35 by 3 pixels; it is now hidden wherever the pointer is not a mouse, and level lives on the hardware buttons there instead.
A media session that fails with SRC_NOT_SUPPORTED, the "media could not be loaded" error with no recovery, is now caught and reloaded within about 30 seconds instead of being left dead. What raises that error in the first place is not established.
One failure stays entirely outside this player's reach: a Safari content process can lose all audio output while every API it exposes, Web Audio and a plain <audio> element alike, keeps reporting success. Nothing on the page can detect or repair it; only quitting Safari does. See docs/IOS-SAFARI.md for the measurements behind both open items.
v1.3.0
Third-order Ambisonics now plays on iPhone. The on-demand clips were verified end to end on an iPhone Xs (A12, iOS 18.7): video, 16-channel spatial audio, head tracking, fullscreen and looping, from the same DASH stream every other client receives. On the live stream the same device plays video without audio, which is open.
Four things stood between the player and iOS. dash.js treats ManagedMediaSource's endstreaming as a permanent stop and blocks even the init segment, which is an upstream defect still present on their development branch. No iPhone before the A17 Pro decodes AV1, so each clip now carries an H.264 ladder beside the AV1 one, with selectionPriority keeping capable devices on the 8K rungs. A muted element makes iOS treat the page as ambient sound, so the audio session is declared explicitly and the AudioContext is closed on pagehide. Device orientation is requested from the touch that starts playback, not from the play event, which fires too late to carry the gesture. docs/IOS-SAFARI.md has the detail and the measurements.
Independently of iOS: VOD manifests were served with no Cache-Control, so a viewer who watched before a repackage kept a manifest naming files that no longer existed and got a player that spun forever. Manifests now ship no-cache. See docs/VOD.md for the two-codec packaging and the cache policy.
Known limits, including live audio and a short gap at the top of a loop, are listed in docs/IOS-SAFARI.md.
v1.2.0
The on-demand clips now play in Safari, which they did not before. The cause was a byte-order bug in how Shaka Packager writes the Opus dOps box, unfixed since 2016 and reported as shaka-packager#1627. The packager is therefore built here rather than pulled, in services/shaka, on the tools profile so it costs nothing unless you package clips. Two smaller things were needed alongside it: a static manifest, without which dash.js treats a two minute clip as a live stream and starts it ninety seconds in, and a rebuilt player, carrying an Opus-probe bypass that had sat on the fork's master since v1.1.0 without reaching a shipped image.
The captions and quality buttons had been appearing on roughly two Safari loads in nine. data-setup on the video element let video.js build the player itself with empty options, racing the player's own constructor and discarding the quality plugin and caption settings whenever it won. Chromium never lost that race, which is why it looked like a Safari bug. The quality menu also kept a stale tick when a rung was picked while paused, in every browser.
Not fixed: audio stops when a Safari tab or Space loses visibility. WebKit suspends a backgrounded muted video element that carries no decodable audio track, and Safari drops the 16-channel Opus set. The answer is a silent AAC keep-alive rendition in the manifest, and the on-demand clips are where it will land first.
Chromium and Firefox are unaffected throughout. The live path never touches Shaka and is unchanged.
v1.1.0
Pushing the wrong audio layout at the guest endpoint got you a dropped SRT connection and nothing else. The gateway knew exactly what was wrong, logged "unusable audio: 1 track(s) x 2 ch; expected 4x4 or 1x4", and that string never left the container. A tester worked through three ffmpeg invocations and OBS against a bare I/O error before writing in to ask.
SRT cannot carry the answer. A rejection reason exists in the handshake, but the audio layout is not known until media has been buffered and probed, and an established connection can only be dropped, never explained. The missing link was not the transport but the sequence: the gateway probes BEFORE it claims a slot, so a refused push never becomes a session telemetry knows about, and there is no session id for the existing done call to reference. /gw/session/reject accepts one without a session and records it while the slot is free.
The panel that displays it had existed since the RTMP path needed it, and had never once appeared for an SRT refusal: it was gated on the reason containing "playable output", the RTMP stall wording. It now shows whatever reason arrives, translated where the shape is known, with the ambisonic channel guidance attached only to the reasons that guidance explains. A stream that simply ran to its end is no longer told what channel counts are required.
Found while testing that, and worse: the player crashed and blamed telemetry. renderGuestNotice guarded the optional brand.json payload on one line and dereferenced it unguarded on the next, so a guest going live on a deployment without branding threw inside the /api/live handler. The throw landed in that fetch's .catch, which told the visitor "the server status service is not responding" while telemetry answered 200 throughout, leaving the player unusable for the whole session it exists to show. Note that a .catch cannot tell a failed request from a handler that threw, so any future render bug will be reported the same way.
Safari can play this stream, which it could not before: it decodes multichannel Opus through no native route, so the player now fetches the DASH audio segments itself and decodes them with libopus compiled to WebAssembly in a worker, scheduled against the video element's clock. Read it as progress rather than support. Moving Safari to another Space still stops the audio after about two seconds, because the fix needs a silent audio track in the manifest that earshot does not emit yet, and iPhone remains blocked behind a separate gate in videojs-contrib-dash. docs/IOS-SAFARI.md carries the measurements and what each route costs.
The status badge now names the source. It read LIVE for the demo loop, a guest and an owner alike, so when the loop resumed after a guest session the picture changed while the page said the same thing.
rtmp-ingest picks up Alpine's patched OpenSSL (CVE-2026-14456). It shipped 3.5.7-r0 because apk add resolves what is MISSING and leaves what the base image already carries, which no amount of rebuilding changes. A comment in another image asserted the opposite and is corrected.
GUEST-ENDPOINT.md now states the four-channel requirement next to the connection URL rather than in a failure-mode bullet further down, and no longer claims a wrong layout takes ~45 s to be refused: that is the RTMP path, and SRT rejects at about 15.
No change to the streaming path, the DASH shape, or the codecs.
v1.0.2
One fix. The stream-stalled check asked only whether someone was publishing and whether segments were old, never whether that publisher had been up long enough to be responsible. Because segment_age_s keeps growing while the guest slot is empty and the demo loop is idle, a publisher inherited the entire preceding gap the moment it claimed.
Seen on the deployment: a guest was alerted as "publishing but segments 33s stale" four seconds after connecting, for 29 s that accrued while the slot was free. The recovery line then reported a 51 s worst case covering a period with no writer at all.
The condition now also requires the publisher to have been publishing for at least SEG_STALE_S, so it can only be blamed for staleness it had time to cause. A genuine stall still fires, SEG_STALE_S later.
No change to the streaming path.
v1.0.1
Both bitrate studies were re-measured on content-selected windows and the write-ups only partly updated. This release brings the documents back into agreement with their own data, and fixes twelve code defects found while checking them.
Measurement studies
- Opus: the date header, the localisation ratio and the figure all described the superseded run. The plot script transcribed its numbers by hand; it now reads results.tsv, so a re-measurement cannot leave the figure behind.
- AAC: named the wrong ffmpeg version twice (7.1, not 4.3), and its BAM-Q section claimed AMBIQUAL corroborated it when the two metrics point opposite ways. The divergence is the more interesting result and is now explained.
- BITRATE.md cited the AAC study for a knee at 96 kbit/s per channel that the study explicitly denies.
Fixes
- setup.cmd never forwarded its arguments, so --source was a documented no-op on Windows.
- Both measurement harnesses could overwrite excerpts.tsv with the wrong offsets, or none, on a warm cache. That file records the windows behind every published figure.
- measure-bamq.sh ignored an explicit MATLAB= and never started the static server binauralize.js needs.
- The VOD player's retry button was never bound, and a network failure was reported as an HTTP status.
Upstream
- EnvelopSound/Earshot#83 and #84 merged. thomasdeppisch/hoast360#34 and #35 opened. QxLabIreland/Ambiqual#2 filed: the tool cannot run as published.
- The earshot fork's history was rebuilt so its delta from upstream is three commits rather than twenty of merge bookkeeping. The tree is unchanged.
No functional change to the streaming path.