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:
+135
-23
@@ -596,13 +596,18 @@ impl EncState {
|
||||
// SwEncState - VAAPI GPU downscale + software H.264 encode
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub enum FrameOutput {
|
||||
Muxer(ff::format::context::Output),
|
||||
Channel(crossbeam_channel::Sender<Vec<u8>>),
|
||||
}
|
||||
|
||||
pub struct SwEncState {
|
||||
hw_dev: AvHwDevCtx,
|
||||
frames_rgb: AvHwFrameCtx,
|
||||
filter_graph: ff::filter::Graph,
|
||||
sws_ctx: *mut ffi::SwsContext,
|
||||
enc_video: ff::codec::encoder::video::Video,
|
||||
octx: ff::format::context::Output,
|
||||
output: Option<FrameOutput>,
|
||||
yuv_frame: *mut ffi::AVFrame,
|
||||
starting_timestamp: Option<i64>,
|
||||
frames_written: bool,
|
||||
@@ -651,7 +656,52 @@ impl SwEncState {
|
||||
filter_graph,
|
||||
sws_ctx,
|
||||
enc_video,
|
||||
octx,
|
||||
output: Some(FrameOutput::Muxer(octx)),
|
||||
yuv_frame,
|
||||
starting_timestamp: None,
|
||||
frames_written: false,
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new_webrtc(
|
||||
drm_device: &Path,
|
||||
width: u32,
|
||||
height: u32,
|
||||
enc_width: u32,
|
||||
enc_height: u32,
|
||||
fps: u32,
|
||||
bitrate: u64,
|
||||
gop_size: u32,
|
||||
tx: crossbeam_channel::Sender<Vec<u8>>,
|
||||
) -> Result<Self> {
|
||||
tracing::info!(
|
||||
"SwEncState::new_webrtc: GPU downscale {width}x{height} BGRA -> {enc_width}x{enc_height} NV12, software H.264 -> WebRTC"
|
||||
);
|
||||
|
||||
let hw_dev = AvHwDevCtx::new_vaapi(drm_device)?;
|
||||
let frames_rgb =
|
||||
AvHwFrameCtx::for_capture(&hw_dev, width, height, ff::format::Pixel::BGRA)?;
|
||||
let filter_graph = build_swenc_filter_graph(
|
||||
&hw_dev,
|
||||
&frames_rgb,
|
||||
width,
|
||||
height,
|
||||
enc_width,
|
||||
enc_height,
|
||||
fps,
|
||||
)?;
|
||||
let sws_ctx = create_nv12_to_yuv420p_sws(enc_width, enc_height)?;
|
||||
let enc_video = create_software_h264_encoder(enc_width, enc_height, fps, bitrate, gop_size)?;
|
||||
let yuv_frame = alloc_yuv420p_frame(enc_width, enc_height)?;
|
||||
|
||||
Ok(Self {
|
||||
hw_dev,
|
||||
frames_rgb,
|
||||
filter_graph,
|
||||
sws_ctx,
|
||||
enc_video,
|
||||
output: Some(FrameOutput::Channel(tx)),
|
||||
yuv_frame,
|
||||
starting_timestamp: None,
|
||||
frames_written: false,
|
||||
@@ -704,7 +754,6 @@ impl SwEncState {
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: Sending a null frame flushes the encoder without transferring ownership.
|
||||
unsafe {
|
||||
let ret = ffi::avcodec_send_frame(self.enc_video.as_mut_ptr(), ptr::null());
|
||||
if ret < 0 && ret != ffi::AVERROR_EOF {
|
||||
@@ -715,9 +764,10 @@ impl SwEncState {
|
||||
self.drain_encoder(start_ts)?;
|
||||
|
||||
if self.frames_written {
|
||||
self.octx
|
||||
.write_trailer()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to write trailer: {e}"))?;
|
||||
if let Some(FrameOutput::Muxer(ref mut octx)) = self.output {
|
||||
octx.write_trailer()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to write trailer: {e}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -793,25 +843,39 @@ impl SwEncState {
|
||||
bail!("avcodec_receive_packet failed: error {ret}");
|
||||
}
|
||||
|
||||
let enc_tb = self.enc_video.time_base();
|
||||
let stream_tb = unsafe {
|
||||
let streams = (*self.octx.as_ptr()).streams;
|
||||
let st = *streams.add(0);
|
||||
ff::Rational::from((*st).time_base)
|
||||
};
|
||||
pkt.rescale_ts(enc_tb, stream_tb);
|
||||
match self.output {
|
||||
Some(FrameOutput::Muxer(ref mut octx)) => {
|
||||
let enc_tb = self.enc_video.time_base();
|
||||
let stream_tb = unsafe {
|
||||
let streams = (*octx.as_ptr()).streams;
|
||||
let st = *streams.add(0);
|
||||
ff::Rational::from((*st).time_base)
|
||||
};
|
||||
pkt.rescale_ts(enc_tb, stream_tb);
|
||||
|
||||
if let Some(pts) = pkt.pts() {
|
||||
pkt.set_pts(Some(pts - start_ts));
|
||||
}
|
||||
if let Some(dts) = pkt.dts() {
|
||||
pkt.set_dts(Some(dts - start_ts));
|
||||
}
|
||||
if let Some(pts) = pkt.pts() {
|
||||
pkt.set_pts(Some(pts - start_ts));
|
||||
}
|
||||
if let Some(dts) = pkt.dts() {
|
||||
pkt.set_dts(Some(dts - start_ts));
|
||||
}
|
||||
|
||||
pkt.set_stream(0);
|
||||
pkt.write_interleaved(&mut self.octx)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to write packet: {e}"))?;
|
||||
self.frames_written = true;
|
||||
pkt.set_stream(0);
|
||||
pkt.write_interleaved(octx)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to write packet: {e}"))?;
|
||||
self.frames_written = true;
|
||||
}
|
||||
Some(FrameOutput::Channel(ref tx)) => {
|
||||
let data: &[u8] = unsafe {
|
||||
std::slice::from_raw_parts(
|
||||
(*pkt.as_mut_ptr()).data,
|
||||
(*pkt.as_mut_ptr()).size as usize,
|
||||
)
|
||||
};
|
||||
let _ = tx.send(data.to_vec());
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1115,6 +1179,54 @@ fn create_software_h264_muxer(
|
||||
Ok((enc_video, octx))
|
||||
}
|
||||
|
||||
fn create_software_h264_encoder(
|
||||
width: u32,
|
||||
height: u32,
|
||||
fps: u32,
|
||||
bitrate: u64,
|
||||
gop_size: u32,
|
||||
) -> Result<ff::codec::encoder::video::Video> {
|
||||
let codec = ff::encoder::find_by_name("libx264")
|
||||
.or_else(|| ff::encoder::find_by_name("libopenh264"))
|
||||
.ok_or_else(|| anyhow::anyhow!("No H.264 software encoder found"))?;
|
||||
let codec_name = codec.name().to_string();
|
||||
|
||||
let mut enc = {
|
||||
let ctx = ff::codec::Context::new_with_codec(codec);
|
||||
ctx.encoder().video()?
|
||||
};
|
||||
enc.set_width(width);
|
||||
enc.set_height(height);
|
||||
enc.set_format(ff::format::Pixel::YUV420P);
|
||||
enc.set_bit_rate(bitrate as usize);
|
||||
enc.set_gop(gop_size);
|
||||
enc.set_time_base(ff::Rational::new(1, fps as i32));
|
||||
enc.set_max_b_frames(0);
|
||||
|
||||
if codec_name == "libx264" {
|
||||
unsafe {
|
||||
let key = CString::new("preset").unwrap();
|
||||
let val = CString::new("ultrafast").unwrap();
|
||||
ffi::av_opt_set((*enc.as_mut_ptr()).priv_data, key.as_ptr(), val.as_ptr(), 0);
|
||||
let key = CString::new("tune").unwrap();
|
||||
let val = CString::new("zerolatency").unwrap();
|
||||
ffi::av_opt_set((*enc.as_mut_ptr()).priv_data, key.as_ptr(), val.as_ptr(), 0);
|
||||
let key = CString::new("threads").unwrap();
|
||||
let val = CString::new("6").unwrap();
|
||||
ffi::av_opt_set((*enc.as_mut_ptr()).priv_data, key.as_ptr(), val.as_ptr(), 0);
|
||||
let key = CString::new("x264opts").unwrap();
|
||||
let val = CString::new("repeat_headers=1").unwrap();
|
||||
ffi::av_opt_set((*enc.as_mut_ptr()).priv_data, key.as_ptr(), val.as_ptr(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
let opened = enc
|
||||
.open()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to open {codec_name} encoder: {e}"))?;
|
||||
tracing::info!("WebRTC encoder: {codec_name} {width}x{height} @ {fps}fps {bitrate}bps");
|
||||
Ok(opened.0)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Filter graph (inline)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user