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 })