Add native ScreenCast portal with PipeWire monitor streams
This commit is contained in:
1 parent
cb512eece9
commit
e7d92280d6
21 files changed
+2228
-15
No files matched your search
@@ -0,0 +1,289 @@
|
||||
//! PipeWire runs outside the compositor. Keep one frame, drop old frames under
|
||||
//! backpressure, and destroy the node when the portal's control pipe closes.
|
||||
use crate::capture::{Capture, Frame};
|
||||
use pipewire::{self as pw, properties::properties, spa};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use spa::pod::Pod;
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Ready {
|
||||
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,
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
pub fn run(name: String, cursor: bool) -> 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)?;
|
||||
let first = capture.frame(&name, cursor, &stop, Vec::new())?;
|
||||
let (width, height) = (first.width, first.height);
|
||||
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 data = Data {
|
||||
latest: latest.clone(),
|
||||
stop: stop.clone(),
|
||||
streaming: streaming.clone(),
|
||||
fresh: fresh.clone(),
|
||||
announced: false,
|
||||
};
|
||||
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 {
|
||||
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 != width
|
||||
|| format.size().height != height
|
||||
|| format.format() != spa::param::video::VideoFormat::BGRx
|
||||
{
|
||||
data.stop.store(true, Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
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)
|
||||
));
|
||||
if stream
|
||||
.update_params(&mut [Pod::from_bytes(&buffers).unwrap()])
|
||||
.is_err()
|
||||
{
|
||||
data.stop.store(true, Ordering::Relaxed);
|
||||
}
|
||||
})
|
||||
.process(|stream, data| {
|
||||
let Some(mut buffer) = stream.dequeue_buffer() else {
|
||||
return;
|
||||
};
|
||||
let Some(target) = buffer.datas_mut().first_mut() 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();
|
||||
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;
|
||||
})
|
||||
.register()?;
|
||||
|
||||
let format = 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 }
|
||||
)
|
||||
));
|
||||
|
||||
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 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) => {
|
||||
eprintln!("{error}");
|
||||
capture_stop.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
std::thread::sleep(interval.saturating_sub(start.elapsed()));
|
||||
}
|
||||
});
|
||||
let mut next_frame = Instant::now();
|
||||
|
||||
while !stop.load(Ordering::Relaxed) {
|
||||
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();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user