Fix AshpdKeeper struct definition and generics in src/media/capture.rs
This commit is contained in:
@@ -15,6 +15,12 @@ use std::process::Stdio;
|
|||||||
use tokio::io::{AsyncReadExt, BufReader, AsyncWriteExt, AsyncBufReadExt};
|
use tokio::io::{AsyncReadExt, BufReader, AsyncWriteExt, AsyncBufReadExt};
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
use xcap::Monitor;
|
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).
|
/// Manages a video capture session (camera or screen).
|
||||||
pub struct VideoCapture {
|
pub struct VideoCapture {
|
||||||
@@ -104,7 +110,17 @@ impl VideoCapture {
|
|||||||
|
|
||||||
tasks.push(tokio::spawn(async move {
|
tasks.push(tokio::spawn(async move {
|
||||||
let result = if matches!(kind_clone, MediaKind::Screen) {
|
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 {
|
} else {
|
||||||
run_ffmpeg_capture(kind_clone, frame_tx_clone, broadcast_tx_clone, capture_running).await
|
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<usize> {
|
|||||||
}
|
}
|
||||||
None
|
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<Vec<u8>>,
|
||||||
|
broadcast_preview: tokio::sync::broadcast::Sender<WebMediaEvent>,
|
||||||
|
running: Arc<AtomicBool>,
|
||||||
|
) -> 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::<Vec<u8>>();
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user