use std::os::fd::{IntoRawFd, OwnedFd};
use ashpd::desktop::{
PersistMode,
screencast::{
CursorMode, Screencast, SelectSourcesOptions, SourceType, Stream as ScreencastStream,
},
};
use pipewire as pw;
use pw::{properties::properties, spa};
struct UserData {
format: spa::param::video::VideoInfoRaw,
}
async fn open_portal() -> ashpd::Result<(ScreencastStream, OwnedFd)> {
let proxy = Screencast::new().await?;
let session = proxy.create_session(Default::default()).await?;
proxy
.select_sources(
&session,
SelectSourcesOptions::default()
.set_cursor_mode(CursorMode::Hidden)
.set_sources(SourceType::Monitor | SourceType::Window)
.set_multiple(false)
.set_restore_token(None)
.set_persist_mode(PersistMode::DoNot),
)
.await?;
let response = proxy
.start(&session, None, Default::default())
.await?
.response()?;
let stream = response
.streams()
.first()
.expect("no stream found / selected")
.to_owned();
let fd = proxy
.open_pipe_wire_remote(&session, Default::default())
.await?;
Ok((stream, fd))
}
async fn start_streaming(node_id: u32, fd: OwnedFd) -> Result<(), pw::Error> {
pw::init();
let mainloop = pw::main_loop::MainLoopBox::new(None)?;
let context = pw::context::ContextBox::new(mainloop.loop_(), None)?;
let core = context.connect_fd(fd, None)?;
let data = UserData {
format: Default::default(),
};
let stream = pw::stream::StreamBox::new(
&core,
"video-test",
properties! {
*pw::keys::MEDIA_TYPE => "Video",
*pw::keys::MEDIA_CATEGORY => "Capture",
*pw::keys::MEDIA_ROLE => "Screen",
},
)?;
let _listener = stream
.add_local_listener_with_user_data(data)
.state_changed(|_, _, old, new| {
println!("State changed: {:?} -> {:?}", old, new);
})
.param_changed(|_, user_data, id, param| {
let Some(param) = param else {
return;
};
if id != pw::spa::param::ParamType::Format.as_raw() {
return;
}
let (media_type, media_subtype) =
match pw::spa::param::format_utils::parse_format(param) {
Ok(v) => v,
Err(_) => return,
};
if media_type != pw::spa::param::format::MediaType::Video
|| media_subtype != pw::spa::param::format::MediaSubtype::Raw
{
return;
}
user_data
.format
.parse(param)
.expect("Failed to parse param changed to VideoInfoRaw");
println!("got video format:");
println!(
"\tformat: {} ({:?})",
user_data.format.format().as_raw(),
user_data.format.format()
);
println!(
"\tsize: {}x{}",
user_data.format.size().width,
user_data.format.size().height
);
println!(
"\tframerate: {}/{}",
user_data.format.framerate().num,
user_data.format.framerate().denom
);
})
.process(|stream, _| {
match stream.dequeue_buffer() {
None => println!("out of buffers"),
Some(mut buffer) => {
let datas = buffer.datas_mut();
if datas.is_empty() {
return;
}
let data = &mut datas[0];
println!("got a frame of size {}", data.chunk().size());
}
}
})
.register()?;
println!("Created stream {:#?}", stream);
let obj = pw::spa::pod::object!(
pw::spa::utils::SpaTypes::ObjectParamFormat,
pw::spa::param::ParamType::EnumFormat,
pw::spa::pod::property!(
pw::spa::param::format::FormatProperties::MediaType,
Id,
pw::spa::param::format::MediaType::Video
),
pw::spa::pod::property!(
pw::spa::param::format::FormatProperties::MediaSubtype,
Id,
pw::spa::param::format::MediaSubtype::Raw
),
pw::spa::pod::property!(
pw::spa::param::format::FormatProperties::VideoFormat,
Choice,
Enum,
Id,
pw::spa::param::video::VideoFormat::RGB,
pw::spa::param::video::VideoFormat::RGB,
pw::spa::param::video::VideoFormat::RGBA,
pw::spa::param::video::VideoFormat::RGBx,
pw::spa::param::video::VideoFormat::BGRx,
pw::spa::param::video::VideoFormat::YUY2,
pw::spa::param::video::VideoFormat::I420,
),
pw::spa::pod::property!(
pw::spa::param::format::FormatProperties::VideoSize,
Choice,
Range,
Rectangle,
pw::spa::utils::Rectangle {
width: 320,
height: 240
},
pw::spa::utils::Rectangle {
width: 1,
height: 1
},
pw::spa::utils::Rectangle {
width: 4096,
height: 4096
}
),
pw::spa::pod::property!(
pw::spa::param::format::FormatProperties::VideoFramerate,
Choice,
Range,
Fraction,
pw::spa::utils::Fraction { num: 25, denom: 1 },
pw::spa::utils::Fraction { num: 0, denom: 1 },
pw::spa::utils::Fraction {
num: 1000,
denom: 1
}
),
);
let values: Vec<u8> = pw::spa::pod::serialize::PodSerializer::serialize(
std::io::Cursor::new(Vec::new()),
&pw::spa::pod::Value::Object(obj),
)
.unwrap()
.0
.into_inner();
let mut params = [spa::pod::Pod::from_bytes(&values).unwrap()];
stream.connect(
spa::utils::Direction::Input,
Some(node_id),
pw::stream::StreamFlags::AUTOCONNECT | pw::stream::StreamFlags::MAP_BUFFERS,
&mut params,
)?;
println!("Connected stream");
mainloop.run();
Ok(())
}
#[tokio::main]
async fn main() {
let (stream, fd) = open_portal().await.expect("failed to open portal");
let pipewire_node_id = stream.pipe_wire_node_id();
println!(
"node id {}, fd {}",
pipewire_node_id,
&fd.try_clone().unwrap().into_raw_fd()
);
if let Err(e) = start_streaming(pipewire_node_id, fd).await {
eprintln!("Error: {}", e);
};
}