Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 27 additions & 18 deletions src/capturer/engine/linux/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::{
mpsc::{self, sync_channel, SyncSender},
},
thread::JoinHandle,
time::Duration,
time::{Duration, SystemTime},
};

use pipewire as pw;
Expand All @@ -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};
Expand All @@ -44,7 +44,7 @@ static STREAM_STATE_CHANGED_TO_ERROR: AtomicBool = AtomicBool::new(false);

#[derive(Clone)]
struct ListenerUserData {
pub tx: mpsc::Sender<Frame>,
pub tx: mpsc::SyncSender<Frame>,
pub format: spa::param::video::VideoInfoRaw,
}

Expand Down Expand Up @@ -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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that the producer/consumer path is sync_channel(2), send() here can block the PipeWire process callback when the consumer is behind. In a realtime-ish callback, that usually hurts more than dropping a frame.

Consider switching to try_send() and treating Full as a silent drop (and only logging on Disconnected).

Suggested change
VideoFormat::RGBx => user_data.tx.send(Frame::Video(VideoFrame::RGBx(RGBxFrame {
let send_result = match user_data.format.format() {
VideoFormat::RGBx => user_data
.tx
.try_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
.try_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
.try_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
.try_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"),
};
match send_result {
Ok(()) => {}
Err(mpsc::TrySendError::Full(_)) => {}
Err(mpsc::TrySendError::Disconnected(_)) => {
eprintln!("frame channel disconnected");
}
}

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}");
Expand All @@ -173,7 +182,7 @@ fn process_callback(stream: &StreamRef, user_data: &mut ListenerUserData) {
// TODO: Format negotiation
fn pipewire_capturer(
options: Options,
tx: mpsc::Sender<Frame>,
tx: mpsc::SyncSender<Frame>,
ready_sender: &SyncSender<bool>,
stream_id: u32,
) -> Result<(), LinCapError> {
Expand Down Expand Up @@ -317,7 +326,7 @@ pub struct LinuxCapturer {

impl LinuxCapturer {
// TODO: Error handling
pub fn new(options: &Options, tx: mpsc::Sender<Frame>) -> Self {
pub fn new(options: &Options, tx: mpsc::SyncSender<Frame>) -> Self {
let connection =
dbus::blocking::Connection::new_session().expect("Failed to create dbus connection");
let stream_id = ScreenCastPortal::new(&connection)
Expand Down Expand Up @@ -364,6 +373,6 @@ impl LinuxCapturer {
}
}

pub fn create_capturer(options: &Options, tx: mpsc::Sender<Frame>) -> LinuxCapturer {
pub fn create_capturer(options: &Options, tx: mpsc::SyncSender<Frame>) -> LinuxCapturer {
LinuxCapturer::new(options, tx)
}
2 changes: 1 addition & 1 deletion src/capturer/engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ pub struct Engine {
}

impl Engine {
pub fn new(options: &Options, tx: mpsc::Sender<ChannelItem>) -> Engine {
pub fn new(options: &Options, tx: mpsc::SyncSender<ChannelItem>) -> Engine {
#[cfg(target_os = "macos")]
{
let error_flag = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
Expand Down
11 changes: 10 additions & 1 deletion src/capturer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor wording nit: this is unbounded growth / retention rather than a true leak, so the comment reads a bit scary as-written.

Suggested change
// limit -- confirmed via heaptrack: ~1.44G leaked over a 25s run,
// consumer (encode/network backpressure) grows this queue without
// limit -- confirmed via heaptrack: ~1.44GiB retained 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 })
Expand Down