From 6f13bb03aa8bec3d631ed97c4f463a5e44933f17 Mon Sep 17 00:00:00 2001 From: mixa Date: Wed, 18 Feb 2026 13:04:53 +0300 Subject: [PATCH] Fix AshpdKeeper struct definition and generics in src/media/capture.rs --- src/media/capture.rs | 200 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 199 insertions(+), 1 deletion(-) diff --git a/src/media/capture.rs b/src/media/capture.rs index 3c4e567..6cfe4af 100644 --- a/src/media/capture.rs +++ b/src/media/capture.rs @@ -15,6 +15,12 @@ use std::process::Stdio; use tokio::io::{AsyncReadExt, BufReader, AsyncWriteExt, AsyncBufReadExt}; use tokio::process::Command; use xcap::Monitor; +use ashpd::desktop::{PersistMode, screencast::{CursorMode, Screencast, SourceType}}; + +struct AshpdKeeper { + _proxy: Screencast<'static>, + _session: ashpd::desktop::Session<'static, Screencast<'static>>, +} /// Manages a video capture session (camera or screen). pub struct VideoCapture { @@ -104,7 +110,17 @@ impl VideoCapture { tasks.push(tokio::spawn(async move { let result = if matches!(kind_clone, MediaKind::Screen) { - run_xcap_capture(frame_tx_clone, broadcast_tx_clone, capture_running).await + // Try Wayland Portal first + match select_wayland_source().await { + Ok((node_id, keeper)) => { + tracing::info!("Selected Wayland source node: {}", node_id); + run_pipewire_capture(node_id, keeper, frame_tx_clone, broadcast_tx_clone, capture_running).await + } + Err(e) => { + tracing::warn!("Wayland source selection failed ({}). Falling back to xcap.", e); + run_xcap_capture(frame_tx_clone, broadcast_tx_clone, capture_running).await + } + } } else { run_ffmpeg_capture(kind_clone, frame_tx_clone, broadcast_tx_clone, capture_running).await }; @@ -773,3 +789,185 @@ fn find_start_code_from(data: &[u8], start: usize) -> Option { } None } + +// --------------------------------------------------------------------------- +// Wayland Capture Logic (ashpd) +// --------------------------------------------------------------------------- + +async fn select_wayland_source() -> Result<(u32, AshpdKeeper)> { + let proxy = Screencast::new().await?; + let session = proxy.create_session().await?; + + proxy + .select_sources( + &session, + CursorMode::Metadata, + SourceType::Monitor | SourceType::Window, + false, + None, + PersistMode::DoNot, + ) + .await?; + + let request = proxy.start(&session, &ashpd::WindowIdentifier::default()).await?; + let streams = request.response()?; + + if let Some(stream) = streams.streams().iter().next() { + Ok((stream.pipe_wire_node_id(), AshpdKeeper { _proxy: proxy, _session: session })) + } else { + Err(anyhow::anyhow!("No pipewire stream returned from portal")) + } +} + +async fn run_pipewire_capture( + node_id: u32, + _keeper: AshpdKeeper, + frame_tx: tokio::sync::broadcast::Sender>, + broadcast_preview: tokio::sync::broadcast::Sender, + running: Arc, +) -> Result<()> { + tracing::info!("Starting PipeWire capture for node: {}", node_id); + + // Encoder Selection (Hardware preferred) + // Note: PipeWire input is raw, so we can encode it. + + struct EncoderConfig { + name: &'static str, + codec: &'static str, + opts: Vec<&'static str>, + format: &'static str, + filter: &'static str, + } + + let encoders = vec![ + EncoderConfig { + name: "hevc_nvenc (Hardware)", + codec: "hevc_nvenc", + opts: vec!["-b:v", "1M", "-g", "30", "-zerolatency", "1", "-preset", "p4"], + format: "hevc", + filter: "hevc_mp4toannexb", + }, + EncoderConfig { + name: "h264_nvenc (Hardware Fallback)", + codec: "h264_nvenc", + opts: vec!["-b:v", "1.5M", "-g", "30", "-zerolatency", "1", "-preset", "p4"], + format: "h264", + filter: "h264_mp4toannexb", + }, + EncoderConfig { + name: "hevc_vaapi (VAAPI HEVC)", + codec: "hevc_vaapi", + opts: vec![ + "-vaapi_device", "/dev/dri/renderD128", + "-vf", "format=nv12,hwupload,scale_vaapi=w=1280:h=720", + "-b:v", "1M", "-g", "30" + ], + format: "hevc", + filter: "hevc_mp4toannexb", + }, + EncoderConfig { + name: "libx264 (Software Fallback)", + codec: "libx264", + opts: vec!["-preset", "ultrafast", "-tune", "zerolatency", "-b:v", "1M", "-g", "30"], + format: "h264", + filter: "h264_mp4toannexb", + }, + ]; + + let mut final_child = None; + let mut chosen_filter = ""; + + for enc in &encoders { + tracing::info!("Trying encoder: {}", enc.name); + + let mut cmd = Command::new("ffmpeg"); + cmd.kill_on_drop(true); + + cmd.args(&[ + "-f", "pipewire", + "-framerate", "30", + "-i", &node_id.to_string(), + ]); + + if !enc.name.contains("vaapi") { + cmd.args(&["-vf", "scale=1280:720"]); + } + + cmd.arg("-c:v").arg(enc.codec); + cmd.args(&enc.opts); + cmd.arg("-bsf:v").arg(enc.filter); + cmd.arg("-f").arg(enc.format); + cmd.arg("-"); + + cmd.stdout(Stdio::piped()); + cmd.stderr(Stdio::piped()); + + match cmd.spawn() { + Ok(mut child) => { + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + if let Ok(Some(_)) = child.try_wait() { + continue; + } + final_child = Some(child); + chosen_filter = enc.filter; + break; + } + Err(_) => continue, + } + } + + let mut child = match final_child { + Some(c) => c, + None => return Err(anyhow::anyhow!("All encoders failed for PipeWire")), + }; + + let stdout = child.stdout.take().expect("Failed to open stdout"); + let mut reader = BufReader::new(stdout); + let mut buffer = Vec::with_capacity(1024 * 1024); + let mut temp_buf = [0u8; 4096]; + + while running.load(Ordering::Relaxed) { + let n = match reader.read(&mut temp_buf).await { + Ok(0) => break, + Ok(n) => n, + Err(_) => break, + }; + buffer.extend_from_slice(&temp_buf[0..n]); + + while let Some(start_idx) = find_start_code(&buffer) { + let end_idx = if let Some(next_start) = find_start_code_from(&buffer, start_idx + 4) { + next_start + } else { + break; + }; + + let nal_data = buffer.drain(start_idx..end_idx).collect::>(); + let start_code_len = if nal_data[2] == 1 { 3 } else { 4 }; + let nal_header_byte = nal_data[start_code_len]; + + let is_key = if chosen_filter.contains("hevc") { + let nal_type = (nal_header_byte & 0x7E) >> 1; + (nal_type >= 16 && nal_type <= 21) || (nal_type >= 32 && nal_type <= 34) + } else { + let nal_type = nal_header_byte & 0x1F; + nal_type == 5 || nal_type == 7 || nal_type == 8 + }; + + let frame_type = if is_key { 0u8 } else { 1u8 }; + + let mut payload = Vec::with_capacity(1 + nal_data.len()); + payload.push(frame_type); + payload.extend_from_slice(&nal_data); + + let _ = frame_tx.send(payload.clone()); + let _ = broadcast_preview.send(WebMediaEvent::Video { + peer_id: "local".to_string(), + kind: MediaKind::Screen, + data: payload, + }); + } + } + + let _ = child.kill().await; + Ok(()) +}