diff --git a/Cargo.toml b/Cargo.toml
index 9d803a18..b9da5778 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -45,6 +45,8 @@ members = [
"utils/togglerecord",
"utils/tracers",
"utils/uriplaylistbin",
+ "utils/xdgscreencapsrc",
+
"video/cdg",
"video/closedcaption",
diff --git a/README.md b/README.md
index dc107d15..577554d7 100644
--- a/README.md
+++ b/README.md
@@ -145,6 +145,8 @@ You will find the following plugins in this repository:
- `uriplaylistbin`: Helper bin to gaplessly play a list of URIs.
+ - `xdgscreencapsrc`: GStreamer xdg-desktop-portal screen capture plugin
+
## Building
gst-plugins-rs relies on [cargo-c](https://github.com/lu-zero/cargo-c/) to
diff --git a/meson.build b/meson.build
index a069e709..9717c91c 100644
--- a/meson.build
+++ b/meson.build
@@ -205,6 +205,7 @@ plugins = {
'extra-deps': {'cairo-gobject': []},
},
'gopbuffer': {'library': 'libgstgopbuffer'},
+ 'xdgscreencapsrc': {'library': 'libgstxdgscreencapsrc'},
}
if get_option('examples').allowed()
diff --git a/meson_options.txt b/meson_options.txt
index 7d416fa6..3efc47f0 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -47,6 +47,7 @@ option('livesync', type: 'feature', value: 'auto', description: 'Build livesync
option('togglerecord', type: 'feature', value: 'auto', description: 'Build togglerecord plugin')
option('tracers', type: 'feature', value: 'auto', description: 'Build tracers plugin')
option('uriplaylistbin', type: 'feature', value: 'auto', description: 'Build uriplaylistbin plugin')
+option('xdgscreencapsrc', type: 'feature', value: 'auto', description: 'Build xdgscreencapsrc plugin')
# video
option('cdg', type: 'feature', value: 'auto', description: 'Build cdg plugin')
diff --git a/utils/xdgscreencapsrc/Cargo.toml b/utils/xdgscreencapsrc/Cargo.toml
new file mode 100644
index 00000000..a7e54d18
--- /dev/null
+++ b/utils/xdgscreencapsrc/Cargo.toml
@@ -0,0 +1,47 @@
+[package]
+name = "gst-plugin-xdgscreencapsrc"
+version = "0.12.0-alpha.1"
+authors = ["Ruben Gonzalez .
+//
+// SPDX-License-Identifier: MPL-2.0
+#![allow(clippy::non_send_fields_in_send_ty, unused_doc_comments)]
+
+/**
+ * plugin-xdgscreencapsrc:
+ *
+ * Since: plugins-rs-0.12.0
+ */
+use gst::glib;
+
+mod xdgscreencapsrc;
+
+fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
+ xdgscreencapsrc::register(plugin)
+}
+
+gst::plugin_define!(
+ xdgscreencapsrc,
+ env!("CARGO_PKG_DESCRIPTION"),
+ plugin_init,
+ concat!(env!("CARGO_PKG_VERSION"), "-", "COMMIT_ID"),
+ // FIXME: MPL-2.0 is only allowed since 1.18.3 (as unknown) and 1.20 (as known)
+ "MPL",
+ env!("CARGO_PKG_NAME"),
+ env!("CARGO_PKG_NAME"),
+ env!("CARGO_PKG_REPOSITORY"),
+ env!("BUILD_REL_DATE")
+);
diff --git a/utils/xdgscreencapsrc/src/xdgscreencapsrc/imp.rs b/utils/xdgscreencapsrc/src/xdgscreencapsrc/imp.rs
new file mode 100644
index 00000000..57ff4484
--- /dev/null
+++ b/utils/xdgscreencapsrc/src/xdgscreencapsrc/imp.rs
@@ -0,0 +1,333 @@
+// Copyright (C) 2023 Ruben Gonzalez .
+//
+// SPDX-License-Identifier: MPL-2.0
+/**
+ * element-xdgscreencapsrc:
+ * @short_description: Source element wrapping pipewiresrc using xdg-desktop-portal to start a screencast session.
+ *
+ * Based on https://gitlab.gnome.org/-/snippets/19 using https://crates.io/crates/ashpd
+ *
+ * ## Example pipeline
+ * ```bash
+ * gst-launch-1.0 -v xdgscreencapsrc ! videoconvert ! identity silent=false ! gtkwaylandsink
+ * ```
+ *
+ * Since: plugins-rs-0.12.0
+ */
+use gst::glib;
+use gst::prelude::*;
+use gst::subclass::prelude::*;
+
+use ashpd::{
+ desktop::screencast::{PersistMode, Screencast},
+ WindowIdentifier,
+};
+
+#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy, glib::Enum)]
+#[repr(u32)]
+#[enum_type(name = "SourceType")]
+pub enum SourceType {
+ #[enum_value(name = "A monitor", nick = "monitor")]
+ Monitor,
+ #[enum_value(name = "A specific window", nick = "window")]
+ Window,
+ #[enum_value(name = "Virtual", nick = "virtual")]
+ Virtual,
+ #[enum_value(name = "monitor+window+virtual", nick = "all")]
+ All,
+}
+
+impl From for ashpd::enumflags2::BitFlags {
+ fn from(v: SourceType) -> Self {
+ use ashpd::desktop::screencast;
+
+ match v {
+ SourceType::Monitor => screencast::SourceType::Monitor.into(),
+ SourceType::Window => screencast::SourceType::Window.into(),
+ SourceType::Virtual => screencast::SourceType::Virtual.into(),
+ SourceType::All => {
+ screencast::SourceType::Monitor
+ | screencast::SourceType::Window
+ | screencast::SourceType::Virtual
+ }
+ }
+ }
+}
+
+#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy, glib::Enum)]
+#[repr(u32)]
+#[enum_type(name = "CursorMode")]
+pub enum CursorMode {
+ #[enum_value(
+ name = "The cursor is not part of the screen cast stream",
+ nick = "hidden"
+ )]
+ Hidden,
+ #[enum_value(
+ name = "The cursor is embedded as part of the stream buffers",
+ nick = "embedded"
+ )]
+ Embedded,
+ #[enum_value(
+ name = "The cursor is not part of the screen cast stream, but sent as PipeWire stream metadata. Not implemented",
+ nick = "metadata"
+ )]
+ Metadata,
+}
+
+impl From for ashpd::desktop::screencast::CursorMode {
+ fn from(v: CursorMode) -> Self {
+ use ashpd::desktop::screencast;
+
+ match v {
+ CursorMode::Hidden => screencast::CursorMode::Hidden,
+ CursorMode::Embedded => screencast::CursorMode::Embedded,
+ CursorMode::Metadata => unimplemented!(),
+ }
+ }
+}
+
+use once_cell::sync::Lazy;
+use parking_lot::Mutex;
+
+pub fn block_on(future: F) -> F::Output {
+ static TOKIO_RT: once_cell::sync::Lazy =
+ once_cell::sync::Lazy::new(|| {
+ tokio::runtime::Builder::new_current_thread()
+ .enable_io()
+ .enable_time()
+ .build()
+ .expect("launch of single-threaded tokio runtime")
+ });
+ TOKIO_RT.block_on(future)
+}
+
+async fn portal_main(cursor_mode: CursorMode, source_type: SourceType) -> ashpd::Result {
+ let proxy = Screencast::new().await?;
+ let session = proxy.create_session().await?;
+ proxy
+ .select_sources(
+ &session,
+ cursor_mode.into(),
+ source_type.into(),
+ false,
+ None,
+ PersistMode::DoNot,
+ )
+ .await?;
+
+ let response = proxy
+ .start(&session, &WindowIdentifier::default())
+ .await?
+ .response()?;
+
+ if let Some(first_value) = response.streams().iter().next() {
+ let id = first_value.pipe_wire_node_id();
+ Ok(id)
+ } else {
+ Err(ashpd::Error::NoResponse)
+ }
+}
+
+const DEFAULT_CURSOR_MODE: CursorMode = CursorMode::Hidden;
+const DEFAULT_SOURCE_TYPE: SourceType = SourceType::All;
+
+#[derive(Debug, Clone, Copy)]
+struct Settings {
+ cursor_mode: CursorMode,
+ source_type: SourceType,
+}
+
+impl Default for Settings {
+ fn default() -> Self {
+ Settings {
+ cursor_mode: DEFAULT_CURSOR_MODE,
+ source_type: DEFAULT_SOURCE_TYPE,
+ }
+ }
+}
+
+pub struct XdpScreenCast {
+ settings: Mutex,
+ src: gst::Element,
+ srcpad: gst::GhostPad,
+}
+
+static CAT: Lazy = Lazy::new(|| {
+ gst::DebugCategory::new(
+ "xdgscreencapsrc",
+ gst::DebugColorFlags::empty(),
+ Some("XDP Screen Cast Bin"),
+ )
+});
+
+impl XdpScreenCast {}
+
+#[glib::object_subclass]
+impl ObjectSubclass for XdpScreenCast {
+ const NAME: &'static str = "GstXdpScreenCast";
+ type Type = super::XdpScreenCast;
+ type ParentType = gst::Bin;
+
+ fn with_class(klass: &Self::Class) -> Self {
+ let settings = Mutex::new(Settings::default());
+ let src = gst::ElementFactory::make("pipewiresrc").build().unwrap();
+ let templ = klass.pad_template("src").unwrap();
+ let srcpad = gst::GhostPad::from_template(&templ);
+
+ Self {
+ settings,
+ src,
+ srcpad,
+ }
+ }
+}
+
+impl ObjectImpl for XdpScreenCast {
+ // based on https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.ScreenCast.html#org-freedesktop-portal-screencast-selectsources
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy> = Lazy::new(|| {
+ vec![
+ glib::ParamSpecEnum::builder_with_default::(
+ "cursor-mode",
+ DEFAULT_CURSOR_MODE,
+ )
+ .nick("cursor mode")
+ .blurb("Determines how the cursor will be drawn in the screen cast stream")
+ .mutable_ready()
+ .build(),
+ glib::ParamSpecEnum::builder_with_default::(
+ "source-type",
+ DEFAULT_SOURCE_TYPE,
+ )
+ .nick("source type")
+ .blurb("Sets the types of content to record")
+ .mutable_ready()
+ .build(),
+ ]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
+ match pspec.name() {
+ "cursor-mode" => {
+ let mut settings = self.settings.lock();
+ let cursor_mode = value.get().expect("type checked upstream");
+ gst::debug!(
+ CAT,
+ imp: self,
+ "Setting cursor-mode from {:?} to {:?}",
+ settings.cursor_mode,
+ cursor_mode
+ );
+
+ settings.cursor_mode = cursor_mode;
+ }
+ "source-type" => {
+ let mut settings = self.settings.lock();
+ let source_type = value.get().expect("type checked upstream");
+ gst::debug!(
+ CAT,
+ imp: self,
+ "Setting source-type from {:?} to {:?}",
+ settings.source_type,
+ source_type
+ );
+
+ settings.source_type = source_type;
+ }
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "cursor-mode" => {
+ let settings = self.settings.lock();
+ settings.cursor_mode.to_value()
+ }
+ "source-type" => {
+ let settings = self.settings.lock();
+ settings.source_type.to_value()
+ }
+ _ => unimplemented!(),
+ }
+ }
+
+ fn constructed(&self) {
+ self.parent_constructed();
+
+ let obj = self.obj();
+
+ obj.add(&self.src).unwrap();
+ self.srcpad
+ .set_target(Some(&self.src.static_pad("src").unwrap()))
+ .unwrap();
+
+ obj.add_pad(&self.srcpad).unwrap();
+ }
+}
+
+impl GstObjectImpl for XdpScreenCast {}
+
+impl ElementImpl for XdpScreenCast {
+ fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
+ static ELEMENT_METADATA: Lazy = Lazy::new(|| {
+ gst::subclass::ElementMetadata::new(
+ "xdg-desktop-portal screen capture",
+ "Generic",
+ "Source element wrapping pipewiresrc using \
+ xdg-desktop-portal to start a screencast session.",
+ "Ruben Gonzalez ",
+ )
+ });
+
+ Some(&*ELEMENT_METADATA)
+ }
+
+ fn pad_templates() -> &'static [gst::PadTemplate] {
+ static PAD_TEMPLATES: Lazy> = Lazy::new(|| {
+ let caps = gst::Caps::new_any();
+ let src_pad_template = gst::PadTemplate::new(
+ "src",
+ gst::PadDirection::Src,
+ gst::PadPresence::Always,
+ &caps,
+ )
+ .unwrap();
+
+ vec![src_pad_template]
+ });
+
+ PAD_TEMPLATES.as_ref()
+ }
+
+ fn change_state(
+ &self,
+ transition: gst::StateChange,
+ ) -> Result {
+ gst::debug!(CAT, imp: self, "Changing state {:?}", transition);
+
+ let settings = self.settings.lock();
+
+ let success = self.parent_change_state(transition)?;
+
+ if transition == gst::StateChange::NullToReady {
+ if let Ok(fd) = block_on(portal_main(settings.cursor_mode, settings.source_type)) {
+ self.src.set_property("fd", fd as i32);
+ } else {
+ return Err(gst::StateChangeError);
+ }
+ }
+
+ Ok(success)
+ }
+}
+
+impl BinImpl for XdpScreenCast {}
diff --git a/utils/xdgscreencapsrc/src/xdgscreencapsrc/mod.rs b/utils/xdgscreencapsrc/src/xdgscreencapsrc/mod.rs
new file mode 100644
index 00000000..d060fd4c
--- /dev/null
+++ b/utils/xdgscreencapsrc/src/xdgscreencapsrc/mod.rs
@@ -0,0 +1,25 @@
+// Copyright (C) 2023 Ruben Gonzalez .
+//
+// SPDX-License-Identifier: MPL-2.0
+
+use gst::glib;
+use gst::prelude::*;
+
+mod imp;
+
+glib::wrapper! {
+ pub struct XdpScreenCast(ObjectSubclass) @extends gst::Bin, gst::Element, gst::Object;
+}
+
+pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
+ gst::Element::register(
+ Some(plugin),
+ "xdgscreencapsrc",
+ gst::Rank::NONE,
+ XdpScreenCast::static_type(),
+ )
+}