427 lines
15 KiB
Rust
427 lines
15 KiB
Rust
use std::io::{Read, Write};
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
use std::sync::{Arc, Mutex};
|
|
use std::time::{Duration, Instant};
|
|
|
|
use pipewire as pw;
|
|
use pipewire::properties::properties;
|
|
use pipewire::spa;
|
|
use serde::{Deserialize, Serialize};
|
|
use spa::pod::Pod;
|
|
|
|
use crate::capture::{Capture, Frame};
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct Ready {
|
|
#[serde(default)]
|
|
pub logical_size: Option<(u32, u32)>,
|
|
pub node: u32,
|
|
pub width: u32,
|
|
pub height: u32,
|
|
}
|
|
|
|
struct Data {
|
|
latest: Arc<Mutex<Frame>>,
|
|
stop: Arc<AtomicBool>,
|
|
streaming: Arc<AtomicBool>,
|
|
fresh: Arc<AtomicBool>,
|
|
announced: bool,
|
|
sequence: u64,
|
|
negotiated: (u32, u32),
|
|
}
|
|
|
|
// pipewire-rs exposes read-only metadata through Buffer. Own a dequeued raw
|
|
// buffer here so header writes retain exclusive access and every exit requeues it.
|
|
struct OutputBuffer<'a> {
|
|
stream: &'a pw::stream::Stream,
|
|
raw: std::ptr::NonNull<pw::sys::pw_buffer>,
|
|
}
|
|
impl<'a> OutputBuffer<'a> {
|
|
fn dequeue(stream: &'a pw::stream::Stream) -> Option<Self> {
|
|
// SAFETY: called on the owning PipeWire loop; the guard queues exactly once.
|
|
let raw = std::ptr::NonNull::new(unsafe { stream.dequeue_raw_buffer() })?;
|
|
Some(Self { stream, raw })
|
|
}
|
|
fn data(&mut self) -> Option<&mut spa::buffer::Data> {
|
|
// SAFETY: the dequeued buffer is exclusively ours until Drop. SPA Data
|
|
// is repr(transparent), and PipeWire owns the mapped allocation.
|
|
unsafe {
|
|
let buffer = self.raw.as_ref().buffer.as_mut()?;
|
|
if buffer.n_datas == 0 || buffer.datas.is_null() {
|
|
return None;
|
|
}
|
|
buffer.datas.cast::<spa::buffer::Data>().as_mut()
|
|
}
|
|
}
|
|
fn timestamp(&mut self, sequence: u64) {
|
|
// SAFETY: mutable access is exclusive, the metadata lookup checks its size,
|
|
// and clock_gettime writes only to our initialized stack timespec.
|
|
unsafe {
|
|
let buffer = self.raw.as_ref().buffer;
|
|
if buffer.is_null() {
|
|
return;
|
|
}
|
|
let header = spa::sys::spa_buffer_find_meta_data(
|
|
buffer,
|
|
spa::sys::SPA_META_Header,
|
|
std::mem::size_of::<spa::sys::spa_meta_header>(),
|
|
)
|
|
.cast::<spa::sys::spa_meta_header>();
|
|
if let Some(header) = header.as_mut() {
|
|
let mut now = libc::timespec {
|
|
tv_sec: 0,
|
|
tv_nsec: 0,
|
|
};
|
|
header.pts = if libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut now) == 0 {
|
|
now.tv_sec * 1_000_000_000 + now.tv_nsec
|
|
} else {
|
|
-1
|
|
};
|
|
header.flags = 0;
|
|
header.offset = 0;
|
|
header.dts_offset = 0;
|
|
header.seq = sequence;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
impl Drop for OutputBuffer<'_> {
|
|
fn drop(&mut self) {
|
|
// SAFETY: raw came from this stream and has not yet been queued.
|
|
unsafe {
|
|
self.stream.queue_raw_buffer(self.raw.as_ptr());
|
|
}
|
|
}
|
|
}
|
|
|
|
fn int_property(key: u32, value: i32) -> spa::pod::Property {
|
|
spa::pod::Property {
|
|
key,
|
|
flags: spa::pod::PropertyFlags::empty(),
|
|
value: spa::pod::Value::Int(value),
|
|
}
|
|
}
|
|
fn pod(object: spa::pod::Object) -> Vec<u8> {
|
|
spa::pod::serialize::PodSerializer::serialize(
|
|
std::io::Cursor::new(Vec::new()),
|
|
&spa::pod::Value::Object(object),
|
|
)
|
|
.unwrap()
|
|
.0
|
|
.into_inner()
|
|
}
|
|
|
|
fn video_format(width: u32, height: u32) -> Vec<u8> {
|
|
pod(spa::pod::object!(
|
|
spa::utils::SpaTypes::ObjectParamFormat,
|
|
spa::param::ParamType::EnumFormat,
|
|
spa::pod::property!(
|
|
spa::param::format::FormatProperties::MediaType,
|
|
Id,
|
|
spa::param::format::MediaType::Video
|
|
),
|
|
spa::pod::property!(
|
|
spa::param::format::FormatProperties::MediaSubtype,
|
|
Id,
|
|
spa::param::format::MediaSubtype::Raw
|
|
),
|
|
spa::pod::property!(
|
|
spa::param::format::FormatProperties::VideoFormat,
|
|
Id,
|
|
spa::param::video::VideoFormat::BGRx
|
|
),
|
|
spa::pod::property!(
|
|
spa::param::format::FormatProperties::VideoSize,
|
|
Rectangle,
|
|
spa::utils::Rectangle { width, height }
|
|
),
|
|
spa::pod::property!(
|
|
spa::param::format::FormatProperties::VideoFramerate,
|
|
Fraction,
|
|
spa::utils::Fraction { num: 30, denom: 1 }
|
|
)
|
|
))
|
|
}
|
|
|
|
pub fn run(
|
|
name: String,
|
|
cursor: bool,
|
|
generation: Option<u32>,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let stop = Arc::new(AtomicBool::new(false));
|
|
let pipe_stop = stop.clone();
|
|
std::thread::spawn(move || {
|
|
let mut byte = [0u8];
|
|
let _ = std::io::stdin().read(&mut byte);
|
|
pipe_stop.store(true, Ordering::Relaxed);
|
|
});
|
|
let mut capture = Capture::connect(&stop)?;
|
|
if let Some(generation) = generation {
|
|
capture.pin_output(&name, generation)?;
|
|
}
|
|
let deadline = Instant::now() + Duration::from_secs(5);
|
|
let first = loop {
|
|
match capture.frame(&name, cursor, &stop, Vec::new()) {
|
|
Err(error) if error == crate::capture::BUSY && Instant::now() < deadline => {
|
|
std::thread::sleep(Duration::from_millis(10));
|
|
}
|
|
result => break result?,
|
|
}
|
|
};
|
|
let (width, height) = (first.width, first.height);
|
|
let logical_size = first.logical_size;
|
|
let resizable = name.starts_with("window:");
|
|
let latest = Arc::new(Mutex::new(first));
|
|
let streaming = Arc::new(AtomicBool::new(false));
|
|
|
|
let fresh = Arc::new(AtomicBool::new(false));
|
|
pw::init();
|
|
|
|
let mainloop = pw::main_loop::MainLoopRc::new(None)?;
|
|
let context = pw::context::ContextRc::new(&mainloop, None)?;
|
|
let core = context.connect_rc(None)?;
|
|
let stream = pw::stream::StreamBox::new(
|
|
&core,
|
|
"Ferese ScreenCast",
|
|
properties! {
|
|
"media.class" => "Video/Source",
|
|
"media.type" => "Video",
|
|
"media.category" => "Capture",
|
|
"media.role" => "Screen",
|
|
"node.name" => format!("ferese.screencast.{}", std::process::id()),
|
|
"node.description" => format!("Ferese — {name}"),
|
|
"node.virtual" => "true",
|
|
"node.want-driver" => "true",
|
|
"node.always-process" => "false",
|
|
"node.pause-on-idle" => "true",
|
|
"node.latency" => "1/30",
|
|
},
|
|
)?;
|
|
let format_frames = latest.clone();
|
|
let data = Data {
|
|
latest: latest.clone(),
|
|
stop: stop.clone(),
|
|
streaming: streaming.clone(),
|
|
fresh: fresh.clone(),
|
|
announced: false,
|
|
sequence: 0,
|
|
negotiated: (width, height),
|
|
};
|
|
let _listener = stream
|
|
.add_local_listener_with_user_data(data)
|
|
.state_changed(move |stream, data, _, state| {
|
|
data.fresh.store(false, Ordering::SeqCst);
|
|
data.streaming.store(
|
|
matches!(state, pw::stream::StreamState::Streaming),
|
|
Ordering::Relaxed,
|
|
);
|
|
match state {
|
|
pw::stream::StreamState::Paused | pw::stream::StreamState::Streaming
|
|
if !data.announced =>
|
|
{
|
|
let node = stream.node_id();
|
|
if node != u32::MAX {
|
|
data.announced = true;
|
|
println!(
|
|
"{}",
|
|
serde_json::to_string(&Ready {
|
|
logical_size,
|
|
node,
|
|
width,
|
|
height
|
|
})
|
|
.unwrap()
|
|
);
|
|
let _ = std::io::stdout().flush();
|
|
}
|
|
}
|
|
pw::stream::StreamState::Unconnected if data.announced => {
|
|
data.stop.store(true, Ordering::Relaxed);
|
|
}
|
|
pw::stream::StreamState::Error(error) => {
|
|
eprintln!("PipeWire stream: {error}");
|
|
data.stop.store(true, Ordering::Relaxed);
|
|
}
|
|
_ => (),
|
|
}
|
|
})
|
|
.param_changed(move |stream, data, id, param| {
|
|
if id != spa::param::ParamType::Format.as_raw() {
|
|
return;
|
|
}
|
|
let Some(param) = param else { return };
|
|
let mut format = spa::param::video::VideoInfoRaw::default();
|
|
if format.parse(param).is_err()
|
|
|| format.size().width == 0
|
|
|| format.size().height == 0
|
|
|| (!resizable && (format.size().width, format.size().height) != (width, height))
|
|
|| u64::from(format.size().width) * u64::from(format.size().height) * 4
|
|
> 128 * 1024 * 1024
|
|
|| format.format() != spa::param::video::VideoFormat::BGRx
|
|
{
|
|
data.stop.store(true, Ordering::Relaxed);
|
|
return;
|
|
}
|
|
let (width, height) = (format.size().width, format.size().height);
|
|
data.negotiated = (width, height);
|
|
let buffers = pod(spa::pod::object!(
|
|
spa::utils::SpaTypes::ObjectParamBuffers,
|
|
spa::param::ParamType::Buffers,
|
|
int_property(spa::sys::SPA_PARAM_BUFFERS_buffers, 4),
|
|
int_property(spa::sys::SPA_PARAM_BUFFERS_blocks, 1),
|
|
int_property(
|
|
spa::sys::SPA_PARAM_BUFFERS_size,
|
|
(width * height * 4) as i32
|
|
),
|
|
int_property(spa::sys::SPA_PARAM_BUFFERS_stride, (width * 4) as i32),
|
|
int_property(spa::sys::SPA_PARAM_BUFFERS_align, 16)
|
|
));
|
|
let header = pod(spa::pod::object!(
|
|
spa::utils::SpaTypes::ObjectParamMeta,
|
|
spa::param::ParamType::Meta,
|
|
spa::pod::Property {
|
|
key: spa::sys::SPA_PARAM_META_type,
|
|
flags: spa::pod::PropertyFlags::empty(),
|
|
value: spa::pod::Value::Id(spa::utils::Id(spa::sys::SPA_META_Header)),
|
|
},
|
|
int_property(
|
|
spa::sys::SPA_PARAM_META_size,
|
|
std::mem::size_of::<spa::sys::spa_meta_header>() as i32
|
|
)
|
|
));
|
|
if stream
|
|
.update_params(&mut [
|
|
Pod::from_bytes(&buffers).unwrap(),
|
|
Pod::from_bytes(&header).unwrap(),
|
|
])
|
|
.is_err()
|
|
{
|
|
data.stop.store(true, Ordering::Relaxed);
|
|
}
|
|
})
|
|
.process(|stream, data| {
|
|
let Some(mut buffer) = OutputBuffer::dequeue(stream) else {
|
|
return;
|
|
};
|
|
let Some(target) = buffer.data() else {
|
|
return;
|
|
};
|
|
if data.stop.load(Ordering::Relaxed) || !data.fresh.load(Ordering::SeqCst) {
|
|
*target.chunk_mut().size_mut() = 0;
|
|
return;
|
|
}
|
|
let frame = data.latest.lock().unwrap();
|
|
if (frame.width, frame.height) != data.negotiated {
|
|
*target.chunk_mut().size_mut() = 0;
|
|
return;
|
|
}
|
|
let row = frame.width as usize * 4;
|
|
let bytes = row * frame.height as usize;
|
|
let Some(destination) = target.data() else {
|
|
return;
|
|
};
|
|
if destination.len() < bytes {
|
|
data.stop.store(true, Ordering::Relaxed);
|
|
return;
|
|
}
|
|
for (dst, src) in destination[..bytes]
|
|
.chunks_exact_mut(row)
|
|
.zip(frame.pixels.chunks_exact(frame.stride as usize))
|
|
{
|
|
dst.copy_from_slice(&src[..row]);
|
|
}
|
|
*target.chunk_mut().offset_mut() = 0;
|
|
*target.chunk_mut().size_mut() = bytes as u32;
|
|
*target.chunk_mut().stride_mut() = row as i32;
|
|
buffer.timestamp(data.sequence);
|
|
data.sequence = data.sequence.wrapping_add(1);
|
|
})
|
|
.register()?;
|
|
|
|
let format = video_format(width, height);
|
|
|
|
stream.connect(
|
|
spa::utils::Direction::Output,
|
|
None,
|
|
pw::stream::StreamFlags::MAP_BUFFERS | pw::stream::StreamFlags::DRIVER,
|
|
&mut [Pod::from_bytes(&format).unwrap()],
|
|
)?;
|
|
|
|
let interval = Duration::from_nanos(1_000_000_000 / 30);
|
|
let capture_stop = stop.clone();
|
|
let capture_streaming = streaming.clone();
|
|
let capture_thread = std::thread::spawn(move || {
|
|
let mut spare = Vec::new();
|
|
let mut checked = Instant::now() - Duration::from_secs(1);
|
|
while !capture_stop.load(Ordering::Relaxed) {
|
|
let start = Instant::now();
|
|
|
|
if capture_streaming.load(Ordering::Relaxed)
|
|
|| checked.elapsed() >= Duration::from_secs(1)
|
|
{
|
|
checked = Instant::now();
|
|
match capture.frame(&name, cursor, &capture_stop, std::mem::take(&mut spare)) {
|
|
Ok(frame) if resizable || (frame.width == width && frame.height == height) => {
|
|
spare = std::mem::replace(&mut *latest.lock().unwrap(), frame).pixels;
|
|
fresh.store(capture_streaming.load(Ordering::Relaxed), Ordering::SeqCst);
|
|
}
|
|
Ok(_) => {
|
|
eprintln!("Shared monitor changed size; start sharing again");
|
|
capture_stop.store(true, Ordering::Relaxed);
|
|
}
|
|
Err(error) if error == crate::capture::BUSY => (),
|
|
Err(error) => {
|
|
eprintln!("{error}");
|
|
capture_stop.store(true, Ordering::Relaxed);
|
|
}
|
|
}
|
|
}
|
|
std::thread::sleep(interval.saturating_sub(start.elapsed()));
|
|
}
|
|
});
|
|
let mut next_frame = Instant::now();
|
|
let mut requested_size = (width, height);
|
|
let mut renegotiation_error = None;
|
|
|
|
while !stop.load(Ordering::Relaxed) {
|
|
let size = {
|
|
let frame = format_frames.lock().unwrap();
|
|
(frame.width, frame.height)
|
|
};
|
|
if resizable && size != requested_size {
|
|
let format = video_format(size.0, size.1);
|
|
if let Err(error) = stream.update_params(&mut [Pod::from_bytes(&format).unwrap()]) {
|
|
renegotiation_error = Some(error);
|
|
stop.store(true, Ordering::Relaxed);
|
|
break;
|
|
}
|
|
requested_size = size;
|
|
}
|
|
let active = streaming.load(Ordering::Relaxed);
|
|
let timeout = if active {
|
|
next_frame
|
|
.saturating_duration_since(Instant::now())
|
|
.min(Duration::from_millis(10))
|
|
} else {
|
|
Duration::from_millis(10)
|
|
};
|
|
mainloop
|
|
.loop_()
|
|
.iterate(pw::loop_::Timeout::Finite(timeout));
|
|
if streaming.load(Ordering::Relaxed) && Instant::now() >= next_frame {
|
|
let _ = stream.trigger_process();
|
|
next_frame = Instant::now() + interval;
|
|
} else if !streaming.load(Ordering::Relaxed) {
|
|
next_frame = Instant::now();
|
|
}
|
|
}
|
|
|
|
let _ = stream.disconnect();
|
|
let _ = capture_thread.join();
|
|
if let Some(error) = renegotiation_error {
|
|
return Err(error.into());
|
|
}
|
|
|
|
Ok(())
|
|
}
|