diff --git a/Cargo.toml b/Cargo.toml index 9401bfb..aeca7eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ axum-extra = { version = "0.12.6", features = ["typed-header"] } derive_more = { version = "2.1.1", features = ["full"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" -str0m = "0.23.0" +str0m = { version = "0.23.0", features = ["unversioned"] } systemstat = "0.2.6" thiserror = "2.0.18" tokio = { version = "1.50.0", features = ["full"] } diff --git a/README.ko.md b/README.ko.md index 30ca5c1..dd03f62 100644 --- a/README.ko.md +++ b/README.ko.md @@ -6,7 +6,9 @@ Rust와 [str0m](https://github.com/algesten/str0m)으로 만든 라이브 스트리밍용 WebRTC SFU입니다. 송출자는 OBS에서 WHIP으로 방송을 올립니다. 시청자는 대역폭 추정치에 맞는 simulcast 레이어를 받고, 업로드 여유가 있는 시청자는 다른 시청자에게 P2P로 스트림을 중계해 서버 부하를 덜어줍니다. - +![OBS 방송을 재생하면서 simulcast 레이어를 전환하는 시청 페이지](docs/assets/demo.webp) + +OBS 방송을 재생하는 시청 페이지입니다. 자동 모드로 시작한 뒤, 시청자가 가장 낮은 simulcast 레이어로 내렸다가 가장 높은 레이어로 되돌립니다. ## 측정 결과 diff --git a/README.md b/README.md index ecd1fce..e39e165 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ English | [한국어](README.ko.md) A WebRTC SFU for live streaming, written in Rust on [str0m](https://github.com/algesten/str0m). Publishers push over WHIP from OBS. Viewers get the simulcast layer their bandwidth estimate allows, and viewers with upload to spare relay the stream to each other over a peer link, taking load off the server. - +![The watch page playing an OBS broadcast and switching simulcast layers](docs/assets/demo.webp) + +The watch page playing an OBS broadcast. It starts in automatic mode, then the viewer switches to the lowest simulcast layer and back to the highest. ## Measured diff --git a/docs/assets/demo.webp b/docs/assets/demo.webp new file mode 100644 index 0000000..439554a Binary files /dev/null and b/docs/assets/demo.webp differ diff --git a/src/sfu/streamer.rs b/src/sfu/streamer.rs index 90b83d2..9477bd4 100644 --- a/src/sfu/streamer.rs +++ b/src/sfu/streamer.rs @@ -8,7 +8,9 @@ use std::{ use str0m::{ Event, IceConnectionState, Rtc, + format::Codec, media::{KeyframeRequest, MediaKind, Mid, Rid}, + unversioned::{H264Packetizer, Packetizer}, }; use uuid::Uuid; @@ -21,6 +23,8 @@ use crate::{ }, }; +const MAX_PACKET_SIZE: usize = 1200; + impl Streamer { pub fn new(rtc: Rtc, session_id: Uuid) -> Streamer { Streamer { @@ -61,10 +65,24 @@ impl Streamer { .iter() .find(|l| l.rid == rid) { + let frame_size = match data.params.spec().codec { + Codec::H264 => { + let mut packetizer = H264Packetizer::default(); + if let Ok(packet) = + packetizer.packetize(MAX_PACKET_SIZE, &data.data) + { + packet.iter().map(|v| v.len()).sum() + } else { + data.data.len() + } + } + _ => data.data.len(), + }; + let push_outcome = simulcast_layer .bitrate_estimator .borrow_mut() - .push(data.data.len(), Instant::now()); + .push(frame_size, Instant::now()); if matches!(push_outcome, PushOutcome::EstimateBecameAvailable) { *should_reevaluate = true; diff --git a/tests/viewer.rs b/tests/viewer.rs index e121a08..967530b 100644 --- a/tests/viewer.rs +++ b/tests/viewer.rs @@ -182,6 +182,16 @@ async fn the_server_measures_every_layer_it_receives() { high > low * 4, "the gap matches what was sent, 300 against 2500 kbps: {estimates:?}" ); + + // Ranking alone would also hold for a count of packets or frames. The + // figure has to be the bitrate itself, since layers are picked by it. + for (bps, sent_kbps) in measured.iter().zip([300u64, 800, 2500]) { + let sent = sent_kbps * 1000; + assert!( + (sent * 3 / 4..=sent * 5 / 4).contains(bps), + "a layer sent at {sent_kbps} kbps is measured near it: {estimates:?}" + ); + } } /// Auto mode filters on two numbers the SDP cannot supply: a bitrate estimate