Add initial monitor screencast portal impl

DmaBuf monitor screencasting through xdg-dekstop-portal-gnome!

Somewhat limited currently, e.g. the cursor is always embedded. But gets
most of the job done.
This commit is contained in:
Ivan Molodetskikh 2023-09-08 17:54:02 +04:00
parent bd0ecf9174
commit d52ca23caa
13 changed files with 1295 additions and 53 deletions

View File

@ -29,8 +29,10 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get install -y software-properties-common
sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
sudo apt-get update -y
sudo apt-get install -y libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev
sudo apt-get install -y libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev
- name: Install Rust
run: |
@ -67,8 +69,10 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get install -y software-properties-common
sudo add-apt-repository -y ppa:pipewire-debian/pipewire-upstream
sudo apt-get update -y
sudo apt-get install -y libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev
sudo apt-get install -y libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev
- name: Install Rust
run: |

218
Cargo.lock generated
View File

@ -294,6 +294,26 @@ version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53"
[[package]]
name = "bindgen"
version = "0.66.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b84e06fc203107bfbad243f4aba2af864eb7db3b1cf46ea0a023b0b433d2a7"
dependencies = [
"bitflags 2.4.0",
"cexpr",
"clang-sys",
"lazy_static",
"lazycell",
"peeking_take_while",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn 2.0.31",
]
[[package]]
name = "bitflags"
version = "1.3.2"
@ -405,6 +425,25 @@ dependencies = [
"libc",
]
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-expr"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03915af431787e6ffdcc74c645077518c6b6e01f80b761e0fbbfa288536311b3"
dependencies = [
"smallvec",
"target-lexicon",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
@ -436,6 +475,17 @@ dependencies = [
"hashbrown 0.12.3",
]
[[package]]
name = "clang-sys"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f"
dependencies = [
"glob",
"libc",
"libloading 0.7.4",
]
[[package]]
name = "clap"
version = "4.4.2"
@ -497,6 +547,21 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "convert_case"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "cookie-factory"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "396de984970346b0d9e93d1415082923c679e5ae5c3ee3dcbd104f5610af126b"
[[package]]
name = "core-foundation"
version = "0.9.3"
@ -634,7 +699,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [
"libloading",
"libloading 0.8.0",
]
[[package]]
@ -920,6 +985,12 @@ dependencies = [
"xml-rs",
]
[[package]]
name = "glob"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "hashbrown"
version = "0.12.3"
@ -1130,12 +1201,28 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "libc"
version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "libloading"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
dependencies = [
"cfg-if",
"winapi",
]
[[package]]
name = "libloading"
version = "0.8.0"
@ -1172,6 +1259,34 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "libspa"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0434617020ddca18b86067912970c55410ca654cdafd775480322f50b857a8c4"
dependencies = [
"bitflags 2.4.0",
"cc",
"convert_case",
"cookie-factory",
"libc",
"libspa-sys",
"nix 0.26.4",
"nom",
"system-deps",
]
[[package]]
name = "libspa-sys"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3e70ca3f3e70f858ef363046d06178c427b4e0b63d210c95fd87d752679d345"
dependencies = [
"bindgen",
"cc",
"system-deps",
]
[[package]]
name = "libudev-sys"
version = "0.1.4"
@ -1368,6 +1483,7 @@ name = "niri"
version = "0.1.0"
dependencies = [
"anyhow",
"async-io",
"bitflags 2.4.0",
"clap",
"directories",
@ -1376,6 +1492,7 @@ dependencies = [
"knuffel",
"logind-zbus",
"miette",
"pipewire",
"portable-atomic",
"profiling",
"sd-notify",
@ -1614,6 +1731,12 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e"
[[package]]
name = "peeking_take_while"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
[[package]]
name = "percent-encoding"
version = "2.3.0"
@ -1632,6 +1755,34 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pipewire"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2d009c8dd65e890b515a71950f7e4c801523b8894ff33863a40830bf762e9e9"
dependencies = [
"anyhow",
"bitflags 2.4.0",
"libc",
"libspa",
"libspa-sys",
"nix 0.26.4",
"once_cell",
"pipewire-sys",
"thiserror",
]
[[package]]
name = "pipewire-sys"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "890c084e7b737246cb4799c86b71a0e4da536031ff7473dd639eba9f95039f64"
dependencies = [
"bindgen",
"libspa-sys",
"system-deps",
]
[[package]]
name = "pkg-config"
version = "0.3.27"
@ -1875,6 +2026,12 @@ version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustix"
version = "0.37.23"
@ -1957,6 +2114,15 @@ dependencies = [
"syn 2.0.31",
]
[[package]]
name = "serde_spanned"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
dependencies = [
"serde",
]
[[package]]
name = "sha1"
version = "0.10.5"
@ -1977,6 +2143,12 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380"
[[package]]
name = "signal-hook"
version = "0.3.17"
@ -2052,7 +2224,7 @@ dependencies = [
"input",
"lazy_static",
"libc",
"libloading",
"libloading 0.8.0",
"libseat",
"nix 0.26.4",
"once_cell",
@ -2175,6 +2347,25 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "system-deps"
version = "6.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30c2de8a4d8f4b823d634affc9cd2a74ec98c53a756f317e529a48046cbf71f3"
dependencies = [
"cfg-expr",
"heck",
"pkg-config",
"toml",
"version-compare",
]
[[package]]
name = "target-lexicon"
version = "0.12.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a"
[[package]]
name = "tempfile"
version = "3.8.0"
@ -2269,11 +2460,26 @@ dependencies = [
"time-core",
]
[[package]]
name = "toml"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de0a3ab2091e52d7299a39d098e200114a972df0a7724add02a273aa9aada592"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
@ -2282,6 +2488,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap 2.0.0",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
@ -2437,6 +2645,12 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "version-compare"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29"
[[package]]
name = "version_check"
version = "0.9.4"

View File

@ -16,6 +16,7 @@ keyframe = { version = "1.1.1", default-features = false }
knuffel = "3.2.0"
logind-zbus = "3.1.2"
miette = { version = "5.10.0", features = ["fancy"] }
pipewire = "0.7.2"
portable-atomic = { version = "1.4.3", default-features = false, features = ["float"] }
profiling = "1.0.10"
sd-notify = "0.4.1"
@ -26,9 +27,11 @@ tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
tracy-client = { version = "0.15", default-features = false }
xcursor = "0.3.4"
zbus = { version = "3.14.1" }
async-io = "1.13.0"
[dependencies.smithay]
git = "https://github.com/Smithay/smithay.git"
# path = "../smithay"
default-features = false
features = [
"backend_drm",
@ -48,6 +51,7 @@ features = [
[dependencies.smithay-drm-extras]
git = "https://github.com/Smithay/smithay.git"
# path = "../smithay/smithay-drm-extras"
[features]
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client/default"]

View File

@ -1,3 +1,8 @@
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use smithay::backend::allocator::gbm::GbmDevice;
use smithay::backend::drm::DrmDeviceFd;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::output::Output;
use smithay::wayland::dmabuf::DmabufFeedback;
@ -79,6 +84,20 @@ impl Backend {
}
}
pub fn connectors(&self) -> Arc<Mutex<HashMap<String, Output>>> {
match self {
Backend::Tty(tty) => tty.connectors(),
Backend::Winit(winit) => winit.connectors(),
}
}
pub fn gbm_device(&self) -> Option<GbmDevice<DrmDeviceFd>> {
match self {
Backend::Tty(tty) => tty.gbm_device(),
Backend::Winit(_) => None,
}
}
pub fn tty(&mut self) -> &mut Tty {
if let Self::Tty(v) = self {
v

View File

@ -1,6 +1,7 @@
use std::collections::{HashMap, HashSet};
use std::os::fd::FromRawFd;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, Arc};
use std::time::Duration;
use anyhow::{anyhow, Context};
@ -44,6 +45,7 @@ pub struct Tty {
udev_dispatcher: Dispatcher<'static, UdevBackend, LoopData>,
primary_gpu_path: PathBuf,
output_device: Option<OutputDevice>,
connectors: Arc<Mutex<HashMap<String, Output>>>,
}
type GbmDrmCompositor = DrmCompositor<
@ -236,6 +238,7 @@ impl Tty {
udev_dispatcher,
primary_gpu_path,
output_device: None,
connectors: Arc::new(Mutex::new(HashMap::new())),
}
}
@ -549,6 +552,11 @@ impl Tty {
tracy_client::internal::create_frame_name(format!("vblank on {output_name}\0").leak())
};
self.connectors
.lock()
.unwrap()
.insert(output_name.clone(), output.clone());
let surface = Surface {
name: output_name,
compositor,
@ -574,10 +582,10 @@ impl Tty {
debug!("disconnecting connector: {connector:?}");
let device = self.output_device.as_mut().unwrap();
if device.surfaces.remove(&crtc).is_none() {
debug!("crts wasn't enabled");
let Some(surface) = device.surfaces.remove(&crtc) else {
debug!("crtc wasn't enabled");
return;
}
};
let output = niri
.global_space
@ -590,6 +598,8 @@ impl Tty {
.clone();
niri.remove_output(&output);
self.connectors.lock().unwrap().remove(&surface.name);
}
pub fn seat_name(&self) -> String {
@ -680,6 +690,14 @@ impl Tty {
pub fn dmabuf_state(&mut self) -> &mut DmabufState {
&mut self.output_device.as_mut().unwrap().dmabuf_state
}
pub fn connectors(&self) -> Arc<Mutex<HashMap<String, Output>>> {
self.connectors.clone()
}
pub fn gbm_device(&self) -> Option<GbmDevice<DrmDeviceFd>> {
self.output_device.as_ref().map(|d| d.gbm.clone())
}
}
fn refresh_interval(mode: DrmMode) -> Duration {

View File

@ -1,3 +1,5 @@
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use smithay::backend::renderer::damage::OutputDamageTracker;
@ -21,6 +23,7 @@ pub struct Winit {
output: Output,
backend: WinitGraphicsBackend<GlesRenderer>,
damage_tracker: OutputDamageTracker,
connectors: Arc<Mutex<HashMap<String, Output>>>,
}
impl Winit {
@ -53,6 +56,11 @@ impl Winit {
);
output.set_preferred(mode);
let connectors = Arc::new(Mutex::new(HashMap::from([(
"winit".to_owned(),
output.clone(),
)])));
let damage_tracker = OutputDamageTracker::from_output(&output);
let timer = Timer::immediate();
@ -99,6 +107,7 @@ impl Winit {
output,
backend,
damage_tracker,
connectors,
}
}
@ -162,4 +171,8 @@ impl Winit {
let renderer = self.backend.renderer();
renderer.set_debug_flags(renderer.debug_flags() ^ DebugFlags::TINT);
}
pub fn connectors(&self) -> Arc<Mutex<HashMap<String, Output>>> {
self.connectors.clone()
}
}

View File

@ -128,12 +128,15 @@ pub enum Action {
pub struct DebugConfig {
#[knuffel(child, unwrap(argument), default = 1.)]
pub animation_slowdown: f64,
#[knuffel(child)]
pub screen_cast_in_non_session_instances: bool,
}
impl Default for DebugConfig {
fn default() -> Self {
Self {
animation_slowdown: 1.,
screen_cast_in_non_session_instances: false,
}
}
}
@ -308,6 +311,7 @@ mod tests {
]),
debug: DebugConfig {
animation_slowdown: 2.,
..Default::default()
},
},
);

View File

@ -1 +1,3 @@
pub mod mutter_display_config;
pub mod mutter_screen_cast;
pub mod mutter_service_channel;

View File

@ -0,0 +1,88 @@
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use serde::Serialize;
use smithay::output::Output;
use zbus::zvariant::{OwnedValue, Type};
use zbus::{dbus_interface, fdo};
pub struct DisplayConfig {
connectors: Arc<Mutex<HashMap<String, Output>>>,
}
#[derive(Serialize, Type)]
pub struct Monitor {
names: (String, String, String, String),
modes: Vec<Mode>,
properties: HashMap<String, OwnedValue>,
}
#[derive(Serialize, Type)]
pub struct Mode {
id: String,
width: i32,
height: i32,
refresh_rate: f64,
preferred_scale: f64,
supported_scales: Vec<f64>,
properties: HashMap<String, OwnedValue>,
}
#[derive(Serialize, Type)]
pub struct LogicalMonitor {
x: i32,
y: i32,
scale: f64,
transform: u32,
is_primary: bool,
monitors: Vec<(String, String, String, String)>,
properties: HashMap<String, OwnedValue>,
}
#[dbus_interface(name = "org.gnome.Mutter.DisplayConfig")]
impl DisplayConfig {
async fn get_current_state(
&self,
) -> fdo::Result<(
u32,
Vec<Monitor>,
Vec<LogicalMonitor>,
HashMap<String, OwnedValue>,
)> {
// Construct the DBus response.
let monitors: Vec<Monitor> = self
.connectors
.lock()
.unwrap()
.keys()
.map(|c| Monitor {
names: (c.clone(), String::new(), String::new(), String::new()),
modes: vec![],
properties: HashMap::new(),
})
.collect();
let logical_monitors = monitors
.iter()
.map(|m| LogicalMonitor {
x: 0,
y: 0,
scale: 1.,
transform: 0,
is_primary: false,
monitors: vec![m.names.clone()],
properties: HashMap::new(),
})
.collect();
Ok((0, monitors, logical_monitors, HashMap::new()))
}
// FIXME: monitors-changed signal.
}
impl DisplayConfig {
pub fn new(connectors: Arc<Mutex<HashMap<String, Output>>>) -> Self {
Self { connectors }
}
}

View File

@ -0,0 +1,256 @@
use std::collections::HashMap;
use std::mem;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use serde::Deserialize;
use smithay::output::Output;
use smithay::reexports::calloop;
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, Type, Value};
use zbus::{dbus_interface, fdo, InterfaceRef, ObjectServer, SignalContext};
#[derive(Clone)]
pub struct ScreenCast {
connectors: Arc<Mutex<HashMap<String, Output>>>,
to_niri: calloop::channel::Sender<ToNiriMsg>,
sessions: Arc<Mutex<Vec<(Session, InterfaceRef<Session>)>>>,
}
#[derive(Clone)]
pub struct Session {
id: usize,
connectors: Arc<Mutex<HashMap<String, Output>>>,
to_niri: calloop::channel::Sender<ToNiriMsg>,
streams: Arc<Mutex<Vec<(Stream, InterfaceRef<Stream>)>>>,
}
#[derive(Debug, Default, Deserialize, Type, Clone, Copy)]
pub enum CursorMode {
#[default]
Hidden = 0,
Embedded = 1,
Metadata = 2,
}
#[derive(Debug, DeserializeDict, Type)]
#[zvariant(signature = "dict")]
struct RecordMonitorProperties {
#[zvariant(rename = "cursor-mode")]
cursor_mode: Option<CursorMode>,
#[zvariant(rename = "is-recording")]
_is_recording: Option<bool>,
}
#[derive(Clone)]
pub struct Stream {
output: Output,
cursor_mode: CursorMode,
was_started: Arc<AtomicBool>,
to_niri: calloop::channel::Sender<ToNiriMsg>,
}
pub enum ToNiriMsg {
StartCast {
session_id: usize,
output: Output,
cursor_mode: CursorMode,
signal_ctx: SignalContext<'static>,
},
StopCast {
session_id: usize,
},
}
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast")]
impl ScreenCast {
async fn create_session(
&self,
#[zbus(object_server)] server: &ObjectServer,
properties: HashMap<&str, Value<'_>>,
) -> fdo::Result<OwnedObjectPath> {
if properties.contains_key("remote-desktop-session-id") {
return Err(fdo::Error::Failed(
"there are no remote desktop sessions".to_owned(),
));
}
static NUMBER: AtomicUsize = AtomicUsize::new(0);
let session_id = NUMBER.fetch_add(1, Ordering::SeqCst);
let path = format!("/org/gnome/Mutter/ScreenCast/Session/u{}", session_id);
let path = OwnedObjectPath::try_from(path).unwrap();
let session = Session::new(session_id, self.connectors.clone(), self.to_niri.clone());
match server.at(&path, session.clone()).await {
Ok(true) => {
let iface = server.interface(&path).await.unwrap();
self.sessions.lock().unwrap().push((session, iface));
}
Ok(false) => return Err(fdo::Error::Failed("session path already exists".to_owned())),
Err(err) => {
return Err(fdo::Error::Failed(format!(
"error creating session object: {err:?}"
)))
}
}
Ok(path)
}
#[dbus_interface(property)]
async fn version(&self) -> i32 {
4
}
}
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast.Session")]
impl Session {
async fn start(&self) {
debug!("start");
for (stream, iface) in &*self.streams.lock().unwrap() {
stream.start(self.id, iface.signal_context().clone());
}
}
pub async fn stop(
&self,
#[zbus(object_server)] server: &ObjectServer,
#[zbus(signal_context)] ctxt: SignalContext<'_>,
) {
debug!("stop");
Session::closed(&ctxt).await.unwrap();
if let Err(err) = self.to_niri.send(ToNiriMsg::StopCast {
session_id: self.id,
}) {
warn!("error sending StopCast to niri: {err:?}");
}
let streams = mem::take(&mut *self.streams.lock().unwrap());
for (_, iface) in streams.iter() {
server
.remove::<Stream, _>(iface.signal_context().path())
.await
.unwrap();
}
server.remove::<Session, _>(ctxt.path()).await.unwrap();
}
async fn record_monitor(
&mut self,
#[zbus(object_server)] server: &ObjectServer,
connector: &str,
properties: RecordMonitorProperties,
) -> fdo::Result<OwnedObjectPath> {
debug!(connector, ?properties, "record_monitor");
let Some(output) = self.connectors.lock().unwrap().get(connector).cloned() else {
return Err(fdo::Error::Failed("no such monitor".to_owned()));
};
static NUMBER: AtomicUsize = AtomicUsize::new(0);
let path = format!(
"/org/gnome/Mutter/ScreenCast/Stream/u{}",
NUMBER.fetch_add(1, Ordering::SeqCst)
);
let path = OwnedObjectPath::try_from(path).unwrap();
let cursor_mode = properties.cursor_mode.unwrap_or_default();
let stream = Stream::new(output, cursor_mode, self.to_niri.clone());
match server.at(&path, stream.clone()).await {
Ok(true) => {
let iface = server.interface(&path).await.unwrap();
self.streams.lock().unwrap().push((stream, iface));
}
Ok(false) => return Err(fdo::Error::Failed("stream path already exists".to_owned())),
Err(err) => {
return Err(fdo::Error::Failed(format!(
"error creating stream object: {err:?}"
)))
}
}
Ok(path)
}
#[dbus_interface(signal)]
async fn closed(ctxt: &SignalContext<'_>) -> zbus::Result<()>;
}
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast.Stream")]
impl Stream {
#[dbus_interface(signal)]
pub async fn pipe_wire_stream_added(ctxt: &SignalContext<'_>, node_id: u32)
-> zbus::Result<()>;
}
impl ScreenCast {
pub fn new(
connectors: Arc<Mutex<HashMap<String, Output>>>,
to_niri: calloop::channel::Sender<ToNiriMsg>,
) -> Self {
Self {
connectors,
to_niri,
sessions: Arc::new(Mutex::new(vec![])),
}
}
}
impl Session {
pub fn new(
id: usize,
connectors: Arc<Mutex<HashMap<String, Output>>>,
to_niri: calloop::channel::Sender<ToNiriMsg>,
) -> Self {
Self {
id,
connectors,
streams: Arc::new(Mutex::new(vec![])),
to_niri,
}
}
}
impl Drop for Session {
fn drop(&mut self) {
let _ = self.to_niri.send(ToNiriMsg::StopCast {
session_id: self.id,
});
}
}
impl Stream {
pub fn new(
output: Output,
cursor_mode: CursorMode,
to_niri: calloop::channel::Sender<ToNiriMsg>,
) -> Self {
Self {
output,
cursor_mode,
was_started: Arc::new(AtomicBool::new(false)),
to_niri,
}
}
fn start(&self, session_id: usize, ctxt: SignalContext<'static>) {
if self.was_started.load(Ordering::SeqCst) {
return;
}
let msg = ToNiriMsg::StartCast {
session_id,
output: self.output.clone(),
cursor_mode: self.cursor_mode,
signal_ctx: ctxt,
};
if let Err(err) = self.to_niri.send(msg) {
warn!("error sending StartCast to niri: {err:?}");
}
}
}

View File

@ -10,6 +10,7 @@ mod handlers;
mod input;
mod layout;
mod niri;
mod pw_utils;
mod utils;
use std::env;

View File

@ -8,6 +8,7 @@ use std::{env, thread};
use anyhow::Context;
use directories::UserDirs;
use sd_notify::NotifyState;
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::surface::{
render_elements_from_surface_tree, WaylandSurfaceRenderElement,
@ -16,7 +17,7 @@ use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderE
use smithay::backend::renderer::element::{
render_elements, AsRenderElements, Element, RenderElement, RenderElementStates,
};
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
use smithay::backend::renderer::gles::{GlesMapping, GlesRenderer, GlesTexture};
use smithay::backend::renderer::{Bind, ExportMem, Frame, ImportAll, Offscreen, Renderer};
use smithay::desktop::utils::{
send_dmabuf_feedback_surface_tree, send_frames_surface_tree,
@ -31,7 +32,7 @@ use smithay::input::pointer::{CursorImageAttributes, CursorImageStatus, MotionEv
use smithay::input::{Seat, SeatState};
use smithay::output::Output;
use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::{Idle, Interest, LoopHandle, LoopSignal, Mode, PostAction};
use smithay::reexports::calloop::{self, Idle, Interest, LoopHandle, LoopSignal, Mode, PostAction};
use smithay::reexports::nix::libc::CLOCK_MONOTONIC;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::WmCapabilities;
use smithay::reexports::wayland_server::backend::{
@ -40,7 +41,7 @@ use smithay::reexports::wayland_server::backend::{
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::reexports::wayland_server::{Display, DisplayHandle};
use smithay::utils::{
IsAlive, Logical, Physical, Point, Rectangle, Scale, Transform, SERIAL_COUNTER,
IsAlive, Logical, Physical, Point, Rectangle, Scale, Size, Transform, SERIAL_COUNTER,
};
use smithay::wayland::compositor::{with_states, CompositorClientState, CompositorState};
use smithay::wayland::data_device::DataDeviceState;
@ -57,9 +58,12 @@ use time::OffsetDateTime;
use crate::backend::{Backend, Tty, Winit};
use crate::config::Config;
use crate::dbus::mutter_display_config::DisplayConfig;
use crate::dbus::mutter_screen_cast::{self, ScreenCast, ToNiriMsg};
use crate::dbus::mutter_service_channel::ServiceChannel;
use crate::frame_clock::FrameClock;
use crate::layout::{MonitorRenderElement, MonitorSet};
use crate::pw_utils::{Cast, PipeWire};
use crate::utils::{center, get_monotonic_time, load_default_cursor};
use crate::LoopData;
@ -102,6 +106,11 @@ pub struct Niri {
pub zbus_conn: Option<zbus::blocking::Connection>,
pub inhibit_power_key_fd: Option<zbus::zvariant::OwnedFd>,
pub screen_cast: ScreenCast,
// Casts are dropped before PipeWire to prevent a double-free (yay).
pub casts: Vec<Cast>,
pub pipewire: Option<PipeWire>,
}
pub struct OutputState {
@ -137,13 +146,7 @@ impl State {
Backend::Tty(Tty::new(event_loop.clone()))
};
let mut niri = Niri::new(
&config,
event_loop,
stop_signal,
display,
backend.seat_name(),
);
let mut niri = Niri::new(&config, event_loop, stop_signal, display, &backend);
backend.init(&mut niri);
Self {
@ -195,7 +198,7 @@ impl Niri {
event_loop: LoopHandle<'static, LoopData>,
stop_signal: LoopSignal,
display: &mut Display<State>,
seat_name: String,
backend: &Backend,
) -> Self {
let display_handle = display.handle();
@ -215,7 +218,7 @@ impl Niri {
let presentation_state =
PresentationState::new::<State>(&display_handle, CLOCK_MONOTONIC as u32);
let mut seat: Seat<State> = seat_state.new_wl_seat(&display_handle, seat_name);
let mut seat: Seat<State> = seat_state.new_wl_seat(&display_handle, backend.seat_name());
let xkb = XkbConfig {
rules: &config.input.keyboard.xkb.rules,
model: &config.input.keyboard.xkb.model,
@ -246,6 +249,102 @@ impl Niri {
socket_name.to_string_lossy()
);
let pipewire = match PipeWire::new(&event_loop) {
Ok(pipewire) => Some(pipewire),
Err(err) => {
warn!("error starting PipeWire: {err:?}");
None
}
};
let (to_niri, from_screen_cast) = calloop::channel::channel();
event_loop
.insert_source(from_screen_cast, {
let to_niri = to_niri.clone();
move |event, _, data| match event {
calloop::channel::Event::Msg(msg) => match msg {
ToNiriMsg::StartCast {
session_id,
output,
cursor_mode,
signal_ctx,
} => {
let _span = tracy_client::span!("StartCast");
debug!(session_id, "StartCast");
let gbm = match data.state.backend.gbm_device() {
Some(gbm) => gbm,
None => {
debug!("no GBM device available");
return;
}
};
let pw = data.state.niri.pipewire.as_ref().unwrap();
match pw.start_cast(
to_niri.clone(),
gbm,
session_id,
output,
cursor_mode,
signal_ctx,
) {
Ok(cast) => {
data.state.niri.casts.push(cast);
}
Err(err) => {
warn!("error starting screencast: {err:?}");
if let Err(err) =
to_niri.send(ToNiriMsg::StopCast { session_id })
{
warn!("error sending StopCast to niri: {err:?}");
}
}
}
}
ToNiriMsg::StopCast { session_id } => {
let _span = tracy_client::span!("StopCast");
debug!(session_id, "StopCast");
for i in (0..data.state.niri.casts.len()).rev() {
let cast = &data.state.niri.casts[i];
if cast.session_id != session_id {
continue;
}
let cast = data.state.niri.casts.swap_remove(i);
if let Err(err) = cast.stream.disconnect() {
warn!("error disconnecting stream: {err:?}");
}
}
let server =
data.state.niri.zbus_conn.as_ref().unwrap().object_server();
let path =
format!("/org/gnome/Mutter/ScreenCast/Session/u{}", session_id);
if let Ok(iface) =
server.interface::<_, mutter_screen_cast::Session>(path)
{
let _span = tracy_client::span!("invoking Session::stop");
async_io::block_on(async move {
iface
.get()
.stop(&server, iface.signal_context().clone())
.await
});
}
}
},
calloop::channel::Event::Closed => (),
}
})
.unwrap();
let screen_cast = ScreenCast::new(backend.connectors(), to_niri);
let mut zbus_conn = None;
let mut inhibit_power_key_fd = None;
if std::env::var_os("NOTIFY_SOCKET").is_some() {
@ -278,7 +377,7 @@ impl Niri {
}
// Set up zbus, make sure it happens before anything might want it.
let conn = zbus::blocking::ConnectionBuilder::session()
let mut conn = zbus::blocking::ConnectionBuilder::session()
.unwrap()
.name("org.gnome.Mutter.ServiceChannel")
.unwrap()
@ -286,9 +385,24 @@ impl Niri {
"/org/gnome/Mutter/ServiceChannel",
ServiceChannel::new(display_handle.clone()),
)
.unwrap()
.build()
.unwrap();
if pipewire.is_some() && !config.debug.screen_cast_in_non_session_instances {
conn = conn
.name("org.gnome.Mutter.ScreenCast")
.unwrap()
.serve_at("/org/gnome/Mutter/ScreenCast", screen_cast.clone())
.unwrap()
.name("org.gnome.Mutter.DisplayConfig")
.unwrap()
.serve_at(
"/org/gnome/Mutter/DisplayConfig",
DisplayConfig::new(backend.connectors()),
)
.unwrap();
}
let conn = conn.build().unwrap();
zbus_conn = Some(conn);
// Notify systemd we're ready.
@ -321,6 +435,23 @@ impl Niri {
warn!("error inhibiting power key: {err:?}");
}
}
} else if pipewire.is_some() && config.debug.screen_cast_in_non_session_instances {
let conn = zbus::blocking::ConnectionBuilder::session()
.unwrap()
.name("org.gnome.Mutter.ScreenCast")
.unwrap()
.serve_at("/org/gnome/Mutter/ScreenCast", screen_cast.clone())
.unwrap()
.name("org.gnome.Mutter.DisplayConfig")
.unwrap()
.serve_at(
"/org/gnome/Mutter/DisplayConfig",
DisplayConfig::new(backend.connectors()),
)
.unwrap()
.build()
.unwrap();
zbus_conn = Some(conn);
}
let display_source = Generic::new(
@ -364,6 +495,9 @@ impl Niri {
zbus_conn,
inhibit_power_key_fd,
screen_cast,
pipewire,
casts: vec![],
}
}
@ -720,6 +854,9 @@ impl Niri {
// Send the frame callbacks.
self.send_frame_callbacks(output);
// Render and send to PipeWire screencast streams.
self.send_for_screen_cast(backend, output, &elements, presentation_time);
}
fn send_dmabuf_feedbacks(&self, output: &Output, feedback: &DmabufFeedback) {
@ -827,6 +964,63 @@ impl Niri {
feedback
}
fn send_for_screen_cast(
&mut self,
backend: &mut Backend,
output: &Output,
elements: &[OutputRenderElements<GlesRenderer>],
presentation_time: Duration,
) {
let _span = tracy_client::span!("Niri::send_for_screen_cast");
let size = output.current_mode().unwrap().size;
for cast in &mut self.casts {
if !cast.is_active.get() {
continue;
}
if &cast.output != output {
continue;
}
let last = cast.last_frame_time;
let min = cast.min_time_between_frames.get();
if !last.is_zero() && presentation_time - last < min {
trace!(
"skipping frame because it is too soon \
last={last:?} now={presentation_time:?} diff={:?} < min={min:?}",
presentation_time - last,
);
continue;
}
{
let mut buffer = match cast.stream.dequeue_buffer() {
Some(buffer) => buffer,
None => {
warn!("no available buffer in pw stream, skipping frame");
continue;
}
};
let data = &mut buffer.datas_mut()[0];
let fd = data.as_raw().fd as i32;
let dmabuf = cast.dmabufs.borrow()[&fd].clone();
// FIXME: Hidden / embedded / metadata cursor
render_to_dmabuf(backend.renderer(), dmabuf, size, elements).unwrap();
let maxsize = data.as_raw().maxsize;
let chunk = data.chunk_mut();
*chunk.size_mut() = maxsize;
*chunk.stride_mut() = maxsize as i32 / size.h;
}
cast.last_frame_time = presentation_time;
}
}
pub fn screenshot(
&mut self,
renderer: &mut GlesRenderer,
@ -835,39 +1029,9 @@ impl Niri {
let _span = tracy_client::span!("Niri::screenshot");
let size = output.current_mode().unwrap().size;
let output_rect = Rectangle::from_loc_and_size((0, 0), size);
let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal);
let fourcc = Fourcc::Abgr8888;
let texture: GlesTexture = renderer
.create_buffer(fourcc, buffer_size)
.context("error creating texture")?;
let elements = self.render(renderer, output);
renderer.bind(texture).context("error binding texture")?;
let mut frame = renderer
.render(size, Transform::Normal)
.context("error starting frame")?;
frame
.clear([0.1, 0.1, 0.1, 1.], &[output_rect])
.context("error clearing")?;
for element in elements.into_iter().rev() {
let src = element.src();
let dst = element.geometry(Scale::from(1.));
element
.draw(&mut frame, src, dst, &[output_rect])
.context("error drawing element")?;
}
let sync_point = frame.finish().context("error finishing frame")?;
sync_point.wait();
let mapping = renderer
.copy_framebuffer(Rectangle::from_loc_and_size((0, 0), buffer_size), fourcc)
.context("error copying framebuffer")?;
let mapping = render_and_download(renderer, size, &elements).context("error rendering")?;
let copy = renderer
.map_texture(&mapping)
.context("error mapping texture")?;
@ -928,3 +1092,76 @@ impl ClientData for ClientState {
fn initialized(&self, _client_id: ClientId) {}
fn disconnected(&self, _client_id: ClientId, _reason: DisconnectReason) {}
}
fn render_and_download(
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
elements: &[OutputRenderElements<GlesRenderer>],
) -> anyhow::Result<GlesMapping> {
let _span = tracy_client::span!("render_and_download");
let output_rect = Rectangle::from_loc_and_size((0, 0), size);
let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal);
let fourcc = Fourcc::Abgr8888;
let texture: GlesTexture = renderer
.create_buffer(fourcc, buffer_size)
.context("error creating texture")?;
renderer.bind(texture).context("error binding texture")?;
let mut frame = renderer
.render(size, Transform::Normal)
.context("error starting frame")?;
frame
.clear([0.1, 0.1, 0.1, 1.], &[output_rect])
.context("error clearing")?;
for element in elements.iter().rev() {
let src = element.src();
let dst = element.geometry(Scale::from(1.));
element
.draw(&mut frame, src, dst, &[output_rect])
.context("error drawing element")?;
}
let sync_point = frame.finish().context("error finishing frame")?;
sync_point.wait();
let mapping = renderer
.copy_framebuffer(Rectangle::from_loc_and_size((0, 0), buffer_size), fourcc)
.context("error copying framebuffer")?;
Ok(mapping)
}
fn render_to_dmabuf(
renderer: &mut GlesRenderer,
dmabuf: Dmabuf,
size: Size<i32, Physical>,
elements: &[OutputRenderElements<GlesRenderer>],
) -> anyhow::Result<()> {
let _span = tracy_client::span!("render_to_dmabuf");
let output_rect = Rectangle::from_loc_and_size((0, 0), size);
renderer.bind(dmabuf).context("error binding texture")?;
let mut frame = renderer
.render(size, Transform::Normal)
.context("error starting frame")?;
frame
.clear([0.1, 0.1, 0.1, 1.], &[output_rect])
.context("error clearing")?;
for element in elements.iter().rev() {
let src = element.src();
let dst = element.geometry(Scale::from(1.));
element
.draw(&mut frame, src, dst, &[output_rect])
.context("error drawing element")?;
}
let _sync_point = frame.finish().context("error finishing frame")?;
Ok(())
}

382
src/pw_utils.rs Normal file
View File

@ -0,0 +1,382 @@
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::io::Cursor;
use std::mem;
use std::os::fd::AsRawFd;
use std::rc::Rc;
use std::time::Duration;
use anyhow::Context as _;
use pipewire::spa::data::DataType;
use pipewire::spa::format::{FormatProperties, MediaSubtype, MediaType};
use pipewire::spa::param::format_utils::parse_format;
use pipewire::spa::param::video::{VideoFormat, VideoInfoRaw};
use pipewire::spa::param::ParamType;
use pipewire::spa::pod::serialize::PodSerializer;
use pipewire::spa::pod::{self, ChoiceValue, Pod, Property, PropertyFlags};
use pipewire::spa::sys::*;
use pipewire::spa::utils::{Choice, ChoiceEnum, ChoiceFlags, Fraction, Rectangle, SpaTypes};
use pipewire::spa::Direction;
use pipewire::stream::{Stream, StreamFlags, StreamListener, StreamState};
use pipewire::{Context, Core, MainLoop, Properties};
use smithay::backend::allocator::dmabuf::{AsDmabuf, Dmabuf};
use smithay::backend::allocator::gbm::{GbmBufferFlags, GbmDevice};
use smithay::backend::allocator::Fourcc;
use smithay::backend::drm::DrmDeviceFd;
use smithay::output::Output;
use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::{self, Interest, LoopHandle, Mode, PostAction};
use smithay::reexports::gbm::Modifier;
use zbus::SignalContext;
use crate::dbus::mutter_screen_cast::{self, CursorMode, ToNiriMsg};
use crate::LoopData;
pub struct PipeWire {
_context: Context<MainLoop>,
pub core: Core,
}
pub struct Cast {
pub session_id: usize,
pub stream: Rc<Stream>,
_listener: StreamListener<()>,
pub is_active: Rc<Cell<bool>>,
pub output: Output,
pub cursor_mode: CursorMode,
pub last_frame_time: Duration,
pub min_time_between_frames: Rc<Cell<Duration>>,
pub dmabufs: Rc<RefCell<HashMap<i32, Dmabuf>>>,
}
impl PipeWire {
pub fn new(event_loop: &LoopHandle<'static, LoopData>) -> anyhow::Result<Self> {
let main_loop = MainLoop::new().context("error creating MainLoop")?;
let context = Context::new(&main_loop).context("error creating Context")?;
let core = context.connect(None).context("error creating Core")?;
let listener = core
.add_listener_local()
.error(|id, seq, res, message| {
warn!(id, seq, res, message, "pw error");
})
.register();
mem::forget(listener);
let generic = Generic::new(main_loop.fd().as_raw_fd(), Interest::READ, Mode::Level);
event_loop
.insert_source(generic, move |_, _, _| {
let _span = tracy_client::span!("pipewire iteration");
main_loop.iterate(Duration::ZERO);
Ok(PostAction::Continue)
})
.unwrap();
Ok(Self {
_context: context,
core,
})
}
pub fn start_cast(
&self,
to_niri: calloop::channel::Sender<ToNiriMsg>,
gbm: GbmDevice<DrmDeviceFd>,
session_id: usize,
output: Output,
cursor_mode: CursorMode,
signal_ctx: SignalContext<'static>,
) -> anyhow::Result<Cast> {
let _span = tracy_client::span!("PipeWire::start_cast");
let stop_cast = move || {
if let Err(err) = to_niri.send(ToNiriMsg::StopCast { session_id }) {
warn!("error sending StopCast to niri: {err:?}");
}
};
let mode = output.current_mode().unwrap();
let size = mode.size;
let refresh = mode.refresh;
let stream = Stream::new(&self.core, "niri-screen-cast-src", Properties::new())
.context("error creating Stream")?;
// Like in good old wayland-rs times...
let stream = Rc::new(stream);
let node_id = Rc::new(Cell::new(None));
let is_active = Rc::new(Cell::new(false));
let min_time_between_frames = Rc::new(Cell::new(Duration::ZERO));
let dmabufs = Rc::new(RefCell::new(HashMap::new()));
let listener = stream
.add_local_listener_with_user_data(())
.state_changed({
let stream = stream.clone();
let is_active = is_active.clone();
let stop_cast = stop_cast.clone();
move |old, new| {
debug!("pw stream: state changed: {old:?} -> {new:?}");
match new {
StreamState::Paused => {
if node_id.get().is_none() {
let id = stream.node_id();
node_id.set(Some(id));
debug!("pw stream: sending signal with {id}");
let _span = tracy_client::span!("sending PipeWireStreamAdded");
async_io::block_on(async {
let res = mutter_screen_cast::Stream::pipe_wire_stream_added(
&signal_ctx,
id,
)
.await;
if let Err(err) = res {
warn!("error sending PipeWireStreamAdded: {err:?}");
stop_cast();
}
});
}
is_active.set(false);
}
StreamState::Error(_) => {
if is_active.get() {
is_active.set(false);
stop_cast();
}
}
StreamState::Unconnected => (),
StreamState::Connecting => (),
StreamState::Streaming => {
is_active.set(true);
}
}
}
})
.param_changed({
let min_time_between_frames = min_time_between_frames.clone();
move |stream, id, _data, pod| {
let id = ParamType::from_raw(id);
trace!(?id, "pw stream: param_changed");
if id != ParamType::Format {
return;
}
let Some(pod) = pod else { return };
let (m_type, m_subtype) = match parse_format(pod) {
Ok(x) => x,
Err(err) => {
warn!("pw stream: error parsing format: {err:?}");
return;
}
};
if m_type != MediaType::Video || m_subtype != MediaSubtype::Raw {
return;
}
let mut format = VideoInfoRaw::new();
format.parse(pod).unwrap();
trace!("pw stream: got format = {format:?}");
let max_frame_rate = format.max_framerate();
// Subtract 0.5 ms to improve edge cases when equal to refresh rate.
let min_frame_time = Duration::from_secs_f64(
max_frame_rate.denom as f64 / max_frame_rate.num as f64,
) - Duration::from_micros(500);
min_time_between_frames.set(min_frame_time);
const BPP: u32 = 4;
let stride = format.size().width * BPP;
let size = stride * format.size().height;
let o1 = pod::object!(
SpaTypes::ObjectParamBuffers,
ParamType::Buffers,
Property::new(
SPA_PARAM_BUFFERS_buffers,
pod::Value::Choice(ChoiceValue::Int(Choice(
ChoiceFlags::empty(),
ChoiceEnum::Range {
default: 16,
min: 2,
max: 16
}
))),
),
Property::new(SPA_PARAM_BUFFERS_blocks, pod::Value::Int(1)),
Property::new(SPA_PARAM_BUFFERS_size, pod::Value::Int(size as i32)),
Property::new(SPA_PARAM_BUFFERS_stride, pod::Value::Int(stride as i32)),
Property::new(SPA_PARAM_BUFFERS_align, pod::Value::Int(16)),
Property::new(
SPA_PARAM_BUFFERS_dataType,
pod::Value::Choice(ChoiceValue::Int(Choice(
ChoiceFlags::empty(),
ChoiceEnum::Flags {
default: 1 << DataType::DmaBuf.as_raw(),
flags: vec![1 << DataType::DmaBuf.as_raw()],
},
))),
),
);
// FIXME: Hidden / embedded / metadata cursor
// let o2 = pod::object!(
// SpaTypes::ObjectParamMeta,
// ParamType::Meta,
// Property::new(SPA_PARAM_META_type,
// pod::Value::Id(Id(SPA_META_Header))),
// Property::new(
// SPA_PARAM_META_size,
// pod::Value::Int(size_of::<spa_meta_header>() as i32)
// ),
// );
let mut b1 = vec![];
// let mut b2 = vec![];
let mut params = [
make_pod(&mut b1, o1).as_raw_ptr().cast_const(),
// make_pod(&mut b2, o2).as_raw_ptr().cast_const(),
];
stream.update_params(&mut params).unwrap();
}
})
.add_buffer({
let dmabufs = dmabufs.clone();
let stop_cast = stop_cast.clone();
move |buffer| {
trace!("pw stream: add_buffer");
unsafe {
let spa_buffer = (*buffer).buffer;
let spa_data = (*spa_buffer).datas;
assert!((*spa_buffer).n_datas > 0);
assert!((*spa_data).type_ & (1 << DataType::DmaBuf.as_raw()) > 0);
let bo = match gbm.create_buffer_object::<()>(
size.w as u32,
size.h as u32,
Fourcc::Xrgb8888,
GbmBufferFlags::RENDERING | GbmBufferFlags::LINEAR,
) {
Ok(bo) => bo,
Err(err) => {
warn!("error creating GBM buffer object: {err:?}");
stop_cast();
return;
}
};
let dmabuf = match bo.export() {
Ok(dmabuf) => dmabuf,
Err(err) => {
warn!("error exporting GBM buffer object as dmabuf: {err:?}");
stop_cast();
return;
}
};
let fd = dmabuf.handles().next().unwrap().as_raw_fd();
(*spa_data).type_ = DataType::DmaBuf.as_raw();
(*spa_data).maxsize = dmabuf.strides().next().unwrap() * size.h as u32;
(*spa_data).fd = fd as i64;
(*spa_data).flags = SPA_DATA_FLAG_READWRITE;
assert!(dmabufs.borrow_mut().insert(fd, dmabuf).is_none());
}
}
})
.remove_buffer({
let dmabufs = dmabufs.clone();
move |buffer| {
trace!("pw stream: remove_buffer");
unsafe {
let spa_buffer = (*buffer).buffer;
let spa_data = (*spa_buffer).datas;
assert!((*spa_buffer).n_datas > 0);
let fd = (*spa_data).fd as i32;
dmabufs.borrow_mut().remove(&fd);
}
}
})
.register()
.unwrap();
let object = pod::object!(
SpaTypes::ObjectParamFormat,
ParamType::EnumFormat,
pod::property!(FormatProperties::MediaType, Id, MediaType::Video),
pod::property!(FormatProperties::MediaSubtype, Id, MediaSubtype::Raw),
pod::property!(FormatProperties::VideoFormat, Id, VideoFormat::BGRx),
Property {
key: FormatProperties::VideoModifier.as_raw(),
value: pod::Value::Long(u64::from(Modifier::Invalid) as i64),
flags: PropertyFlags::MANDATORY,
},
pod::property!(
FormatProperties::VideoSize,
Rectangle,
Rectangle {
width: size.w as u32,
height: size.h as u32,
}
),
pod::property!(
FormatProperties::VideoFramerate,
Fraction,
Fraction { num: 0, denom: 1 }
),
pod::property!(
FormatProperties::VideoMaxFramerate,
Choice,
Range,
Fraction,
Fraction {
num: refresh as u32,
denom: 1000
},
Fraction { num: 1, denom: 1 },
Fraction {
num: refresh as u32,
denom: 1000
}
),
);
let mut buffer = vec![];
let mut params = [make_pod(&mut buffer, object)];
stream
.connect(
Direction::Output,
None,
StreamFlags::DRIVER | StreamFlags::ALLOC_BUFFERS,
&mut params,
)
.context("error connecting stream")?;
let cast = Cast {
session_id,
stream,
_listener: listener,
is_active,
output,
cursor_mode,
last_frame_time: Duration::ZERO,
min_time_between_frames,
dmabufs,
};
Ok(cast)
}
}
fn make_pod(buffer: &mut Vec<u8>, object: pod::Object) -> &Pod {
PodSerializer::serialize(Cursor::new(&mut *buffer), &pod::Value::Object(object)).unwrap();
Pod::from_bytes(buffer).unwrap()
}