Skip to content

Commit

Permalink
examples: Add screen cast with pipewire (#247)
Browse files Browse the repository at this point in the history
Fixes #155
  • Loading branch information
SyedAhkam authored Nov 24, 2024
1 parent 90df195 commit 80fac9e
Show file tree
Hide file tree
Showing 5 changed files with 241 additions and 0 deletions.
1 change: 1 addition & 0 deletions .typos.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[default.extend-words]
# Ignore false-positives
eis = "eis"
datas = "datas"
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ zbus = { version = "5.0", default-features = false, features = ["url"] }
[dev-dependencies]
serde_json = "1.0"
reis = { version = "0.4", features = ["tokio"] }
pipewire = "0.8.0"
tokio = { version = "1.41", features = [ "rt-multi-thread", "macros" ] }

[package.metadata.docs.rs]
features = ["backend", "gtk4", "raw_handle"]
Expand Down
224 changes: 224 additions & 0 deletions examples/screen_cast_pw.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
use std::os::fd::{IntoRawFd, OwnedFd};

use ashpd::desktop::{
screencast::{CursorMode, Screencast, SourceType, Stream as ScreencastStream},
PersistMode,
};
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().await?;
proxy
.select_sources(
&session,
CursorMode::Hidden,
SourceType::Monitor.into(),
false,
None,
PersistMode::DoNot,
)
.await?;

let response = proxy.start(&session, None).await?.response()?;
let stream = response
.streams()
.first()
.expect("no stream found / selected")
.to_owned();

let fd = proxy.open_pipe_wire_remote(&session).await?;

Ok((stream, fd))
}

async fn start_streaming(node_id: u32, fd: OwnedFd) -> Result<(), pw::Error> {
pw::init();

let mainloop = pw::main_loop::MainLoop::new(None)?;
let context = pw::context::Context::new(&mainloop)?;
let core = context.connect_fd(fd, None)?;

let data = UserData {
format: Default::default(),
};

let stream = pw::stream::Stream::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
);

// prepare to render video of this size
})
.process(|stream, _| {
match stream.dequeue_buffer() {
None => println!("out of buffers"),
Some(mut buffer) => {
let datas = buffer.datas_mut();
if datas.is_empty() {
return;
}

// copy frame data to screen
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);
};
}
13 changes: 13 additions & 0 deletions src/desktop/camera.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@
//! Ok(())
//! }
//! ```
//! An example on how to connect with Pipewire can be found [here](https://github.com/bilelmoussaoui/ashpd/blob/master/examples/screen_cast_pw.rs).
//! Although the example's primary focus is screen casting, stream connection logic remains the same -- with one accessibility change:
//! ```rust,ignore
//! let stream = pw::stream::Stream::new(
//! &core,
//! "video-test",
//! properties! {
//! *pw::keys::MEDIA_TYPE => "Video",
//! *pw::keys::MEDIA_CATEGORY => "Capture",
//! *pw::keys::MEDIA_ROLE => "Screen", // <-- make this 'Camera'
//! },
//! )?;
//! ```
use std::{collections::HashMap, os::fd::OwnedFd};

Expand Down
1 change: 1 addition & 0 deletions src/desktop/screencast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
//! Ok(())
//! }
//! ```
//! An example on how to connect with Pipewire can be found [here](https://github.com/bilelmoussaoui/ashpd/blob/master/examples/screen_cast_pw.rs).
use std::{collections::HashMap, fmt::Debug, os::fd::OwnedFd};

Expand Down

0 comments on commit 80fac9e

Please sign in to comment.