feat(portal): BWE-driven resolution adaptation + duplicate frame skipping
WebRTC client bandwidth estimate now drives both encoder bitrate and resolution tier selection, replacing the previous static-target encoder. - webrtc.rs: enable str0m BWE (seeded at 5 Mbps), surface EgressBitrateEstimate + KeyframeRequest events, expose get_bwe_estimate() / set_need_keyframe() - state_portal.rs: wire bitrate/resolution channels between the WebRTC thread and the encode thread; tier ladder [1440p, 1080p, 720p] with downscale at 60% budget and upscale hysteresis (120% sustained 10s) - avhw.rs: SwEncImport::poll_resolution_commands() rebuilds the import filter graph on UpdateResolution; SwEncEncode::recreate_encoder() rebuilds sws/enc_video/yuv_frame atomically; hash_sampled_y_plane() skips duplicate frames; VBV x264opts cap IDR bursts; H.264 level 4.0 (muxer) / 4.2 (WebRTC) - state.rs: sync wlr-screencopy GOP to fps*2 max 20 for parity - fix: drain bitrate_rx + resolution_rx BEFORE the stride check in encode_cpu_frame() so the new (smaller-stride) frame produced after a resolution change does not hit the stale (larger) enc_width and crash the encode thread - WebRTC GOP widened to fps*2 max 20 (was fps/2 max 10)
This commit is contained in:
+85
-29
@@ -4,6 +4,7 @@ use std::net::{SocketAddr, TcpListener, UdpSocket};
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use str0m::bwe::{Bitrate, BweKind};
|
||||
use str0m::change::SdpOffer;
|
||||
use str0m::format::Codec;
|
||||
use str0m::media::{Frequency, MediaKind, MediaTime, Mid, Pt};
|
||||
@@ -199,6 +200,7 @@ struct WebRtcInner {
|
||||
video_pt: Option<Pt>,
|
||||
connected: bool,
|
||||
need_keyframe: bool,
|
||||
current_bwe_estimate: Option<Bitrate>,
|
||||
rtp_clock: u32,
|
||||
buf: Vec<u8>,
|
||||
}
|
||||
@@ -260,11 +262,10 @@ impl WebRtcState {
|
||||
continue;
|
||||
}
|
||||
|
||||
match WebRtcInner::new(self.fps)
|
||||
.and_then(|mut new_inner| {
|
||||
let answer_json = new_inner.handle_sdp_offer(body.as_bytes())?;
|
||||
Ok((new_inner, answer_json))
|
||||
}) {
|
||||
match WebRtcInner::new(self.fps).and_then(|mut new_inner| {
|
||||
let answer_json = new_inner.handle_sdp_offer(body.as_bytes())?;
|
||||
Ok((new_inner, answer_json))
|
||||
}) {
|
||||
Ok((new_inner, answer_json)) => {
|
||||
let replacing = self.inner.is_some();
|
||||
self.inner = Some(new_inner);
|
||||
@@ -285,7 +286,8 @@ impl WebRtcState {
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("SDP offer handling failed: {e}");
|
||||
let resp = "HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n";
|
||||
let resp =
|
||||
"HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n";
|
||||
if let Err(e) = stream.write_all(resp.as_bytes()) {
|
||||
tracing::debug!("HTTP write error: {e}");
|
||||
}
|
||||
@@ -340,12 +342,27 @@ impl WebRtcState {
|
||||
pub fn is_connected(&self) -> bool {
|
||||
self.inner.as_ref().is_some_and(WebRtcInner::is_connected)
|
||||
}
|
||||
|
||||
/// Returns the latest bandwidth estimation estimate in bits per second, if available.
|
||||
pub fn get_bwe_estimate(&self) -> Option<u64> {
|
||||
self.inner
|
||||
.as_ref()
|
||||
.and_then(|inner| inner.current_bwe_estimate.map(|b| b.as_u64()))
|
||||
}
|
||||
|
||||
pub fn set_need_keyframe(&mut self) {
|
||||
if let Some(inner) = self.inner.as_mut() {
|
||||
inner.need_keyframe = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WebRtcInner {
|
||||
fn new(fps: u32) -> Result<Self> {
|
||||
let _ = fps;
|
||||
let mut rtc = RtcConfig::new().build(Instant::now());
|
||||
let mut rtc = RtcConfig::new()
|
||||
.enable_bwe(Some(Bitrate::mbps(5)))
|
||||
.build(Instant::now());
|
||||
|
||||
let socket = UdpSocket::bind("0.0.0.0:0")?;
|
||||
socket.set_nonblocking(true)?;
|
||||
@@ -368,10 +385,14 @@ impl WebRtcInner {
|
||||
std::mem::size_of::<libc::c_int>() as libc::socklen_t,
|
||||
);
|
||||
if ret < 0 {
|
||||
tracing::warn!("setsockopt SO_SNDBUF failed (errno {})", std::io::Error::last_os_error());
|
||||
tracing::warn!(
|
||||
"setsockopt SO_SNDBUF failed (errno {})",
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
}
|
||||
let mut actual: libc::c_int = 0;
|
||||
let mut actual_len: libc::socklen_t = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
|
||||
let mut actual_len: libc::socklen_t =
|
||||
std::mem::size_of::<libc::c_int>() as libc::socklen_t;
|
||||
let gret = libc::getsockopt(
|
||||
fd,
|
||||
libc::SOL_SOCKET,
|
||||
@@ -408,14 +429,15 @@ impl WebRtcInner {
|
||||
video_pt: None,
|
||||
connected: false,
|
||||
need_keyframe: false,
|
||||
current_bwe_estimate: None,
|
||||
rtp_clock: 0,
|
||||
buf: vec![0u8; 65535],
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_sdp_offer(&mut self, body: &[u8]) -> Result<String> {
|
||||
let offer: SdpOffer = serde_json::from_slice(body)
|
||||
.map_err(|e| anyhow::anyhow!("parse SDP offer: {e}"))?;
|
||||
let offer: SdpOffer =
|
||||
serde_json::from_slice(body).map_err(|e| anyhow::anyhow!("parse SDP offer: {e}"))?;
|
||||
|
||||
let answer = self
|
||||
.rtc
|
||||
@@ -492,9 +514,7 @@ impl WebRtcInner {
|
||||
tracing::info!("Media added: mid={} kind={:?}", ma.mid, ma.kind);
|
||||
if ma.kind == MediaKind::Video {
|
||||
if let Some(media) = self.rtc.media(ma.mid) {
|
||||
if media.direction().is_sending()
|
||||
&& self.video_mid.is_none()
|
||||
{
|
||||
if media.direction().is_sending() && self.video_mid.is_none() {
|
||||
self.video_mid = Some(ma.mid);
|
||||
tracing::info!("Captured video mid: {}", ma.mid);
|
||||
self.discover_video_params();
|
||||
@@ -502,6 +522,22 @@ impl WebRtcInner {
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::KeyframeRequest(_) => {
|
||||
tracing::info!("received keyframe request from viewer");
|
||||
self.need_keyframe = true;
|
||||
}
|
||||
Event::EgressBitrateEstimate(est) => {
|
||||
let bitrate = match est {
|
||||
BweKind::Twcc(b) => *b,
|
||||
BweKind::Remb(_, b) => *b,
|
||||
_ => {
|
||||
tracing::debug!("BWE estimate: unrecognized kind, skipping");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
tracing::info!("BWE estimate: {bitrate}");
|
||||
self.current_bwe_estimate = Some(bitrate);
|
||||
}
|
||||
_ => {
|
||||
tracing::debug!("WebRTC event: {:?}", e);
|
||||
}
|
||||
@@ -538,9 +574,9 @@ impl WebRtcInner {
|
||||
.map_err(|e| anyhow::anyhow!("receive contents: {e}"))?,
|
||||
},
|
||||
);
|
||||
self.rtc
|
||||
.handle_input(input)
|
||||
.map_err(|e| anyhow::anyhow!("handle_input({n} bytes from {source}): {e}"))?;
|
||||
self.rtc.handle_input(input).map_err(|e| {
|
||||
anyhow::anyhow!("handle_input({n} bytes from {source}): {e}")
|
||||
})?;
|
||||
}
|
||||
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
|
||||
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
|
||||
@@ -636,18 +672,16 @@ fn extract_body(req: &str) -> &str {
|
||||
}
|
||||
|
||||
fn local_ip() -> Option<String> {
|
||||
std::net::UdpSocket::bind("0.0.0.0:0")
|
||||
.ok()
|
||||
.and_then(|s| {
|
||||
s.connect("1.1.1.1:80").ok()?;
|
||||
let addr = s.local_addr().ok()?;
|
||||
drop(s);
|
||||
let ip = addr.ip().to_string();
|
||||
if ip == "0.0.0.0" || ip.starts_with("127.") {
|
||||
return None;
|
||||
}
|
||||
Some(ip)
|
||||
})
|
||||
std::net::UdpSocket::bind("0.0.0.0:0").ok().and_then(|s| {
|
||||
s.connect("1.1.1.1:80").ok()?;
|
||||
let addr = s.local_addr().ok()?;
|
||||
drop(s);
|
||||
let ip = addr.ip().to_string();
|
||||
if ip == "0.0.0.0" || ip.starts_with("127.") {
|
||||
return None;
|
||||
}
|
||||
Some(ip)
|
||||
})
|
||||
}
|
||||
|
||||
fn is_idr_nalu(data: &[u8]) -> bool {
|
||||
@@ -746,4 +780,26 @@ mod tests {
|
||||
fn all_zeros() {
|
||||
assert!(!is_idr_nalu(&[0, 0, 0, 0, 0, 0, 0, 0]));
|
||||
}
|
||||
|
||||
// ── Task 5: BWE estimate handling ──
|
||||
|
||||
#[test]
|
||||
fn bitrate_mbps_conversion() {
|
||||
let b = Bitrate::mbps(5);
|
||||
assert!(b.as_u64() > 0, "5 Mbps should be > 0 bps");
|
||||
assert_eq!(b.as_u64(), 5_000_000, "5 Mbps should be 5,000,000 bps");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bitrate_bps_conversion() {
|
||||
let b = Bitrate::bps(1_234_567);
|
||||
assert_eq!(b.as_u64(), 1_234_567);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_bwe_is_5mbps() {
|
||||
let default = Bitrate::mbps(5);
|
||||
let bps = default.as_u64();
|
||||
assert_eq!(bps, 5_000_000);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user