feat: add WebRTC streaming via str0m + portal session persistence

- Add src/webrtc.rs: HTTP signaling server + str0m Sans-IO WebRTC transport
  with H.264 Annex-B → RTP packetization and key-frame request handling
- avhw: introduce FrameOutput enum (Muxer | Channel) so SwEncState can
  output to either MP4 muxer or crossbeam channel for WebRTC
- cap_portal: support portal session restore tokens (PersistMode::ExplicitlyRevoked)
  to skip re-authorization dialog; add --no-persist flag to force fresh dialog
- args: make --output optional when --port is used for WebRTC mode
- state_portal: integrate WebRTC pipeline (encoder channel → RTP forwarding)
  with shorter GOP for WebRTC (fps/2, min 10)
- main: redirect tracing to stderr; validate --output or --port required
- Add dependencies: str0m 0.20, serde_json 1, dirs 6
This commit is contained in:
dailz
2026-06-04 20:54:16 +08:00
parent 74f4dc826d
commit b0ed6548a6
10 changed files with 1611 additions and 70 deletions
+78 -12
View File
@@ -8,6 +8,7 @@ use anyhow::{bail, Result};
use crate::args::Args;
use crate::avhw::{self, SwEncState};
use crate::cap_portal::{CapPortal, PwCtrlEvent, PwDmaBufFrame};
use crate::webrtc::WebRtcState;
/// 门户采集的阶段状态
/// - WaitingForFormat: 等待接收到第一帧 DMA-BUF 以确定视频格式参数
@@ -32,6 +33,10 @@ pub struct StatePortal {
start_time: Option<Instant>,
last_stats_time: Option<Instant>,
last_stats_frames: u64,
webrtc: Option<WebRtcState>,
webrtc_tx: Option<crossbeam_channel::Sender<Vec<u8>>>,
webrtc_rx: Option<crossbeam_channel::Receiver<Vec<u8>>>,
webrtc_frames_sent: u64,
}
impl StatePortal {
@@ -48,6 +53,14 @@ impl StatePortal {
let cap = CapPortal::new(&args)?;
let (webrtc, webrtc_tx, webrtc_rx) = if args.port > 0 {
let (tx, rx) = crossbeam_channel::bounded(32);
let wrtc = WebRtcState::new(args.port, args.fps)?;
(Some(wrtc), Some(tx), Some(rx))
} else {
(None, None, None)
};
Ok(Self {
stage: PortalStage::WaitingForFormat,
enc: None,
@@ -59,6 +72,10 @@ impl StatePortal {
start_time: None,
last_stats_time: None,
last_stats_frames: 0,
webrtc,
webrtc_tx,
webrtc_rx,
webrtc_frames_sent: 0,
})
}
@@ -68,6 +85,9 @@ impl StatePortal {
/// `block=false` 时使用 try_recv 非阻塞检查。
/// 返回 `Ok(true)` 表示已处理事件,`Ok(false)` 表示暂无数据。
pub fn poll_and_encode(&mut self, block: bool) -> Result<bool> {
// WebRTC: process signaling, network, and forward encoded frames
self.poll_webrtc()?;
if let Ok(ctrl) = self.cap.event_receiver().try_recv() {
match ctrl {
PwCtrlEvent::StreamEnded => {
@@ -119,19 +139,39 @@ impl StatePortal {
let actual_bitrate = self.args.bitrate.unwrap_or_else(|| {
2 * (enc_width as u64) * (enc_height as u64) * (self.args.fps as u64) / 100
});
let actual_gop_size = self.args.gop_size.unwrap_or(self.args.fps);
let actual_gop_size = self.args.gop_size.unwrap_or_else(|| {
if self.webrtc_tx.is_some() {
(self.args.fps / 2).max(10)
} else {
self.args.fps
}
});
let enc = avhw::SwEncState::new(
&drm_path,
self.args.output.as_ref(),
frame.width,
frame.height,
enc_width,
enc_height,
self.args.fps,
actual_bitrate,
actual_gop_size,
)?;
let enc = if let Some(ref tx) = self.webrtc_tx {
avhw::SwEncState::new_webrtc(
&drm_path,
frame.width,
frame.height,
enc_width,
enc_height,
self.args.fps,
actual_bitrate,
actual_gop_size,
tx.clone(),
)?
} else {
avhw::SwEncState::new(
&drm_path,
std::path::Path::new(self.args.output.as_deref().expect("output required for MP4 mode")),
frame.width,
frame.height,
enc_width,
enc_height,
self.args.fps,
actual_bitrate,
actual_gop_size,
)?
};
self.enc = Some(enc);
self.stage = PortalStage::Streaming;
@@ -145,6 +185,9 @@ impl StatePortal {
}
}
// WebRTC: drain encoded frames produced by this poll before returning.
self.poll_webrtc()?;
Ok(true)
}
@@ -266,6 +309,29 @@ impl StatePortal {
pub fn is_errored(&self) -> bool {
self.errored
}
fn poll_webrtc(&mut self) -> Result<()> {
let Some(ref mut wrtc) = self.webrtc else { return Ok(()); };
wrtc.handle_signaling()?;
wrtc.poll_and_feed()?;
if let Some(ref rx) = self.webrtc_rx {
let mut count = 0u32;
while let Ok(data) = rx.try_recv() {
count += 1;
if let Err(e) = wrtc.write_h264_frame(&data, self.webrtc_frames_sent, self.args.fps) {
tracing::debug!("WebRTC write frame error: {e}");
}
self.webrtc_frames_sent = self.webrtc_frames_sent.saturating_add(1);
}
if count > 0 {
tracing::info!("WebRTC forwarded {count} frames from channel");
}
}
Ok(())
}
}
impl Drop for StatePortal {