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:
+78
-12
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user