webrtc-rs 0.20 upgrade¶
Prompt¶
Port shakenfist-spice-webrtc from webrtc-rs 0.17.1 to 0.20.x.
This is a deliberate deferral, not a discovery: Renovate raised
the bump as PR #245 on 2026-08-03, CI failed with 25 compile
errors in the lib and 36 in the lib tests, and we chose to pin
webrtc < 0.18 and write this plan rather than either rush the
port or leave a permanently-red PR open. PR #245 was repurposed
to carry the pin and this document.
Before executing any phase, read
shakenfist-spice-webrtc/src/bridge.rs end to end — it is the
only production file that touches webrtc-rs, and the port lives
almost entirely inside it. Read the 0.20 API docs at
https://docs.rs/webrtc/0.20.0/webrtc/ rather than trusting the
API sketch in this plan; it was written from the docs index and
the CI error output, not from a compiling port.
Follow the project's plan conventions: per-phase plan files named
PLAN-webrtc-0.20-upgrade-phase-NN-*.md, one logical change per
commit, this master plan's phase table updated as work lands.
Situation¶
What we depend on today¶
Updated after phase 01 landed; the shape below is develop
e07cfd4f, not the tree this plan was first written against.
Phase 01 removed a manifest, a duplicated client peer connection,
and both of the API calls that had no direct replacement, so
several of the original numbers here were substantial
overstatements of the remaining work.
webrtc = "0.17.1" appears in exactly one manifest:
shakenfist-spice-webrtc/Cargo.toml:27. Step 1c moved ryll's
--web signalling test onto the shared TestPeer helper, after
which ryll named no webrtc type at all and its dev-dependency
was deleted.
Cargo.lock resolves it to 0.17.2, alongside rtp 0.17.2 and
rtcp 0.17.2. rtp is also a direct dependency of both
shakenfist-spice-webrtc (Cargo.toml:36, added by step 1b) and
shakenfist-spice-renderer (Cargo.toml:137), so the
webrtc::rtp re-export is already off the port's critical path.
The abstraction boundary is good. WebrtcBridge in
shakenfist-spice-webrtc/src/bridge.rs (1448 lines) is the single
production chokepoint. The complete webrtc-facing surface of the
workspace is three files and 37 use webrtc:: lines:
| File | use webrtc:: lines |
Role |
|---|---|---|
shakenfist-spice-webrtc/src/bridge.rs |
18 | The production bridge |
shakenfist-spice-webrtc/src/test_client.rs |
13 | TestPeer, the shared client half (test-support feature) |
shakenfist-spice-webrtc/tests/loopback.rs |
6 | on_track / on_data_channel wiring specific to that test |
shakenfist-spice-webrtc/tests/lifecycle.rs,
ryll/src/web/* and
shakenfist-spice-renderer/tests/webrtc_h264_smoke.rs name no
webrtc type: lifecycle and the reaper go through WebrtcBridge
and StickySignal, and the renderer's smoke test uses the
standalone rtp crate.
Within bridge.rs, the webrtc-facing code is concentrated:
| Region | Lines | What it does |
|---|---|---|
BridgeEvents |
114–213 | The four callback bodies, shaped for 0.20's handler trait |
WebrtcBridge::new |
295–455 | Media engine, interceptors, PC, tracks, control DC, callback registration |
accept_offer |
492–517 | set-remote / create-answer / set-local / wait-for-gathering |
send_control / close |
596–624 | DC send, PC teardown |
| RTP pumps | 659–910 | track.write_rtp against rtp crate types |
Neither of the two API calls with no 0.20 replacement survives.
gathering_complete_promise() was replaced in step 1f by a sticky
gathered signal raised from on_ice_gathering_state_change, and
RTCPeerConnection::connection_state() was replaced in steps 1d
and 1e by a Mutex shadow fed by the state-change callback —
WebrtcBridge::connection_state (bridge.rs:949) and
TestPeer::connection_state (test_client.rs:289) both read it.
The four near-identical client-side peer connection setups the
first draft of this plan found are now one: TestPeer::build
(test_client.rs:94). Collapsing them was phase 01's largest
step.
What 0.20 changes¶
webrtc-rs 0.20 re-homes the crate on the sans-io rtc protocol
core, wrapped in a thin async layer. Note that the async model
survives — create_answer, set_remote_description, add_track
and friends are all still async. The CI errors reading
"Option<RTCSessionDescription> is not a future" are a
consequence of those methods moving onto a trait that is not in
scope, not of the API going synchronous.
The account below was written from the 0.20 docs index and PR
245's CI output, before anyone read the source. Phase-02 planning¶
checked it against webrtc 0.20.2 and found it right in outline and wrong in emphasis; the corrections are marked, and PLAN-webrtc-0.20-upgrade-phase-02-bump.md carries the full findings with citations.
1. Module reshuffling. api, interceptor, track and rtp
are gone as top-level modules; peer_connection::{configuration,
sdp, peer_connection_state} and rtp_transceiver::{rtp_codec,
rtp_transceiver_direction} flatten into their parents. Local
tracks move to media_stream::track_local::static_rtp.
APIBuilder becomes PeerConnectionBuilder; RTCConfiguration
gains an RTCConfigurationBuilder.
Correction: this is bigger than a reshuffle. webrtc 0.20 is a
thin shim that does not re-export its sans-io core, so rtc
becomes a direct dependency carrying MediaEngine, Registry,
the MIME constants and rtp::Packet. And the standalone rtp
crate phase 01 adopted is a dead line ending at 0.17.2 — RTP now
lives in rtc-rtp, reached as rtc::rtp. The API is
near-identical (codecs becomes codec), but it is a different
crate, so an rtp 0.17 Packet is a different type, not an
older one.
2. Trait-scoped methods. RTCPeerConnection's operations
moved onto an object-safe PeerConnection trait, so
use webrtc::peer_connection::PeerConnection is required before
any of create_answer, set_local_description, add_track,
create_data_channel or close resolve. Addition: the builder
returns an unnameable impl PeerConnection, so the bridge stores
Arc<dyn PeerConnection>.
3. The callback model inverted. Today bridge.rs registers
four callbacks after construction (:406, :412, :418,
:426). In 0.20 the peer-connection ones collapse into a single
PeerConnectionEventHandler impl — nine async methods, all
defaulted no-op — handed to the builder via .with_handler() at
build time, which is the one mandatory builder call. Phase 01
shaped BridgeEvents for exactly this.
Correction, and the biggest single miss in this section:
datachannel messages and remote tracks did not move onto the
handler, they stopped being callbacks altogether. on_message does
not exist; a channel is an Arc<dyn DataChannel> you poll() in
a loop, and remote tracks are the same. So the callback bodies
do move after all, from registrations into spawned poll loops —
the one thing phase 01's BridgeEvents comment promised would not
happen.
4. Things with no direct replacement.
gathering_complete_promise()— retired by phase 01 step 1f. The 0.20 ordering was checked and is safe: every candidate is in the ICE agent before theCompleteevent is dispatched, andlocal_description()re-renders from that agent on each call.connection_state()— retired by phase 01 steps 1d and 1e. Correction: it has no replacement anywhere in 0.20, not merely no trait method, so shadowing was the only option rather than the tidy one.RTCRtpTransceiverInitgained astreamsfield. Correction: the struct derivesDefault, so this is not breakage at all..with_udp_addrs(...)— 0.17 bound sockets internally; 0.20 makes the caller choose. Correction, and this one enlarges phase 02: the natural placeholder0.0.0.0:0binds fine and emits a literal0.0.0.0host candidate that browsers discard, while every Rust-to-Rust test still passes. Phase 02 therefore has to enumerate interface addresses rather than defer the question to phase 03.- New:
TrackLocalStaticRTP::newnow takes a wholeMediaStreamTrackand the caller supplies the SSRC and codec parameters that webrtc-rs previously chose.
Why this cannot be staged the usual way¶
The project convention is that every commit builds and passes
tests. A dependency major bump cannot honour that incrementally —
shakenfist-spice-webrtc is broken from the moment the version
changes until the port is complete, and the test crates break
with it. Phase 02 is therefore an unavoidably large atomic
commit.
Phase 01 exists to shrink it. A surprising amount of the work can be done against 0.17, where it compiles and tests today, so that the atomic step is mostly import rewriting.
Mission and problem statement¶
Get shakenfist-spice-webrtc onto webrtc-rs 0.20.x with --web
mode behaving identically — same SDP exchange, same H.264 and
Opus tracks, same control datachannel, same terminal-state
reaping — and remove the < 0.18 Renovate pin.
Out of scope: adopting 0.20's new capabilities. The release adds opt-in send back-pressure, GSO/GRO UDP batching, a bounded shared reactor pool, and a configurable SCTP receive window. Those are interesting for a video-streaming workload and may well justify their own plan, but tuning them during a port makes it impossible to attribute a regression. Port first, tune later.
Approach¶
Phase 01 — Pre-work on 0.17¶
Version-neutral refactoring that compiles and passes tests against 0.17.1 today, and shrinks the atomic step:
- Promote
rtp(andrtcpif used) to direct dependencies ofshakenfist-spice-webrtcat the version the lockfile already resolves, and switchbridge.rsoff thewebrtc::rtpre-export. Mirrors whatshakenfist-spice-rendereralready does, and the comment atshakenfist-spice-webrtc/Cargo.toml:15explaining the re-export pin goes away with it. - Collapse the three callback registrations into a single struct
with three methods, still registered through the 0.17 API. The
struct is then trivially re-targeted at
PeerConnectionEventHandlerin phase 02. - Collapse the four duplicated client-side peer connection setups
into one shared test helper behind a
test-supportfeature, so phase 02 rewrites that boilerplate once instead of four times across two crates. This is the largest step in the phase and was not visible when this master plan was first written. - Shadow the connection state inside that helper — the
state-change callback already sees every transition — so
wait_until_connectedreads the shadow rather than callingRTCPeerConnection::connection_state(). This removes one of the no-direct-replacement items entirely, and works identically on 0.17. - Give
accept_offeran explicit "gathering complete" signal — a stickyNotify+AtomicBoolpair raised from a newon_ice_gathering_state_changehandler, which 0.17 already provides — rather than callinggathering_complete_promise()inline, and prove the answer SDP carries the same candidate set. This validates the phase 02 design for the riskiest no-direct-replacement item while we can still fall back.
Each of these is its own commit. All of them are testable now,
which is the point. Detailed in
PLAN-webrtc-0.20-upgrade-phase-01-prework.md,
which corrects two things this master plan got wrong on first
writing: connection_state is test-only, and the client-PC
setup is duplicated four times.
Phase 02 — The atomic bump¶
Detailed in PLAN-webrtc-0.20-upgrade-phase-02-bump.md, which corrects most of the factual claims this master plan made about the tree — all in the direction of less work, because phase 01 did it — and revises the API account above from the 0.20.2 source.
Three preparatory commits that still build against 0.17, then one atomic commit, then cleanup:
- Move
tests/loopback.rs's post-constructionon_trackandon_data_channelregistrations intoTestPeerBuilder, since 0.20 has no post-construction registration to move them to. - Make the
BridgeEventsbodies non-blocking, because 0.20 awaits handler methods inline in the connection's driver loop. - Write and unit-test the UDP bind-address selection.
- The bump itself:
webrtc = "0.20.2",rtcin,rtpandrustlsout,PeerConnectionBuilderwith a build-time handler, datachannel and remote-track poll loops in place ofon_messageandread_rtp, tracks rebuilt with explicit SSRCs and codings. - Both webrtc rules out of
renovate.json, and the docs that name the version or describe the UDP port behaviour.
Green tests/loopback.rs (two bridges exchanging offer/answer
plus DC traffic) and tests/lifecycle.rs (terminal-state
detection) are necessary but explicitly not sufficient here: a
wrong bind address leaves both green and every browser broken, so
the phase also requires a real browser session before it closes.
Phase 03 — Socket binding configuration¶
Phase 02 answers the hard half of this — what to bind, which it
has to, because the placeholder the original plan proposed
(0.0.0.0:0) silently produces unroutable candidates. Phase 03 is
what is left: exposing that choice as configuration so an operator
can pin the media port or restrict the interface, which matters
behind a firewall or in a container.
Note that WebrtcBridgeConfig currently has no path from the
command line at all — ice_servers exists on the struct but ryll
passes an empty vector unconditionally
(ryll/src/web/signalling.rs:300-303, the only production
construction site; the other twenty are tests). So phase 03 builds
that plumbing rather than extending it, and should carry
ice_servers along with the bind address while it is there.
Phase 03's planning survey corrected two things this section said
or assumed. The signalling.rs line number above drifted by one
during phase 02. And "touches docs/configuration.md" turns out
to mean writing its web section rather than extending it:
docs/configuration.md documents no --web flag at all today,
not even the --web-host and --web-port that have shipped since
the web frontend landed. Phase 02's review also deferred two items
into this phase — an interface allowlist rather than only a port
pin, and an opt-in for loopback-only hosts — both of which
the phase plan
now carries.
Touches docs/configuration.md, docs/web-frontend.md and
docs/web-mode-internals.md.
Phase 04 — Soak validation and docs¶
The 0.20 release notes headline UDP batching via GSO/GRO,
elimination of tokio scheduler overhead in datachannel
operations, and opt-in send back-pressure. All three land on the
run_video_pump write path (bridge.rs:1576). Integration tests
exercise that path for seconds; a regression there shows up over
minutes.
Phase 04's planning survey corrected four claims this section made; they are fixed in place below, and the phase plan records what was wrong with each.
- A real browser session against a real SPICE guest, held long
enough to see steady-state behaviour. Listen to the audio
while it is open: phase 02's browser session confirmed the
playback channel negotiated Opus but nobody confirmed sound by
ear, so that Definition-of-done clause is inherited here.
Correction: this section originally asked for "the latency
HUD and runtime metrics captured". Neither exists under
--web— both are GUI-mode-only — which phase 01 discovered during 1a and worked around with external/procsampling. Phase 04 must sample the same way for the comparison to hold. - Chrome and Firefox at minimum; Safari if a Mac is available. Firefox is a known blocker inherited from phase 02: a Firefox that does not offer H.264 gets no video at all, because ryll encodes H.264 only. Land #289 (tell the viewer) before soaking, and settle whether a Firefox with a working OpenH264 plugin is enough for this criterion or whether ryll needs a second codec. Correction: phase 01's Baseline conditions block concluded Firefox "cannot be the phase-04 viewer on this host" after it failed to establish ICE under 0.17. Phase 02 contradicted that on 0.20 — ICE was fully healthy and everything but video worked — so the blocker is codec-specific, not transport-specific.
- Compare RSS and CPU against a 0.17 baseline captured before
the bump — take that baseline during phase 01 while we are
still on the old version. Correction: the baseline exists,
but the harness that produced it was never committed, so
reproducing its conditions is a phase 04 step
(
tools/web-soak.sh) rather than a given. - Run
RYLL_GATHERING_SOAK=1 make teston a quiet host: the 20-iteration invariant-candidate-count check on the gathering signal is off by default (host interface churn makes it flaky in CI) and this soak is exactly the deliberate occasion it is gated for. - Check
ARCHITECTURE.mdandAGENTS.mdagainst the bridge's shipped task and callback structure. Correction: this asked phase 04 to update them "if the bridge's task and callback structure changed shape, which phase 02 makes likely". It did, and phase 02 already wrote it up —AGENTS.mdcarries a "WebRTC conventions" section andARCHITECTURE.md's file tree was corrected by phase 03. Phase 04 verifies rather than writes.
Phase order¶
| Phase | Plan | Status |
|---|---|---|
| 1. Pre-work on 0.17 | PLAN-webrtc-0.20-upgrade-phase-01-prework.md | Complete — baseline captured, 1g agrees within noise |
| 2. Atomic bump to 0.20 | PLAN-webrtc-0.20-upgrade-phase-02-bump.md | Complete — Chromium session on 7e2fb58e confirms the bind address. Its two deferrals were discharged in phase 04: the audio check was performed by ear, and #289/#290 are fixed |
| 3. Socket binding configuration | PLAN-webrtc-0.20-upgrade-phase-03-udp-addrs.md | Complete — --web-media-addr (address or interface name), --web-media-port and --web-ice-server, carried through WebState into a UdpBindPolicy the bridge resolves per offer. Explicit addresses override the loopback default; 0.0.0.0 is refused at startup |
| 4. Soak validation and docs | PLAN-webrtc-0.20-upgrade-phase-04-soak.md | Complete — audio confirmed by ear at last; the bump costs no CPU and slightly less memory, bisected either side of the phase-02 merge; #289/#290 fixed, plus four input bugs the browser check found. Safari unexercised (#310), Firefox still has no video (#311; #289 makes it legible) |
Phase 01 is a hard prerequisite for 02 only in the sense that it makes 02 tractable; 02 could be done standalone at higher risk. Phases 03 and 04 both depend on 02.
Phase 02 is now Complete: phase 04's browser sessions supplied the
verification it was waiting for. What follows is the reasoning it
was left open with, kept because it explains why.
The port
itself has landed on webrtc = "0.20.2" and both Renovate rules
are gone, rtc is now a direct dependency, the standalone rtp
and rustls dependencies are gone, datachannels and remote
tracks became poll loops, and UDP sockets bind enumerated
interface addresses because 0.0.0.0 passes every test and
reaches no browser. That last point is why the phase stays
open: only a real browser session can catch a wrong bind
address, and the test suite cannot stand in for it.
Effort estimate¶
Roughly a week and a half, with a realistic band of five days to two weeks:
| Phase | Estimate |
|---|---|
| 01 — pre-work on 0.17 | 2 days (actual) |
| 02 — atomic bump | 3–5 days |
| 03 — socket binding config | 1 day (revised from ½ by the phase plan; actual) |
| 04 — soak and docs | 1 day |
Phase 01 grew by a day after detailed planning surfaced the four-way client-PC duplication. Phase 02 was expected to come down by the same amount, and in one sense it did — the four-way rewrite is gone — but detailed planning then found three things this plan had not: the datachannel and remote-track surfaces became poll-based rather than moving onto the handler, track construction now requires caller-supplied SSRCs and codec parameters, and the UDP bind address has to be solved in 02 rather than deferred to 03. Net, 02 roughly doubled.
Two things went the other way and are already priced in.
ICE-gathering completion — the item this plan called its riskiest
— was retired by phase 01 and independently confirmed safe in
0.20. And the rustls coupling subtracts work: the pin and eleven
install_default() calls delete outright.
The remaining variance is in phase 02's atomic commit, and it is now concentrated in the media path rather than the signalling path: whether explicit codings reproduce 0.17's negotiated result first time, and whether 0.20's new RTX advertisement changes browser behaviour. Neither can restructure the signalling protocol, which is why the upper bound came in from "add a week" to five days.
Open questions¶
All seven were answered during phase-02 planning, against the
webrtc 0.20.2 and rtc 0.20.2 sources rather than the docs index.
They are kept here with their answers because the answers are what
sized phase 02, and two of them moved work between phases.
-
~~What replaces
gathering_complete_promise()?~~ Answered:on_ice_gathering_state_change, and the ordering is safe.local_description()re-renders from the live ICE agent on every call, candidates are pushed into the core before the completion sentinel, and the sentinel is what queues the event. Non-trickle signalling is correct on 0.20, and getsa=end-of-candidatesfor free. Gathering does not start untilset_local_description(), so our existing call order is required rather than incidental. -
~~Does
RTCDataChannelkeepon_message?~~ Answered: no, and neither does anything else. Datachannel messages and remote-track RTP both became poll-based —DataChannel::poll()andTrackRemote::poll()— so the wiring becomes a spawned loop per channel and per track. This is the answer that most enlarged phase 02, because it means the callback bodies move, which phase 01 had been told they would not. -
~~Does
TrackLocalStaticRTP::write_rtpkeep its signature?~~ Answered: nearly — it takes the packet by value and is aTrackLocaltrait method. There is no fallible variant on tracks;writable/try_sendexist only onDataChannel.write_rtpalready applies back-pressure by awaiting on the driver's bounded event channel. Separately,TrackLocalStaticRTP::newchanged materially and now wants the SSRC and codec from us. -
~~Does
with_udp_addrsaccept0.0.0.0:0?~~ Answered: yes, and that is the trap. It binds happily and emits a literal0.0.0.0host candidate, which browsers discard — while two Rust peers on one host agree about it and connect, so no test we have would fail. Phase 03 does not become a prerequisite of phase 02, but choosing the addresses does move into phase 02, leaving phase 03 to expose the choice as configuration. -
~~Which
rtpmajor does 0.20 pair with?~~ Answered: none — thertpcrate is dead at 0.17.2. RTP moved tortc-rtp, reached asrtc::rtp, so we depend onrtcand the type identity comes for free from webrtc's exact pin. The only source change iscodecs→codec. The renderer's smoke test does not move in lockstep: itsrtpis a dev-dependency with no shared types. It should move eventually, on its own schedule — see Future work. -
~~Does 0.20 make the #215 sibling-skew problem obsolete?~~ Answered: yes, arithmetically.
webrtc0.20.x requiresrtcat an exact patch, andrtcrequires each of its 16 siblings at that same exact patch, so cargo cannot resolve the inconsistent set that broke #215. Both Renovate rules go when the port lands, not just the pin. -
~~Does rustls stay pinned the same way?~~ Answered: it is not pinned at all any more.
rtc-dtls0.20.2 selects its crypto provider from its own cargo features and passes it explicitly rather than reading the process default — upstream hit our exact bug and fixed it properly. Our directrustlsdependency and everyinstall_default()call inshakenfist-spice-webrtcdelete. ryll's own rustls dependency stays: it serves SPICE TLS andaxum-server, andaws-lc-rsstill reaches ryll's graph viareqwest → quinnregardless of webrtc. This also retires the forcing function described below — after 0.20 our rustls version is an ordinary^0.23.35floor, so an advisory is a lockfile bump rather than an emergency port.
Why we are deferring rather than doing it now¶
cargo auditandcargo denyboth pass on 0.17.2 today, so there is no security pressure.- 0.17.x is nonetheless the end of the old line; fixes land on 0.20+ only.
- The forcing function is most likely rustls, not webrtc. The
pin at
shakenfist-spice-webrtc/Cargo.toml:67-72couples our rustls version to webrtc's, so a rustls advisory would demand this port with no notice and no schedule. That is the scenario this plan exists to make survivable. (Phase-02 planning established that the port removes this coupling — see open question 7 — which makes the argument for doing it stronger, not weaker: the exposure persists for exactly as long as we stay on 0.17.) - Secondarily, 0.20's performance work is aimed squarely at our workload, so this may become something we want before it is something we must do.
Administration and logistics¶
Success criteria¶
webrtc = "0.20.2"(or later) in the one manifest that names it, with both webrtc rules removed fromrenovate.json.make testpasses, includingtests/loopback.rsandtests/lifecycle.rs.pre-commit run --all-filespasses.- A real browser reaches a real SPICE guest through
--web, with video, audio, input, and cursor all working, and survives a soak long enough to compare RSS and CPU against the 0.17 baseline. Note this is the only check that can catch a wrong UDP bind address, which is why phase 02 requires a browser session of its own rather than waiting for phase 04's soak. - The answer SDP advertises no candidate with an unspecified address, and at least one candidate.
- The reaper still tears the bridge down when the browser goes
away —
wait_for_deadfires onFailed,DisconnectedandClosed. docs/configuration.mdanddocs/web-frontend.mdcover the UDP bind address;ARCHITECTURE.mdandAGENTS.mdreflect any change to the bridge's task and callback structure.
Future work¶
- Adopt 0.20's send back-pressure (
writable/try_send) inrun_video_pumpto bound peak RSS under a slow consumer. Deliberately out of scope for the port itself. - Evaluate GSO/GRO batching and the configurable SCTP receive window against our own latency measurements.
- Reconsider whether
shakenfist-spice-webrtcshould depend on the sans-iortccore directly rather than the async wrapper. Probably not — we are happy with tokio — but 0.20 makes it a real option for the first time. (Note the port makesrtca direct dependency regardless, becausewebrtcdoes not re-export the types its own API takes.) - Move
shakenfist-spice-renderer/tests/webrtc_h264_smoke.rsoff the abandonedrtp0.17 crate ontortc::rtp. It is a dev-dependency with no type coupling to the webrtc crate, so it does not have to move with the port — but once it has, the test is exercising a payloader we no longer ship. - Ask upstream to re-export
rtc::rtp(or at leastrtp::Packet) fromwebrtc. The crate already hand-re-exports two DTLS enums with a comment explaining that forcing callers to add a version-locked second dependency is bad — and then does exactly that on the primary media write path. - Reconsider
register_default_codecs(). On 0.20 it advertises RTX, HEVC and AV1 that we never send. Deliberately left alone during the port so that negotiation differences stay attributable; worth revisiting once the phase-04 soak has a clean baseline. -
Let
--web-ice-servercarry TURN credentials.RTCIceServerhasusernameandcredentialfields and the bridge already maps a URL string into one, but the flag takes a bare URL, so an authenticated TURN server cannot be configured — only STUN and open TURN work today. Associating a credential pair with a specific URL is a flag-syntax question phase 03 deliberately did not answer in half an hour. Phase 03's plan says twice that this was "recorded in Future work"; phase 04's planning survey found it had not been recorded anywhere at all, which is why it appears here rather than in phase 03's commit. -
Give phase 03's configuration surface an interface allowlist, not just a port pin. Since 0.20 made socket binding the caller's job,
host_udp_bind_addrsbinds and advertises every non-loopback address the host has — including RFC 1918, 169.254/16 and container/veth addresses — so a browser on the public interface learns the host's internal addressing. This is what 0.17 did internally too, so the port did not regress it, but 0.20'sSettingEngine::set_ip_filter/set_interface_filterstill compile while doing nothing, so there is no way to narrow it today. Raised by the automated review of PR #278. Carried into phase 03's plan as--web-media-addr, which takes an interface name as well as an address; phase 02's other deferral, an opt-in for loopback-only hosts, falls out of the same flag.
Bugs fixed during this work¶
Two, both found by the automated review of PR #278 and both introduced by the 0.20 port itself:
- The RTP pumps stamped a hardcoded payload type. 0.20 validates
the payload type against the negotiated codec list rather than
rewriting it, and which type is negotiated depends on what the
browser offered — so Chrome worked and Firefox would have shown
a black screen with nothing above
tracein the log. The pumps now read the resolved value out of the senders' parameters. - The bridge reaper parked forever on a bridge replaced by
POST /offer, becauseclose()on 0.20 does not reliably raise the dead signal the reaper waits on. A viewer reloading the page would strand it for the life of the process. It now also wakes on a bridge-replacement notification.
Both are covered by regression tests that fail on the pre-fix code; see the phase-02 plan's review follow-up.
Related existing issues: #215 (webrtc sibling-crate lockfile skew, the reason for the patch-disable rule) and PR #245 (the Renovate bump this plan defers).
Back brief¶
Before executing any step of this plan, please back brief the operator as to your understanding of the plan and how the work you intend to do aligns with that plan.