diff --git a/src/capturer/engine/linux/mod.rs b/src/capturer/engine/linux/mod.rs index 07967fb3..15f3f0c5 100644 --- a/src/capturer/engine/linux/mod.rs +++ b/src/capturer/engine/linux/mod.rs @@ -5,7 +5,7 @@ use std::{ mpsc::{self, sync_channel, SyncSender}, }, thread::JoinHandle, - time::Duration, + time::{Duration, SystemTime}, }; use pipewire as pw; @@ -31,7 +31,7 @@ use pw::{ use crate::{ capturer::Options, - frame::{BGRxFrame, Frame, RGBFrame, RGBxFrame, XBGRFrame}, + frame::{BGRxFrame, Frame, RGBFrame, RGBxFrame, VideoFrame, XBGRFrame}, }; use self::{error::LinCapError, portal::ScreenCastPortal}; @@ -44,7 +44,7 @@ static STREAM_STATE_CHANGED_TO_ERROR: AtomicBool = AtomicBool::new(false); #[derive(Clone)] struct ListenerUserData { - pub tx: mpsc::Sender, + pub tx: mpsc::SyncSender, pub format: spa::param::video::VideoInfoRaw, } @@ -133,31 +133,40 @@ fn process_callback(stream: &StreamRef, user_data: &mut ListenerUserData) { .to_vec() }; + // `timestamp` (from spa_meta_header.pts) is a PipeWire monotonic + // nanosecond count since an arbitrary reference — not wall-clock. + // display_time's SystemTime contract is wall-clock, so we use + // SystemTime::now() here (matches what the macOS and Windows + // engines do today). Relative frame ordering survives via + // channel-send order; sub-millisecond buffer timing is lost. + let _ = timestamp; // suppress "unused" warning until we wire pts elsewhere + let display_time = SystemTime::now(); + if let Err(e) = match user_data.format.format() { - VideoFormat::RGBx => user_data.tx.send(Frame::RGBx(RGBxFrame { - display_time: timestamp as u64, + VideoFormat::RGBx => user_data.tx.send(Frame::Video(VideoFrame::RGBx(RGBxFrame { + display_time, width: frame_size.width as i32, height: frame_size.height as i32, data: frame_data, - })), - VideoFormat::RGB => user_data.tx.send(Frame::RGB(RGBFrame { - display_time: timestamp as u64, + }))), + VideoFormat::RGB => user_data.tx.send(Frame::Video(VideoFrame::RGB(RGBFrame { + display_time, width: frame_size.width as i32, height: frame_size.height as i32, data: frame_data, - })), - VideoFormat::xBGR => user_data.tx.send(Frame::XBGR(XBGRFrame { - display_time: timestamp as u64, + }))), + VideoFormat::xBGR => user_data.tx.send(Frame::Video(VideoFrame::XBGR(XBGRFrame { + display_time, width: frame_size.width as i32, height: frame_size.height as i32, data: frame_data, - })), - VideoFormat::BGRx => user_data.tx.send(Frame::BGRx(BGRxFrame { - display_time: timestamp as u64, + }))), + VideoFormat::BGRx => user_data.tx.send(Frame::Video(VideoFrame::BGRx(BGRxFrame { + display_time, width: frame_size.width as i32, height: frame_size.height as i32, data: frame_data, - })), + }))), _ => panic!("Unsupported frame format received"), } { eprintln!("{e}"); @@ -173,7 +182,7 @@ fn process_callback(stream: &StreamRef, user_data: &mut ListenerUserData) { // TODO: Format negotiation fn pipewire_capturer( options: Options, - tx: mpsc::Sender, + tx: mpsc::SyncSender, ready_sender: &SyncSender, stream_id: u32, ) -> Result<(), LinCapError> { @@ -317,7 +326,7 @@ pub struct LinuxCapturer { impl LinuxCapturer { // TODO: Error handling - pub fn new(options: &Options, tx: mpsc::Sender) -> Self { + pub fn new(options: &Options, tx: mpsc::SyncSender) -> Self { let connection = dbus::blocking::Connection::new_session().expect("Failed to create dbus connection"); let stream_id = ScreenCastPortal::new(&connection) @@ -364,6 +373,6 @@ impl LinuxCapturer { } } -pub fn create_capturer(options: &Options, tx: mpsc::Sender) -> LinuxCapturer { +pub fn create_capturer(options: &Options, tx: mpsc::SyncSender) -> LinuxCapturer { LinuxCapturer::new(options, tx) } diff --git a/src/capturer/engine/mod.rs b/src/capturer/engine/mod.rs index e8aa3a00..a1535c11 100644 --- a/src/capturer/engine/mod.rs +++ b/src/capturer/engine/mod.rs @@ -58,7 +58,7 @@ pub struct Engine { } impl Engine { - pub fn new(options: &Options, tx: mpsc::Sender) -> Engine { + pub fn new(options: &Options, tx: mpsc::SyncSender) -> Engine { #[cfg(target_os = "macos")] { let error_flag = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); diff --git a/src/capturer/mod.rs b/src/capturer/mod.rs index 04a189bb..769f13d0 100644 --- a/src/capturer/mod.rs +++ b/src/capturer/mod.rs @@ -111,7 +111,16 @@ impl Capturer { return Err(CapturerBuildError::PermissionNotGranted); } - let (tx, rx) = mpsc::channel(); + // Bounded (not `mpsc::channel()`): the Linux engine's PipeWire + // callback thread pushes frames into this channel unconditionally, + // independent of how fast `get_next_frame()` is drained downstream. + // An unbounded channel here means any transient slowdown in the + // consumer (encode/network backpressure) grows this queue without + // limit -- confirmed via heaptrack: ~1.44G leaked over a 25s run, + // entirely attributed to undrained `Frame` allocations queued from + // `engine::linux::process_callback`. A small bound applies real + // backpressure to the PipeWire iterate thread instead. + let (tx, rx) = mpsc::sync_channel(2); let engine = engine::Engine::new(&options, tx); Ok(Capturer { engine, rx })