Media negotiation
The gap between “WebRTC connects” and “WebRTC connects at 4K H.265 stereo” is almost entirely SDP work.
Codec preference
AppSettings.videoCodec is one of AV1, H265, H264 - there is no “Auto”.
Auto used to let the SDK’s native order decide (and pushed HEVC to the back), but
the outcome varied by device and build and made calls unpredictable, so it was
removed. The default is H.264: universal hardware support, and the only codec
a browser peer can receive.
WebRtcClient.setPreferredCodec(name) stores an uppercase codec name (default
"H264") and calls applyCodecPreference():
private fun applyCodecPreference() {
val transceiver = pc.transceivers.first { it.mediaType == MEDIA_TYPE_VIDEO }
val caps = pcf.getRtpSenderCapabilities(MEDIA_TYPE_VIDEO)
val usable = caps.codecs.filterNot { it.name.uppercase() in setOf("VP8", "VP9") }
// chosen codec first, then the rest by efficiency as a fallback
val ordered = usable.sortedByDescending { c ->
val n = c.name.uppercase()
when {
n == pref -> 100
n in codecRank -> 50 - codecRank.indexOf(n) // AV1 > H265 > H264
else -> -1
}
}
transceiver.setCodecPreferences(ordered)
}- VP8 / VP9 are always stripped, both directions. H.264 is the universal fallback and covers everything VPx would, so the app doesn’t carry the older codecs.
setCodecPreferencesonly affects offer ordering - the peer still has to support the codec for it to be selected.- It runs again after any renegotiation / ICE restart.
raiseH265Level - making 4K HEVC legal
webrtc-sdk M144 advertises H.265 with level-id=93 in its a=fmtp line. That
is HEVC Level 3.1 - roughly a 720p@30 budget. A strict hardware decoder
(MediaTek Codec2, Tensor) will reject any frame that exceeds the negotiated
level, so a 4K stream negotiated at Level 3.1 simply never decodes.
The fix rewrites every H.265 fmtp line before setLocalDescription:
a=fmtp:<pt> level-id=93 → a=fmtp:<pt> level-id=153level-id=153 is Level 5.1 - 4K@60, Main tier. If a payload type has an
rtpmap but no fmtp, one is synthesised (a=fmtp:<pt> level-id=153). Both
peers run Viora, so both sides raise the ceiling and the negotiation lands at
5.1.
private fun raiseH265Level(sdp: String): String { /* per-m-section fmtp rewrite */ }Failures are swallowed - if the rewrite throws, the original SDP is used.
tuneOpus - stereo, FEC, no DTX
The default Opus offer is mono, ~40 kbps, DTX on. tuneOpus rewrites the Opus
a=fmtp line with:
| Param | Value | Why |
|---|---|---|
stereo / sprop-stereo | 1 | Full stereo capture and playback (both directions declared) |
maxaveragebitrate | 262144 (256 kbps) | 128 kbps/channel VBR ceiling - headroom for shared media audio and FEC |
useinbandfec | 1 | In-band forward error correction - lost packets are partially reconstructed from the next one |
usedtx | 0 | DTX (comfort-noise on silence) off - it interacts badly with the loudness probe and music |
The regex is careful to match stereo=1 at a param boundary and not the
stereo=1 inside sprop-stereo=1.
The SDP munge is paired with an RtpSender parameter change
(applyAudioBitrate() sets encoding.maxBitrateBps = 262144) - the SDP sets the
codec ceiling, the sender parameters set the transport ceiling.
Bitrate seeding for strict HEVC encoders
Some hardware HEVC encoders (again MediaTek Codec2, Tensor) honour the initial bandwidth estimate very literally. WebRTC’s BWE starts low and ramps, so a 4K HEVC stream can spend its first seconds cranked to QP ~37 (a smeary mess) or starved to ~0.2 Mbps before the estimate climbs.
WebRtcClient seeds the estimate with PeerConnection.setBitrate(min, start, max) right after the tracks are added, so the encoder starts near its target
instead of ramping from the floor.
withRaisedLevel
Everything is composed in one call applied to both offer and answer:
private fun withRaisedLevel(desc: SessionDescription) =
SessionDescription(desc.type, tuneOpus(raiseH265Level(desc.description)))Call sites:
createOffer → onCreateSuccess { d -> pc.setLocalDescription(obs, withRaisedLevel(d)) }
createAnswer → onCreateSuccess { d -> pc.setLocalDescription(obs, withRaisedLevel(d)) }Video quality → capture format
AppSettings.VideoQuality maps to a concrete capture request, not a label:
| Preset | Capture | Encoder cap |
|---|---|---|
| Auto | 960×540@30, WebRTC adapts | uncapped (BWE-driven) |
| Data Saver | 640×480@30 | 900 kbps |
| HD | 1280×720@30 | 2,500 kbps |
| Full HD | 1920×1080@30 | 6,000 kbps |
| 2K | 2560×1440@30 | 12,000 kbps |
| 4K | 3840×2160@30 | 20,000 kbps |
An explicit preset locks the resolution and lets FPS give under load; Auto
keeps WebRTC’s BALANCED degradation (drop resolution, hold FPS). See
video capture.