From f632d5c4df3fd37eb99e8ae514278b41b7122975 Mon Sep 17 00:00:00 2001 From: Dave Patrick Caberto Date: Mon, 19 Feb 2024 18:03:34 +0800 Subject: [PATCH] misc(device): rely solely on gstreamer device provider It does what we currently with pulse internally. --- Cargo.lock | 73 +------------- Cargo.toml | 2 - meson.build | 2 - po/POTFILES.in | 2 +- src/audio_device.rs | 229 -------------------------------------------- src/device.rs | 76 +++++++++++++++ src/help.rs | 27 +++++- src/main.rs | 2 +- src/pipeline.rs | 101 +++++++++++++------ src/recording.rs | 17 +--- 10 files changed, 179 insertions(+), 352 deletions(-) delete mode 100644 src/audio_device.rs create mode 100644 src/device.rs diff --git a/Cargo.lock b/Cargo.lock index 9e85a35ac..42a3a1d5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,12 +63,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.4.2" @@ -93,7 +87,7 @@ version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc1c415b7088381c53c575420899c34c9e6312df5ac5defd05614210e9fd6e1b" dependencies = [ - "bitflags 2.4.2", + "bitflags", "cairo-sys-rs", "glib", "libc", @@ -520,7 +514,7 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "170ee82b9b44b3b5fd1cf4971d6cf0eadec38303bb84c7bcc4e6b95a18934e71" dependencies = [ - "bitflags 2.4.2", + "bitflags", "futures-channel", "futures-core", "futures-executor", @@ -952,8 +946,6 @@ dependencies = [ "gstreamer", "gtk4", "libadwaita", - "libpulse-binding", - "libpulse-glib-binding", "num-rational", "num-traits", "once_cell", @@ -1007,56 +999,6 @@ version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" -[[package]] -name = "libpulse-binding" -version = "2.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3557a2dfc380c8f061189a01c6ae7348354e0c9886038dc6c171219c08eaff" -dependencies = [ - "bitflags 1.3.2", - "libc", - "libpulse-sys", - "num-derive", - "num-traits", - "winapi", -] - -[[package]] -name = "libpulse-glib-binding" -version = "2.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72bb604d4f32d4c60e02581a67f9d9fd7500cb963ad984cee032013edeaf6bee" -dependencies = [ - "glib", - "glib-sys", - "libpulse-binding", - "libpulse-mainloop-glib-sys", -] - -[[package]] -name = "libpulse-mainloop-glib-sys" -version = "1.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00f9e6fbee0a60ac3f5751e3cc68eeaf9bff9d2687502df17b5c726220217531" -dependencies = [ - "glib-sys", - "libpulse-sys", - "pkg-config", -] - -[[package]] -name = "libpulse-sys" -version = "1.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc19e110fbf42c17260d30f6d3dc545f58491c7830d38ecb9aaca96e26067a9b" -dependencies = [ - "libc", - "num-derive", - "num-traits", - "pkg-config", - "winapi", -] - [[package]] name = "locale_config" version = "0.3.0" @@ -1116,17 +1058,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "num-derive" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "num-integer" version = "0.1.46" diff --git a/Cargo.toml b/Cargo.toml index 268cff549..52e750d47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,8 +24,6 @@ gtk = { package = "gtk4", version = "0.8", features = ["v4_14"] } num-rational = { version = "0.4", default-features = false } num-traits = "0.2" once_cell = "1.19.0" -pulse = { package = "libpulse-binding", version = "2.26.0" } -pulse_glib = { package = "libpulse-glib-binding", version = "2.25.1" } serde_yaml = "0.9.31" serde = { version = "1.0.196", features = ["derive"] } tracing = "0.1.36" diff --git a/meson.build b/meson.build index 71d59dd8e..ef85e47c4 100644 --- a/meson.build +++ b/meson.build @@ -17,8 +17,6 @@ dependency('gtk4', version: '>= 4.13') dependency('libadwaita-1', version: '>= 1.5') dependency('gstreamer-1.0', version: '>= 1.22') dependency('gstreamer-plugins-base-1.0', version: '>= 1.22') -dependency('libpulse-mainloop-glib', version: '>= 16.0') -dependency('libpulse', version: '>= 16.0') glib_compile_resources = find_program('glib-compile-resources', required: true) glib_compile_schemas = find_program('glib-compile-schemas', required: true) diff --git a/po/POTFILES.in b/po/POTFILES.in index 6086a17ea..f3d861035 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -7,7 +7,7 @@ data/resources/ui/shortcuts.ui data/resources/ui/window.ui src/about.rs src/application.rs -src/audio_device.rs +src/device.rs src/format_time.rs src/main.rs src/preferences_dialog.rs diff --git a/src/audio_device.rs b/src/audio_device.rs deleted file mode 100644 index 8c612192a..000000000 --- a/src/audio_device.rs +++ /dev/null @@ -1,229 +0,0 @@ -use anyhow::{anyhow, Result}; -use gettextrs::gettext; -use gst::prelude::*; -use gtk::gio; - -use crate::help::ResultExt; - -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -pub enum AudioDeviceClass { - #[default] - Source, - Sink, -} - -impl AudioDeviceClass { - fn as_str(self) -> &'static str { - match self { - Self::Source => "Audio/Source", - Self::Sink => "Audio/Sink", - } - } -} - -pub async fn find_default_name(class: AudioDeviceClass) -> Result { - match gio::spawn_blocking(move || find_default_name_gst(class)) - .await - .map_err(|err| anyhow!("Failed to spawn blocking task: {:?}", err))? - { - Ok(res) => Ok(res), - Err(err) => { - tracing::warn!("Failed to find default name using gstreamer: {:?}", err); - tracing::debug!("Manually using libpulse instead"); - - let pa_context = pa::Context::connect().await?; - pa_context.find_default_device_name(class).await - } - } -} - -fn find_default_name_gst(class: AudioDeviceClass) -> Result { - let device_monitor = gst::DeviceMonitor::new(); - device_monitor.add_filter(Some(class.as_str()), None); - - device_monitor.start().with_help( - || gettext("Make sure that you have PulseAudio installed in your system."), - || gettext("Failed to start device monitor"), - )?; - let devices = device_monitor.devices(); - device_monitor.stop(); - - tracing::debug!("Finding device name for class `{:?}`", class); - - for device in devices { - if !device.has_classes(class.as_str()) { - tracing::debug!( - "Skipping device `{}` as it has unknown device class `{}`", - device.name(), - device.device_class() - ); - continue; - } - - let Some(properties) = device.properties() else { - tracing::warn!( - "Skipping device `{}` as it has no properties", - device.name() - ); - continue; - }; - - let is_default = match properties.get::("is-default") { - Ok(is_default) => is_default, - Err(err) => { - tracing::warn!( - "Skipping device `{}` as it has no `is-default` property: {:?}", - device.name(), - err - ); - continue; - } - }; - - if !is_default { - tracing::debug!( - "Skipping device `{}` as it is not the default", - device.name() - ); - continue; - } - - let mut node_name = match properties.get::("node.name") { - Ok(node_name) => node_name, - Err(err) => { - tracing::warn!( - "Skipping device `{}` as it has no node.name property. {:?}", - device.name(), - err - ); - continue; - } - }; - - if device.has_classes(AudioDeviceClass::Sink.as_str()) { - node_name.push_str(".monitor"); - } - - return Ok(node_name); - } - - Err(anyhow!("Failed to find a default device")) -} - -mod pa { - use anyhow::{bail, Context as ErrContext, Result}; - use futures_channel::{mpsc, oneshot}; - use futures_util::StreamExt; - use gettextrs::gettext; - use gtk::glib; - use pulse::{ - context::{Context as ContextInner, FlagSet, State}, - proplist::{properties, Proplist}, - }; - - use std::time::Duration; - - use super::AudioDeviceClass; - use crate::{config::APP_ID, help::ResultExt}; - - const INTROSPECT_TIMEOUT: Duration = Duration::from_secs(2); - - pub struct Context { - inner: ContextInner, - - // `ContextInner` does not hold a reference causing it - // to be freed and cause error after `Context::connect`. - #[allow(dead_code)] - main_loop: pulse_glib::Mainloop, - } - - impl Context { - pub async fn connect() -> Result { - let main_loop = - pulse_glib::Mainloop::new(None).context("Failed to create pulse Mainloop")?; - - let mut proplist = Proplist::new().unwrap(); - proplist - .set_str(properties::APPLICATION_ID, APP_ID) - .unwrap(); - proplist - .set_str(properties::APPLICATION_NAME, "Kooha") - .unwrap(); - - let mut inner = ContextInner::new_with_proplist(&main_loop, APP_ID, &proplist) - .context("Failed to create pulse Context")?; - - inner.connect(None, FlagSet::NOFLAGS, None).with_help( - || gettext("Make sure that you have PulseAudio installed in your system."), - || gettext("Failed to connect to PulseAudio daemon"), - )?; - - let (mut tx, mut rx) = mpsc::channel(1); - - inner.set_state_callback(Some(Box::new(move || { - let _ = tx.start_send(()); - }))); - - tracing::debug!("Waiting for context server connection"); - - while rx.next().await.is_some() { - match inner.get_state() { - State::Ready => break, - State::Failed => bail!("Connection failed or disconnected"), - State::Terminated => bail!("Connection context terminated"), - _ => {} - } - } - - tracing::debug!("Connected context to server"); - - inner.set_state_callback(None); - - Ok(Self { inner, main_loop }) - } - - pub async fn find_default_device_name(&self, class: AudioDeviceClass) -> Result { - let (tx, rx) = oneshot::channel(); - let mut tx = Some(tx); - - let mut operation = self.inner.introspect().get_server_info(move |server_info| { - let Some(tx) = tx.take() else { - tracing::error!("Called get_server_info twice!"); - return; - }; - - match class { - AudioDeviceClass::Source => { - let _ = tx.send( - server_info - .default_source_name - .as_ref() - .map(|s| s.to_string()), - ); - } - AudioDeviceClass::Sink => { - let _ = tx.send( - server_info - .default_sink_name - .as_ref() - .map(|s| format!("{}.monitor", s)), - ); - } - } - }); - - let Ok(name) = glib::future_with_timeout(INTROSPECT_TIMEOUT, rx).await else { - operation.cancel(); - bail!("Timeout reached when getting server info"); - }; - - name.unwrap().context("Found no default device") - } - } - - impl Drop for Context { - fn drop(&mut self) { - self.inner.disconnect(); - } - } -} diff --git a/src/device.rs b/src/device.rs new file mode 100644 index 000000000..ab9c99b2f --- /dev/null +++ b/src/device.rs @@ -0,0 +1,76 @@ +use anyhow::{anyhow, Result}; +use gettextrs::gettext; +use gst::prelude::*; + +use crate::help::ResultExt; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DeviceClass { + Source, + Sink, +} + +impl DeviceClass { + fn as_str(self) -> &'static str { + match self { + Self::Source => "Audio/Source", + Self::Sink => "Audio/Sink", + } + } +} + +pub fn find_default(class: DeviceClass) -> Result { + let provider = gst::DeviceProviderFactory::by_name("pulsedeviceprovider").with_help( + || gettext("Make sure that you have PulseAudio installed in your system."), + || gettext("No pulseaudio device provider found"), + )?; + + provider.start()?; + let devices = provider.devices(); + provider.stop(); + + tracing::debug!("Finding device name for class `{:?}`", class); + + for device in devices { + if !device.has_classes(class.as_str()) { + tracing::debug!( + "Skipping device `{}` as it has unknown device class `{}`", + device.name(), + device.device_class() + ); + continue; + } + + let Some(properties) = device.properties() else { + tracing::warn!( + "Skipping device `{}` as it has no properties", + device.name() + ); + continue; + }; + + let is_default = match properties.get::("is-default") { + Ok(is_default) => is_default, + Err(err) => { + tracing::warn!( + "Skipping device `{}` as it has no `is-default` property: {:?}", + device.name(), + err + ); + continue; + } + }; + + if !is_default { + tracing::debug!( + "Skipping device `{}` as it is not the default", + device.name() + ); + continue; + } + + return Ok(device); + } + + Err(anyhow!("Failed to find a default device")) +} diff --git a/src/help.rs b/src/help.rs index 929948eb0..317c1c9e8 100644 --- a/src/help.rs +++ b/src/help.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Error, Result}; -use std::fmt; +use std::{convert::Infallible, fmt}; #[derive(Debug)] pub struct Help(String); @@ -43,7 +43,7 @@ pub trait ResultExt { fn with_help( self, - help_msg: impl FnOnce() -> M, + help_msg_fn: impl FnOnce() -> M, context_fn: impl FnOnce() -> C, ) -> Result where @@ -76,3 +76,26 @@ where .with_context(context_fn) } } + +impl ResultExt for Option { + fn help(self, help_msg: M, context: C) -> Result + where + M: Into, + C: fmt::Display + Send + Sync + 'static, + { + self.context(Help::new(help_msg)).context(context) + } + + fn with_help( + self, + help_msg_fn: impl FnOnce() -> M, + context_fn: impl FnOnce() -> C, + ) -> Result + where + M: Into, + C: fmt::Display + Send + Sync + 'static, + { + self.with_context(|| Help::new(help_msg_fn())) + .with_context(context_fn) + } +} diff --git a/src/main.rs b/src/main.rs index b5b1b5b70..40eddb503 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,9 +26,9 @@ mod about; mod application; mod area_selector; -mod audio_device; mod cancelled; mod config; +mod device; mod format_time; mod framerate_option; mod help; diff --git a/src/pipeline.rs b/src/pipeline.rs index cfe8b74f6..2d0a0aafe 100644 --- a/src/pipeline.rs +++ b/src/pipeline.rs @@ -1,4 +1,4 @@ -use anyhow::{bail, Context, Ok, Result}; +use anyhow::{bail, ensure, Context, Ok, Result}; use gst::prelude::*; use gtk::graphene::Rect; use num_rational::Rational32; @@ -8,7 +8,12 @@ use std::{ path::{Path, PathBuf}, }; -use crate::{area_selector::SelectAreaData, profile::Profile, screencast_session::Stream}; +use crate::{ + area_selector::SelectAreaData, + device::{self, DeviceClass}, + profile::Profile, + screencast_session::Stream, +}; const AUDIO_SAMPLE_RATE: i32 = 48_000; const AUDIO_N_CHANNELS: i32 = 1; @@ -23,8 +28,8 @@ pub struct PipelineBuilder { profile: Profile, fd: RawFd, streams: Vec, - desktop_audio_name: Option, - microphone_name: Option, + record_desktop_audio: bool, + record_microphone: bool, select_area_data: Option, } @@ -42,19 +47,19 @@ impl PipelineBuilder { profile, fd, streams, - desktop_audio_name: None, - microphone_name: None, + record_desktop_audio: false, + record_microphone: false, select_area_data: None, } } - pub fn desktop_audio_name(&mut self, desktop_audio_name: String) -> &mut Self { - self.desktop_audio_name = Some(desktop_audio_name); + pub fn record_desktop_audio(&mut self, record_desktop_audio: bool) -> &mut Self { + self.record_desktop_audio = record_desktop_audio; self } - pub fn microphone_name(&mut self, microphone_name: String) -> &mut Self { - self.microphone_name = Some(microphone_name); + pub fn record_microphone(&mut self, record_microphone: bool) -> &mut Self { + self.record_microphone = record_microphone; self } @@ -70,8 +75,6 @@ impl PipelineBuilder { profile = ?self.profile.id(), stream_len = self.streams.len(), streams = ?self.streams, - desktop_audio_name = ?self.desktop_audio_name, - microphone_name = ?self.microphone_name, select_area_data = ?self.select_area_data, ); @@ -98,12 +101,20 @@ impl PipelineBuilder { pipeline.add_many([videosrc_bin.upcast_ref(), &videoenc_queue, &filesink])?; videosrc_bin.link(&videoenc_queue)?; - let has_audio_source = self.desktop_audio_name.is_some() || self.microphone_name.is_some(); - let audioenc_queue = if self.profile.supports_audio() && has_audio_source { + let audioenc_queue = if self.record_desktop_audio || self.record_microphone { + debug_assert!(self.profile.supports_audio()); + + let pulsesrcs = [ + self.record_desktop_audio + .then(|| make_pulsesrc(DeviceClass::Sink, "desktop-audio-src")), + self.record_microphone + .then(|| make_pulsesrc(DeviceClass::Source, "microphone-src")), + ]; let audiosrc_bin = make_pulsesrc_bin( - [&self.desktop_audio_name, &self.microphone_name] + &pulsesrcs .into_iter() - .filter_map(|s| s.as_deref()), + .flatten() + .collect::>>()?, ) .context("Failed to create audiosrc bin")?; let audioenc_queue = gst::ElementFactory::make("queue").build()?; @@ -113,12 +124,6 @@ impl PipelineBuilder { Some(audioenc_queue) } else { - if has_audio_source { - tracing::warn!( - "Selected profile does not support audio, but audio sources are provided. Ignoring audio sources" - ); - } - None }; @@ -321,6 +326,47 @@ pub fn make_pipewiresrc_bin( Ok(bin) } +/// Creates a new audio src element with the given name. +/// +/// If the class is already a source, it will return the device name as is, +/// otherwise, if it is a sink, it will append `.monitor` to the device name. +fn make_pulsesrc(class: DeviceClass, element_name: &str) -> Result { + let device = device::find_default(class)?; + + let pulsesrc = gst::ElementFactory::make("pulsesrc") + .name(element_name) + .property("provide-clock", false) + .property("do-timestamp", true) + .build()?; + + match class { + DeviceClass::Sink => { + let pulsesink = device.create_element(None)?; + let device_name = pulsesink + .property::>("device") + .context("No device name")?; + ensure!(!device_name.is_empty(), "Empty device name"); + + let monitor_name = format!("{}.monitor", device_name); + pulsesrc.set_property("device", &monitor_name); + + tracing::debug!("Found desktop audio with name `{}`", monitor_name); + } + DeviceClass::Source => { + device.reconfigure_element(&pulsesrc)?; + + let device_name = pulsesrc + .property::>("device") + .context("No device name")?; + ensure!(!device_name.is_empty(), "Empty device name"); + + tracing::debug!("Found microphone with name `{}`", device_name); + } + } + + Ok(pulsesrc) +} + /// Creates a bin with a src pad for a pulse audio device /// /// pulsesrc1 -> audiorate -> | @@ -328,7 +374,9 @@ pub fn make_pipewiresrc_bin( /// pulsesrc2 -> audiorate -> | -> audiomixer /// | /// pulsesrcn -> audiorate -> | -fn make_pulsesrc_bin<'a>(device_names: impl IntoIterator) -> Result { +fn make_pulsesrc_bin<'a>( + pulsesrcs: impl IntoIterator, +) -> Result { let bin = gst::Bin::builder().name("kooha-pulsesrc-bin").build(); let audiomixer = gst::ElementFactory::make("audiomixer") @@ -347,12 +395,7 @@ fn make_pulsesrc_bin<'a>(device_names: impl IntoIterator) -> Res .field("rate", AUDIO_SAMPLE_RATE) .field("channels", AUDIO_N_CHANNELS) .build(); - for device_name in device_names { - let pulsesrc = gst::ElementFactory::make("pulsesrc") - .property("device", device_name) - .property("provide-clock", false) - .property("do-timestamp", true) - .build()?; + for pulsesrc in pulsesrcs { let audiorate = gst::ElementFactory::make("audiorate") .property("skip-to-first", true) .build()?; diff --git a/src/recording.rs b/src/recording.rs index 554038418..7d0269617 100644 --- a/src/recording.rs +++ b/src/recording.rs @@ -20,7 +20,6 @@ use gtk::{ use crate::{ application::Application, area_selector::AreaSelector, - audio_device::{self, AudioDeviceClass}, cancelled::Cancelled, help::{ErrorExt, ResultExt}, i18n::gettext_f, @@ -214,20 +213,8 @@ impl Recording { // setup audio sources if profile_supports_audio { - if settings.record_microphone() { - pipeline_builder.microphone_name( - audio_device::find_default_name(AudioDeviceClass::Source) - .await - .with_context(|| gettext("No microphone source found"))?, - ); - } - if settings.record_desktop_audio() { - pipeline_builder.desktop_audio_name( - audio_device::find_default_name(AudioDeviceClass::Sink) - .await - .with_context(|| gettext("No desktop audio source found"))?, - ); - } + pipeline_builder.record_desktop_audio(settings.record_desktop_audio()); + pipeline_builder.record_microphone(settings.record_microphone()); } // build pipeline