Fox 2026-06-06: "fedora chrome is flawless besides not able to hear
any mics it was working a few days back and nothing was changed on
the system, only thing we changed was our zebra codes."
The race: on a listener joining a room with existing speakers, the
SFU sub PC ontrack can fire BEFORE the signal-server peer-joined
event populates `members`. handleRemoteSfuTrack already does prefix
resolution at line ~4640:
let pubHex = pubHex16;
for (const [, mm] of members){
if (fh.startsWith(pubHex16)){ pubHex = fh; break; }
}
When the roster is empty, the loop finds nothing, pubHex stays the
16-char streamID prefix, and sfuStreamsByPubHex.set(pubHex, stream)
caches under that short key. peer-joined arrives later,
flushSfuStreams runs to attach what was cached — but its inner match
was strict ===:
if (mm.pubkey && hex(unb64(mm.pubkey)) === pubHex){
mm.pubkey decodes to the FULL 64-char hex; pubHex from the cache is
the 16-char prefix; === never matches; listener stays permanently
silent for every speaker who was already in the room.
Pre-cascade this defect was masked: the old guard `!remoteAudio.has(uuid)`
was always true for worklet listeners (remoteAudio is the <audio>
fallback path only), so flushSfuStreams re-attached every cached
stream on every peer-joined — the eventual second ontrack from a
later renegotiation would land with members populated, cache key
became the full pubhex, and === matched. The 2e74b92 fix replaced
the always-true guard with `!listenerAudioNodes.has(uuid)`, which
correctly skipped re-attach but also exposed the strict-equality
matcher in the cold path.
Fix: switch flushSfuStreams' inner match from `=== pubHex` to
`fh.startsWith(pubHex)`. Symmetric with handleRemoteSfuTrack's own
prefix resolution. Works for both cases:
- cache key is full 64-char pubhex → startsWith with a full string
requires equality, so behavior is unchanged when ontrack arrived
after peer-joined (the common case).
- cache key is 16-char prefix → startsWith matches the first 16
chars of any member's full pubhex. 64 bits of prefix entropy =
astronomical collision probability.
Pinned by 29 new assertions in test/listener-audio-attach.test.js,
extracted from the live page so they cannot drift:
- 22 cover the attach FSM (chain reachability, dedup, in-place
swap, jbuf race, idempotent re-attach).
- 7 cover handleRemoteSfuTrack including the failing scenario:
"ontrack ARRIVES BEFORE peer-joined (member roster empty) →
cached + audible after flush" — fails pre-fix, passes post-fix.
Makefile gets test-listener-audio + adds it to test-all.
151 lines
5.9 KiB
Makefile
151 lines
5.9 KiB
Makefile
CC = gcc
|
|
CFLAGS = -Wall -Wextra -O2 -Iinclude $(shell pkg-config --cflags libpulse)
|
|
LDFLAGS = $(shell pkg-config --libs libpulse) -lrt -lpthread
|
|
|
|
PULSE = src/pulse.c
|
|
|
|
.PHONY: all clean serve blog test test-all test-web test-zebra-spaces stamp zebrad
|
|
|
|
# stamp each web page with today's date + its own md5/sha256 (run before deploy)
|
|
stamp:
|
|
@node web/stamp.js web/chat.html web/zebra-audio.html web/zebra-spaces.html web/how-it-works.html web/host-your-own.html
|
|
|
|
all: tx rx chat bt carrier zebrad
|
|
|
|
test: test/unit
|
|
@./test/unit
|
|
|
|
# web modem/protocol tests — pure Node, no browser or second device needed
|
|
test-web:
|
|
@node test/web-protocol.test.js
|
|
|
|
# zebra-spaces state-machine tests — pure unit tests for the FSM framework
|
|
# + each machine spec (publish / subscribe / call / remote-tile). Extracts
|
|
# the live code from web/zebra-spaces.html so the tests track the page.
|
|
test-fsm:
|
|
@node test/zebra-fsm.test.js
|
|
|
|
# Receive-side stream lifecycle (watchVideoTrackForRemoval) — fake-clock
|
|
# state-machine tests covering 'when is it safe to remove a tile'. Catches
|
|
# the regressions where transient mutes (NACK gaps, network blips, mobile
|
|
# handoffs, hard-refresh renegotiation churn) would otherwise kill live
|
|
# tiles. Extracts the function from the page and the shipped mute window
|
|
# so the assertions track what's live.
|
|
test-video-removal:
|
|
@node test/video-track-removal.test.js
|
|
|
|
# Mesh state-sync invariant tests — pins the contract fox stated as
|
|
# "whatever one device shares all should see, and when unshared none
|
|
# should see." Runs the shipped handleRemoteSfuTrack + renderVideoTile +
|
|
# removeVideoTile + watchVideoTrackForRemoval against multi-peer
|
|
# scenarios with synthetic ontrack/mute/unmute/ended events. Catches
|
|
# regressions where one peer's publish/unpublish leaves another peer
|
|
# out of sync.
|
|
test-mesh:
|
|
@node test/multi-peer-mesh.test.js
|
|
|
|
# Mod-action serializer — pins the kick race fix fox hit 2026-06-04
|
|
# ("kicked two phones, only one was kicked"). Extracts runModSerial +
|
|
# awaitStateUpdate + resolvePendingStateUpdate from the live page so
|
|
# the assertions track the shipped code, then drives synthetic mod
|
|
# actions through them with a stubbed roomEpoch counter to prove the
|
|
# second action signs against the FRESH epoch.
|
|
test-mod-actions:
|
|
@node test/mod-action-serializer.test.js
|
|
|
|
# SelfListenerFSM — pins the state-machine contract for the speaker/
|
|
# cohost/host "switch myself to the buffered HTTP listener stream"
|
|
# toggle. Extracts createFSM + selfListenerSpec from the live page so
|
|
# the spec can't drift from shipped transitions (off↔on with
|
|
# TOGGLE/ENABLE/DISABLE/UNMUTE/DEMOTED/CLEAR edges).
|
|
test-self-listener:
|
|
@node test/self-listener-fsm.test.js
|
|
|
|
# Listener audio attach pipeline — pins src→(jbuf)→gain→destination
|
|
# graph reachability through attachAudioStreamViaWorklet,
|
|
# setWorkletStream, installJitterBuffer, attachSfuTrack,
|
|
# flushSfuStreams. Extracts each function from the live page and
|
|
# replays the cascade scenarios (fresh attach, same-stream no-op,
|
|
# different-stream in-place swap, dedup by pubkey, publisher rejoin,
|
|
# zero-live-track reject). Catches silent-listener regressions where
|
|
# a chain builds but never reaches audioCtx.destination.
|
|
test-listener-audio:
|
|
@node test/listener-audio-attach.test.js
|
|
|
|
# zebra-spaces JS↔Go protocol parity + vault + ed25519 + (optionally) a live
|
|
# server flow. The live-server tier auto-runs when proxy.unturf.com sits
|
|
# alongside this checkout AND has a Go toolchain — we build the relay binary
|
|
# transparently and point the test at it. Otherwise that tier is skipped and
|
|
# only the pure-protocol/crypto tiers run.
|
|
test-zebra-spaces:
|
|
@bin=""; \
|
|
if [ -d ../proxy.unturf.com/cmd/zebra-spaces-signal ]; then \
|
|
go=$$(command -v go || echo /home/fox/.local/go/bin/go); \
|
|
if [ -x "$$go" ]; then \
|
|
echo "Building zebra-spaces-signal for live-server test..."; \
|
|
(cd ../proxy.unturf.com && "$$go" build -o /tmp/zspc-signal-test ./cmd/zebra-spaces-signal/) && bin=/tmp/zspc-signal-test; \
|
|
fi; \
|
|
fi; \
|
|
ZEBRA_SPACES_BINARY=$$bin node test/zebra-spaces.test.js; \
|
|
rc=$$?; rm -f /tmp/zspc-signal-test; exit $$rc
|
|
|
|
test-all: test/unit test/integration test/functional test-web test-fsm test-video-removal test-mesh test-mod-actions test-self-listener test-listener-audio test-zebra-spaces
|
|
@echo "--- unit ---"
|
|
@./test/unit
|
|
@echo "--- integration ---"
|
|
@./test/integration
|
|
@echo "--- functional ---"
|
|
@./test/functional
|
|
@echo "--- web protocol ---"
|
|
@node test/web-protocol.test.js
|
|
@echo "--- zebra-fsm ---"
|
|
@node test/zebra-fsm.test.js
|
|
@echo "--- video-track-removal ---"
|
|
@node test/video-track-removal.test.js
|
|
@echo "--- multi-peer-mesh ---"
|
|
@node test/multi-peer-mesh.test.js
|
|
@echo "--- mod-action serializer ---"
|
|
@node test/mod-action-serializer.test.js
|
|
@echo "--- self-listener FSM ---"
|
|
@node test/self-listener-fsm.test.js
|
|
@echo "--- listener audio attach ---"
|
|
@node test/listener-audio-attach.test.js
|
|
@echo "--- zebra-spaces ---"
|
|
@$(MAKE) -s test-zebra-spaces
|
|
|
|
test/unit: test/unit.c include/zebra.h include/modem.h test/test.h
|
|
$(CC) $(CFLAGS) -o $@ test/unit.c
|
|
|
|
test/integration: test/integration.c src/pulse.c include/zebra.h include/modem.h test/test.h
|
|
$(CC) $(CFLAGS) -o $@ test/integration.c src/pulse.c $(LDFLAGS)
|
|
|
|
test/functional: test/functional.c src/pulse.c include/zebra.h include/modem.h test/test.h
|
|
$(CC) $(CFLAGS) -o $@ test/functional.c src/pulse.c $(LDFLAGS)
|
|
|
|
tx: src/tx.c $(PULSE)
|
|
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
|
|
|
rx: src/rx.c $(PULSE)
|
|
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
|
|
|
chat: src/chat.c $(PULSE)
|
|
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
|
|
|
bt: src/bt.c $(PULSE)
|
|
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
|
|
|
carrier: src/carrier.c
|
|
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
|
|
|
zebrad: src/zebrad.c include/zebra.h
|
|
$(CC) $(CFLAGS) -o $@ src/zebrad.c $(LDFLAGS) -lm
|
|
|
|
blog:
|
|
python3 blog/build.py
|
|
|
|
serve: blog
|
|
cd web && python3 -m http.server 8765
|
|
|
|
clean:
|
|
rm -f tx rx chat bt carrier zebrad test/unit test/integration test/functional
|
|
rm -rf web/blog/001-volume-modem web/blog/002-sse-chatroom web/blog/index.html
|