Commit 0dccc82
fix(voice): make calls actually deliver media — five root-cause fixes (#673)
Voice/video calls connected (roster visible) but no audio/video ever
reached the remote peer. Root-cause investigation
(docs/reports/2026-06-07-voice-media-connectivity-investigation.md)
found five independent, compounding bugs; all but cross-NAT traversal
are fixed here and proven by a new 2-peer Playwright media test.
RC0 — voice wire addressed by channel name, gates validate UUIDs.
VoiceJoin/VoiceLeave/VoiceSignal carried the UI's channel *name*, but
the SEC-V-03 existence gates check ServerState.channels (keyed by
UUID), so every voice message was dropped — presence and signaling
never crossed at all. The wire now carries the canonical channel_id:
senders resolve name->id (channel_id_for_voice), the listener gates
resolve id->name and keep voice state / ClientEvents name-keyed to
match the UI. Security tests updated to the UUID-on-wire contract.
RC2 — 4 KB SIGNALING_CAP silently dropped video SDP. Video offers
run 5-15 KB; unpack_wire discarded them post-decode so the offerer
never got an answer. VoiceSignal now has a dedicated 64 KB SDP_CAP
(still inside the 256 KB transport cap); oversize is still rejected.
RC3 — early remote ICE candidates were lost. addIceCandidate rejects
while remoteDescription is null (browsers don't buffer remote
candidates) and the rejection was swallowed with `let _`. Candidates
arriving before the offer/answer (gossip has no ordering; ICE handling
was synchronous while offer/answer were spawned) are now queued in a
per-peer PendingIceCandidates buffer and flushed after
setRemoteDescription resolves; rejections are logged.
RC4 — RefCell<VoiceManager> borrow held across await. The "safe on
single-threaded WASM, no preemption" comment was wrong: await is a
yield point, and a concurrent VoiceSignal task could double-borrow and
panic mid-negotiation. VoiceManager now owns its state behind interior
mutability with &self methods; the handle is Rc<VoiceManager> (no
outer RefCell), and the clippy::await_holding_refcell_ref allows are
gone, so the lint enforces the invariant from now on.
Perfect negotiation — the collision check tested only making_offer,
not signalingState != stable (the canonical MDN condition), which
breaks renegotiation (screen share added mid-call). Extracted as the
pure should_ignore_offer(polite, making_offer, stable) and fixed.
Also: self-offer filter (don't offer to our own echoed VoiceJoin),
ICE/connection state-change logging (failures were invisible), and
TURN credential plumbing — build_rtc_config only ever set urls, so it
could not drive TURN even if configured; a new
window.__WILLOW_ICE_SERVERS knob carries {urls, username, credential}
(legacy __WILLOW_STUN_URLS still honored).
NOT fixed here (follow-up, needs infra + spec): cross-NAT traversal.
iceServers stays empty by default (privacy-first, issue #179); the
plan is self-hosted coturn beside the relay — see the report's
recommendation. Same-host/LAN calls work without it; the "iroh relay
path for ICE" idea was researched and is infeasible for browsers
(relay-only WebSocket, not TURN/ICE-compatible).
Tests: ~10 KB SDP wire round-trip (native); ICE buffer, collision
rule, TURN credential propagation, ICE-server config knobs
(wasm-pack); new e2e/voice-video.spec.ts under a voice-chrome
Playwright project with fake media devices proving remote audio both
ways + screen-share renegotiation over a real RTCPeerConnection.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>1 parent c69d023 commit 0dccc82
13 files changed
Lines changed: 1274 additions & 282 deletions
File tree
- crates
- client/src
- common/src
- web
- src
- components
- tests
- docs
- reports
- e2e
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
644 | 644 | | |
645 | 645 | | |
646 | 646 | | |
647 | | - | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
| 652 | + | |
648 | 653 | | |
649 | | - | |
| 654 | + | |
650 | 655 | | |
651 | 656 | | |
652 | | - | |
| 657 | + | |
653 | 658 | | |
654 | 659 | | |
655 | 660 | | |
656 | 661 | | |
657 | 662 | | |
658 | | - | |
659 | | - | |
| 663 | + | |
| 664 | + | |
660 | 665 | | |
661 | 666 | | |
662 | 667 | | |
| |||
680 | 685 | | |
681 | 686 | | |
682 | 687 | | |
683 | | - | |
| 688 | + | |
684 | 689 | | |
685 | 690 | | |
686 | 691 | | |
| |||
695 | 700 | | |
696 | 701 | | |
697 | 702 | | |
698 | | - | |
| 703 | + | |
| 704 | + | |
| 705 | + | |
699 | 706 | | |
700 | | - | |
| 707 | + | |
701 | 708 | | |
702 | 709 | | |
703 | | - | |
| 710 | + | |
704 | 711 | | |
705 | 712 | | |
706 | 713 | | |
707 | 714 | | |
708 | 715 | | |
709 | | - | |
710 | | - | |
| 716 | + | |
| 717 | + | |
711 | 718 | | |
712 | 719 | | |
713 | 720 | | |
| |||
717 | 724 | | |
718 | 725 | | |
719 | 726 | | |
720 | | - | |
| 727 | + | |
721 | 728 | | |
722 | 729 | | |
723 | 730 | | |
| |||
734 | 741 | | |
735 | 742 | | |
736 | 743 | | |
737 | | - | |
| 744 | + | |
738 | 745 | | |
739 | | - | |
| 746 | + | |
740 | 747 | | |
741 | 748 | | |
742 | | - | |
| 749 | + | |
743 | 750 | | |
744 | 751 | | |
745 | 752 | | |
746 | 753 | | |
747 | 754 | | |
748 | | - | |
| 755 | + | |
749 | 756 | | |
750 | 757 | | |
751 | 758 | | |
752 | | - | |
| 759 | + | |
753 | 760 | | |
754 | 761 | | |
755 | 762 | | |
| |||
1604 | 1611 | | |
1605 | 1612 | | |
1606 | 1613 | | |
| 1614 | + | |
| 1615 | + | |
| 1616 | + | |
| 1617 | + | |
| 1618 | + | |
| 1619 | + | |
| 1620 | + | |
| 1621 | + | |
| 1622 | + | |
| 1623 | + | |
| 1624 | + | |
| 1625 | + | |
1607 | 1626 | | |
1608 | 1627 | | |
1609 | 1628 | | |
| |||
1652 | 1671 | | |
1653 | 1672 | | |
1654 | 1673 | | |
| 1674 | + | |
| 1675 | + | |
1655 | 1676 | | |
| 1677 | + | |
1656 | 1678 | | |
1657 | 1679 | | |
1658 | 1680 | | |
| |||
1665 | 1687 | | |
1666 | 1688 | | |
1667 | 1689 | | |
1668 | | - | |
| 1690 | + | |
1669 | 1691 | | |
1670 | 1692 | | |
1671 | 1693 | | |
| |||
1679 | 1701 | | |
1680 | 1702 | | |
1681 | 1703 | | |
1682 | | - | |
| 1704 | + | |
1683 | 1705 | | |
1684 | 1706 | | |
1685 | 1707 | | |
| |||
1748 | 1770 | | |
1749 | 1771 | | |
1750 | 1772 | | |
| 1773 | + | |
1751 | 1774 | | |
| 1775 | + | |
1752 | 1776 | | |
1753 | | - | |
1754 | | - | |
| 1777 | + | |
| 1778 | + | |
1755 | 1779 | | |
1756 | 1780 | | |
1757 | 1781 | | |
| |||
1763 | 1787 | | |
1764 | 1788 | | |
1765 | 1789 | | |
1766 | | - | |
| 1790 | + | |
1767 | 1791 | | |
1768 | 1792 | | |
1769 | 1793 | | |
| |||
1781 | 1805 | | |
1782 | 1806 | | |
1783 | 1807 | | |
1784 | | - | |
| 1808 | + | |
1785 | 1809 | | |
1786 | 1810 | | |
1787 | 1811 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
156 | 156 | | |
157 | 157 | | |
158 | 158 | | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
159 | 183 | | |
160 | 184 | | |
161 | 185 | | |
| |||
699 | 723 | | |
700 | 724 | | |
701 | 725 | | |
702 | | - | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
| 730 | + | |
| 731 | + | |
703 | 732 | | |
704 | 733 | | |
705 | 734 | | |
706 | 735 | | |
707 | 736 | | |
708 | | - | |
| 737 | + | |
| 738 | + | |
709 | 739 | | |
| 740 | + | |
710 | 741 | | |
711 | 742 | | |
712 | 743 | | |
713 | 744 | | |
714 | 745 | | |
| 746 | + | |
| 747 | + | |
| 748 | + | |
| 749 | + | |
| 750 | + | |
715 | 751 | | |
716 | | - | |
| 752 | + | |
717 | 753 | | |
718 | 754 | | |
719 | 755 | | |
| |||
736 | 772 | | |
737 | 773 | | |
738 | 774 | | |
| 775 | + | |
| 776 | + | |
| 777 | + | |
| 778 | + | |
| 779 | + | |
| 780 | + | |
739 | 781 | | |
740 | | - | |
| 782 | + | |
741 | 783 | | |
742 | 784 | | |
743 | 785 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
43 | | - | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
44 | 49 | | |
45 | | - | |
| 50 | + | |
46 | 51 | | |
47 | 52 | | |
48 | 53 | | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
49 | 58 | | |
50 | | - | |
| 59 | + | |
51 | 60 | | |
52 | 61 | | |
53 | 62 | | |
| |||
0 commit comments