ActiveAudioSsrcs was a test-SFU-only colibri message. With the test
SFU no longer sending it, the handler is dead code. Removing now
before introducing the new discovery path so we don't briefly have
two paths inserting into _remoteSsrcs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The test-only ActiveAudioSsrcs colibri message is being replaced by
ReferenceImpl's per-receiver FrameTransformer-based discovery (next
commits). Removing the message first forces every subsequent test
run to exercise the new code path — keeping the message in place
would let test passes mask production-real bugs.
T1/T2 currently FAIL as expected; T3-T5 likewise — the muted-peer
invariant cannot hold without working discovery. All restored by the
end of this PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace GroupInstanceReferenceImpl's hardcoded 0.1 synthetic
audio level (reported for every known SSRC, regardless of whether
the participant was actually producing audio) with a real
peak-amplitude reading per remote receiver.
Implementation:
- New GRAudioLevelSink (webrtc::AudioTrackSinkInterface)
attached to each remote audio track after onRenegotiationComplete
via the new wireRemoteAudioLevelSinks(). Tracks per-window peak
amplitude in PCM, normalized to RFC 6464's "0 dBov = full-scale
sine wave" reference (peak / (32768/sqrt(2))) so the value is
comparable to CustomImpl's pow(10, -dbov/20) reading.
- pollAudioLevels() now reads consumeLevel() from each sink and
skips entries whose sink has produced no samples since the last
poll, eliminating phantom level entries for known-but-silent
SSRCs.
- buildRemoteAnswer() now includes the remote SSRC on each
recvonly audio m-line. Without a signaled SSRC the
AudioRtpReceiver calls SetDefaultRawAudioSink and only one of N
transceivers' RemoteAudioSource ever receives PCM (the rest stay
silent and our sinks never fire). MID is excluded from audio
m-lines, so BUNDLE demuxes by SSRC.
- Result for a 3000-amplitude sine through Opus: ~0.126, matching
CustomImpl's reading.
Test infrastructure (CLI):
- New --mute-participants <ids> flag (comma-separated participant
IDs) keeps a participant muted after join.
- ParticipantState gains a per-source-SSRC max-level map.
- validateGroupState enforces: no peer may report a muted
participant's SSRC at level >= 0.05. The relaxed audio-received
check applies only to unmuted participants.
This is a pre-requisite for the upcoming SSRC-discovery work
(per-receiver level sinks must already exist before
onRenegotiationComplete starts wiring them up for tap-discovered
SSRCs).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings the testbench source into the submodule:
- tools/cli/ — C++ CLI test tool (P2P, reflector, group, group-churn modes)
- tools/go_sfu/ — Go/Pion SFU library, c-archive linked into tgcalls_cli
- Dockerfile — multi-stage Linux container build
- CLAUDE.md (top-level), tools/cli/CLAUDE.md, tools/go_sfu/CLAUDE.md — docs
Bazel build glue (.bazelrc, MODULE.bazel, third-party BUILD edits, tgcalls_core
target) remains in the outer repo since the dependency stack lives there;
labels and paths in this repo reference the outer-repo workspace root.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Enable H264 simulcast video in group calls for both GroupInstanceCustomImpl
and GroupInstanceReferenceImpl, with reactive channel setup mirroring the
real Telegram app's flow.
Infrastructure:
- DiscardPacketsWithUnknownSsrc field trial prevents outgoing video channel
from stealing incoming RTP for unregistered SSRCs
- dataChannelMessageReceived callback on GroupInstanceDescriptor forwards
Colibri messages (ActiveVideoSsrcs) to the application layer
ReferenceImpl video implementation:
- Pre-allocate 6 video SSRCs (3 layers x primary + RTX) at construction
- SDP munging: replace PeerConnection's auto-generated StreamParams with
pre-allocated SSRCs + SIM + FID groups before SetLocalDescription
- Video source activated via sender()->SetTrack() (no renegotiation)
- Incoming video: recvonly transceivers with explicit remote SSRCs in answer
(required because DiscardPacketsWithUnknownSsrc is process-wide)
- Video sinks wired explicitly after SetRemoteDescription (OnTrack doesn't
fire for locally-created recvonly transceivers)
- Colibri ReceiverVideoConstraints sent over data channel to control SFU
forwarding; SFU responds with SenderVideoConstraints + proactive PLI
Squashed from:
- feat: enable DiscardPacketsWithUnknownSsrc field trial for video group calls
- feat: add dataChannelMessageReceived callback to GroupInstanceDescriptor
- feat(group-ref): store video configuration from descriptor
- feat(group-ref): pre-allocate video SSRCs and include in join payload
- feat(group-ref): add video encoder/decoder factories to PeerConnectionFactory
- feat(group-ref): implement setVideoSource with SDP munging for simulcast SSRCs
- feat(group-ref): extend buildRemoteAnswer with video m-line construction
- feat(group-ref): forward data channel messages to app for video signaling
- feat(group-ref): implement setRequestedVideoChannels with Colibri constraints
- feat(group-ref): implement addIncomingVideoOutput with video sink wiring
- fix(group-ref): activate outgoing video after join response is applied
- feat(group-ref): add video support to GroupInstanceReferenceImpl
Alternative group call implementation using standard WebRTC PeerConnection
instead of the manual ICE/DTLS/SRTP management in GroupInstanceCustomImpl.
Implements the same GroupInstanceInterface (~1500 lines vs ~4700).
Uses a single PeerConnection to the SFU with:
- sendrecv audio transceiver (outgoing audio)
- recvonly audio transceivers added dynamically per remote SSRC
- data channel for Colibri protocol (ActiveAudioSsrcs discovery)
- Programmatic SDP construction via cricket::SessionDescription API
Key implementation details:
- Loopback ICE enabled (network_ignore_mask=0) for localhost SFU
- RTP header extensions copied from local offer per m-line (BUNDLE-safe)
- MID extension excluded from remote answer to prevent wrong-channel routing
- Renegotiation mirrors local offer mids exactly in constructed answer
- Synthetic audio levels (0.1) for known remote SSRCs
- Serialized renegotiation (one offer/answer at a time)
Squashed from:
- feat: add GroupInstanceReferenceImpl — PeerConnection-based group call client
- fix: add missing jsep_session_description.h include
- fix: enable loopback ICE and add remote candidates
- fix: add RTP header extensions to SDP answer
- fix: rewrite buildRemoteAnswer to mirror local offer mids for renegotiation
- fix: simplify audio level polling to use known remote SSRCs directly
New implementation that uses WebRTC PeerConnection internally but speaks
V2Impl's signaling protocol (InitialSetupMessage, NegotiateChannelsMessage,
CandidatesMessage). Enables bidirectional calls between PeerConnection-based
clients and V2Impl clients (versions 7.0.0-13.0.0).
Architecture: PeerConnection <-> SignalingTranslator <-> EncryptedConnection
<-> SignalingSctpConnection
Key components:
- SignalingTranslator: converts between cricket::SessionDescription and V2Impl
signaling messages using JsepSessionDescription programmatic API (no SDP
string round-trips)
- Shared conversion functions extracted to Signaling.h/.cpp for use by both
ContentNegotiationContext (V2Impl) and SignalingTranslator (CompatImpl)
- Data channel works for CompatImpl<->CompatImpl calls; padded as rejected
when paired with V2Impl (which has no PeerConnection)
100% success rate at 30% loss in both call directions.
Squashed from:
- refactor: extract signaling content conversion functions to Signaling.h/.cpp
- feat: add SignalingTranslator for V2Impl signaling <-> PeerConnection conversion
- feat: add InstanceV2CompatImpl — PeerConnection with V2Impl signaling (version 14.0.0)
- feat: add data channel support to InstanceV2CompatImpl
writeStateLogRecords() captured a raw Call* pointer on the media thread
and posted it to the worker thread. If stop() called
_peerConnection->Close() (which destroys Call) between the post and
worker thread execution, the worker thread would dereference a dangling
pointer. WebRTC's call_ptr_ is Call* const and never nulled after Close(),
so the existing null check didn't catch this.
Fix: add _isStopped atomic flag, set before Close() in stop(), checked
in the worker thread lambda before accessing call.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add writable gate for role-based SCTP handshake ordering: caller
(isOutgoing=true) starts writable and sends INIT immediately; callee
starts not-writable and defers Connect() until first received packet
triggers setWritable(true).
Implement CustomDcSctpSocket to fix WebRTC's missing timer backoff cap
on t1_init and t1_cookie handshake timers. Without this, simultaneous-open
under packet loss causes 20+ second stalls due to unlimited exponential
backoff (1s, 2s, 4s, 8s...). With the fix: 400ms init, 750ms max backoff,
yielding ~18 attempts in 15s and 100% success at 30% loss.
Timer values are configurable via JSON custom parameters:
- network_sctp_t1_init_ms, network_sctp_t1_cookie_ms, network_sctp_max_backoff_ms
Squashed from:
- feat: add SCTP writable gate for role-based handshake ordering
- chore: whitespace cleanup in NativeNetworkingImpl
- fix: CustomDcSctpSocket with t1 timer backoff cap for signaling SCTP