the hardcoded 400kb/s was a stability compromise and the previous knob
just moved the guesswork to operators: they had to hand-measure each
sfu's policer knee by ramping until stalls. replace both with a
delay-based aimd controller that finds the knee at runtime.
the sfu policer queues before it drops, so kcp's smoothed rtt inflates
ahead of the throughput collapse from #95/#107. the pacer folds one
srtt sample per writer tick: probe the rate up additively while delay
sits near the path baseline, back off multiplicatively once it
inflates. it settles just under the per-sfu knee on its own, both on
the client->server writerLoop and each server->client peer pump.
vp8.max_bytes_per_sec is now just the probe ceiling (default 1mb/s);
a fresh session still starts from the old 400kb/s operating point so a
healthy path only ramps up. no per-service hand-tuning left.
refs #107
the pacer was pinned at a hardcoded 400kb/s stability compromise, so
operators could not reach their sfu's real throughput ceiling without
recompiling. the options.maxbytespersec knob already existed but was
never wired from yaml. plumb it through config -> session -> transport,
keeping 400kb/s as the conservative default.
refs #107
detect a server restart when a frame from a new epoch arrives after the
latched peer has gone silent past a grace window, then drive the full
carrier rebuild (stream.Reconnect) instead of a bare re-handshake over
the stale carrier. the restarted server rejoins the sfu as a fresh
participant, so re-handshaking on the old media path only times out;
rebuilding the carrier re-establishes a path the new server answers on
and recovers in seconds instead of waiting out the ~70s liveness window
A restarted server rejoins the SFU with a new vp8channel epoch. The
client stayed latched to the old epoch and dropped every frame from the
new one, so recovery only happened once the relaxed control-liveness
window expired (~70s).
Watch for a frame from a different epoch after the latched peer has been
silent past a short grace window and fire the carrier reconnect callback,
driving a re-handshake against the new epoch in seconds. A live peer keeps
the latch fresh via its ~2s keepalives, so the watchdog never trips under
load or during SFU renegotiation.
- swap readme.md to english as default entry point
- move russian readme to readme.ru.md
- use RU / EN language switch buttons
- drop ffmpeg remark for videochannel
WaitForPeer makes the client block before it sends its SYN, so the
server cannot yet know the client epoch and its first welcome arrives as
a broadcast (receiverEpoch=0). The same happens on every reconnect:
peerEpoch is reset to 0 and the peer re-announces via a broadcast
Send(nil). The strict requireTargetedPeer guard dropped those broadcasts
while unlatched, so peerEpoch never latched and WaitForPeer hung for its
whole timeout - the link connected and pinged but no data ever flowed
("ping works, no connection"), most visibly when a peer left and
reconnected. Accept a broadcast while unlatched (peerEpoch==0) and keep
rejecting third-party broadcasts once latched.
peerWriterPump emitted every KCP frame the instant it was queued. With
KCP congestion control off (nc=1) and a BDP-sized window that overran the
SFU policer on the server->client direction. Pace it on the same frame
ticker and per-tick byte budget as writerLoop, sharing batchSample via
batchSampleFrom.
Server data-plane peer sessions used the conservative 30s smux keepalive
while the client used the relaxed window (SmuxConfigFor) for ControlPlane
transports like vp8channel. When the carrier went legitimately silent for
tens of seconds (publisher-PC reconnect / SFU renegotiation), the server
tore down its peer data session first at 30s, surfacing as 'closed pipe'
on the client and triggering a reconnect storm. Mirror the client and
apply SmuxConfigFor on the server data plane too.
pion TrackLocalStaticSample.WriteSample packetizes under its lock but
emits RTP after releasing it, so concurrent callers interleave sequence
numbers on the wire. The server runs a per-peer writer pump for bulk data
alongside writerLoop for control/keepalive, both hitting the shared track;
the remote VP8 reassembler enforces strict sequence contiguity and drops
the interleaved frames, stalling server->client traffic. Funnel every
WriteSample through one mutex so each frame's packetize+emit is atomic.
- bound WaitForPeer in bringUpLink/tryReopenSession to handshake timeout
so a missing peer fails fast instead of hanging before the SOCKS listener
- call waitForPeer on the reconnect path too: resetLinkPeer clears the peer
epoch, so without it the post-reconnect SYN re-races the server bridge
- restore strict requireTargetedPeer guard: the server always replies
targeted (it latches the client epoch from the SYN before smux replies),
so untargeted broadcasts before latch must be dropped
- remove per-frame debug logging on the bridge send/recv hot path
- extract buildSmuxClient / establishPeerSession helpers and wrap interface
errors to satisfy cyclop/nestif/wrapcheck
add a silent dead-link mode to the memory carrier (link stays up,
frames vanish) and a flag-gated TestDeadLinkDetectionWindow that
measures how long the datachannel client takes to notice a dead link
and reconnect. on the conservative liveness window it reconnects in
~58s; with the buggy 120s smux keepalive it fails to detect within 75s.
also drop dead restartControlKCP (both callers moved to
restartControlKCPWithHeader)
The #95 telemost fix widened three global windows (control pong 15s->45s,
smux keepalive 30s->120s, tunnel ack 15s->90s). These live in shared
packages, so they also slowed dead-link detection on jitsi/datachannel:
a genuinely dead session stayed alive up to 120s before reconnecting,
which presented as the link going down and not recovering.
Restore the conservative defaults globally and apply the relaxed windows
only when the transport implements transport.ControlPlane (vp8channel/
goolom), where KCP batching + SFU publisher-PC renegotiation legitimately
go silent for ~25-30s. Conventional carriers reconnect promptly again.
Without this, the client could send a smux SYN frame before the server
had opened its bridge side. JVB would drop the frame and smux would not
retransmit, causing a connection timeout.
Mirrors the same fix already applied to transport/datachannel.
Client was racing ahead of the server by ~2s: it opened the smux SYN
right after bridge-ready, but JVB silently dropped those frames because
the server hadn't opened its datachannel yet. smux does not retransmit
SYN, so AcceptStream on the server side never returned and the handshake
timed out.
Add WaitForPeer (engine.PeerReadySession / transport.PeerReadyTransport)
that polls peerEpoch until the first epoch broadcast from the remote
side is received, then proceed to smux + handshake.
In peer-routing mode installControlSession() returns early when the
transport does not implement transport.ControlPlane (e.g. datachannel),
leaving acceptHandshake() never called and s.sessionID never set.
servePeer() then spin-waits forever for a sessionID that never arrives,
so the client never receives the SERVER_WELCOME frame.
Mirror the legacy path from waitHandshake/serveSingle: if controlConn
is nil, drive acceptHandshake directly on the per-peer smux session
before entering the AcceptStream loop.
xmppKeepalive was using fire-and-forget Send(); on a half-open TCP
connection pings were sent but the missing reply went undetected,
leaving the session stuck. Switch to SendIQWait so a timeout triggers
a reconnect.
In peer-routing mode the server may send the initial welcome frame as a
broadcast (receiverEpoch=0) before it has learned the client's
localEpoch. The previous requireTargetedPeer guard dropped every
broadcast unconditionally. Tighten the guard: only drop frames whose
senderEpoch is unknown or belongs to a different peer; frames from the
already-latched peer are always accepted.
Control KCP epoch must stay stable across both carrier reconnects and
liveness resets to avoid breaking ping/pong routing. Previously:
- SetReconnectCallback rotated control epoch (fix: preserve)
- ResetPeer also rotated control epoch (fix: preserve)
Both now snapshot the control epoch header before data epoch rotation
and use restartControlKCPWithHeader() with the preserved epoch.
This prevents control frames from using a mismatched epoch after
liveness timeout or carrier reconnect, which broke routing and caused
reconnect loops.
inject a clean VP8 keyframe every ~2s during bulk so the SFU keeps
forwarding the track instead of stalling after its decode timeout.
add RTCP interceptors and IPv4-only ICE, priority control/data mux,
publisher PC reconnect with stable control epoch, and longer smux
keepalive/liveness windows to survive the reconnect gap
Video-paced transports (vp8channel, videochannel, seichannel) need
longer operation timeouts due to frame pacing and KCP batching:
- streamPatternForDuration: 15s -> 60s for video transports
- sustainedEcho: 5s -> 60s for video transports
Part of fix for issue #95.
Related to #95