Merge pull request #981 from JasonLG1979/port-976

Port PulseAudio enhancements
This commit is contained in:
Roderick van Domburg 2022-04-07 21:24:33 +02:00 committed by GitHub
commit 6a98a0138c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 81 additions and 45 deletions

View file

@ -23,6 +23,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [main] Add a `-q`, `--quiet` option that changes the logging level to warn. - [main] Add a `-q`, `--quiet` option that changes the logging level to warn.
- [main] Add a short name for every flag and option. - [main] Add a short name for every flag and option.
- [main] Add the ability to parse environment variables. - [main] Add the ability to parse environment variables.
- [playback] `pulseaudio`: set the PulseAudio name to match librespot's device name via `PULSE_PROP_application.name` environment variable (user set env var value takes precedence). (breaking)
- [playback] `pulseaudio`: set icon to `audio-x-generic` so we get an icon instead of a placeholder via `PULSE_PROP_application.icon_name` environment variable (user set env var value takes precedence). (breaking)
- [playback] `pulseaudio`: set values to: `PULSE_PROP_application.version`, `PULSE_PROP_application.process.binary`, `PULSE_PROP_stream.description`, `PULSE_PROP_media.software` and `PULSE_PROP_media.role` environment variables (user set env var values take precedence). (breaking)
### Fixed ### Fixed
- [main] Prevent hang when discovery is disabled and there are no credentials or when bad credentials are given. - [main] Prevent hang when discovery is disabled and there are no credentials or when bad credentials are given.

View file

@ -257,7 +257,7 @@ impl SpClient {
let mut tries: usize = 0; let mut tries: usize = 0;
let mut last_response; let mut last_response;
let body = body.unwrap_or_else(String::new); let body = body.unwrap_or_default();
loop { loop {
tries += 1; tries += 1;

View file

@ -16,7 +16,6 @@ use std::{
task::{Context, Poll}, task::{Context, Poll},
}; };
use cfg_if::cfg_if;
use futures_core::Stream; use futures_core::Stream;
use thiserror::Error; use thiserror::Error;
@ -117,11 +116,8 @@ impl Builder {
let name = self.server_config.name.clone().into_owned(); let name = self.server_config.name.clone().into_owned();
let server = DiscoveryServer::new(self.server_config, &mut port)??; let server = DiscoveryServer::new(self.server_config, &mut port)??;
let svc; #[cfg(feature = "with-dns-sd")]
let svc = dns_sd::DNSService::register(
cfg_if! {
if #[cfg(feature = "with-dns-sd")] {
svc = dns_sd::DNSService::register(
Some(name.as_ref()), Some(name.as_ref()),
"_spotify-connect._tcp", "_spotify-connect._tcp",
None, None,
@ -130,16 +126,13 @@ impl Builder {
&["VERSION=1.0", "CPath=/"], &["VERSION=1.0", "CPath=/"],
)?; )?;
} else { #[cfg(not(feature = "with-dns-sd"))]
let responder = libmdns::Responder::spawn(&tokio::runtime::Handle::current())?; let svc = libmdns::Responder::spawn(&tokio::runtime::Handle::current())?.register(
svc = responder.register(
"_spotify-connect._tcp".to_owned(), "_spotify-connect._tcp".to_owned(),
name, name,
port, port,
&["VERSION=1.0", "CPath=/"], &["VERSION=1.0", "CPath=/"],
) );
}
};
Ok(Discovery { server, _svc: svc }) Ok(Discovery { server, _svc: svc })
} }

View file

@ -144,7 +144,7 @@ fn list_compatible_devices() -> SinkResult<()> {
println!( println!(
"\tDescription:\n\n\t\t{}\n", "\tDescription:\n\n\t\t{}\n",
a.desc.unwrap_or_default().replace("\n", "\n\t\t") a.desc.unwrap_or_default().replace('\n', "\n\t\t")
); );
println!( println!(

View file

@ -5,11 +5,9 @@ use crate::decoder::AudioPacket;
use crate::{NUM_CHANNELS, SAMPLE_RATE}; use crate::{NUM_CHANNELS, SAMPLE_RATE};
use libpulse_binding::{self as pulse, error::PAErr, stream::Direction}; use libpulse_binding::{self as pulse, error::PAErr, stream::Direction};
use libpulse_simple_binding::Simple; use libpulse_simple_binding::Simple;
use std::env;
use thiserror::Error; use thiserror::Error;
const APP_NAME: &str = "librespot";
const STREAM_NAME: &str = "Spotify endpoint";
#[derive(Debug, Error)] #[derive(Debug, Error)]
enum PulseError { enum PulseError {
#[error("<PulseAudioSink> Unsupported Pulseaudio Sample Spec, Format {pulse_format:?} ({format:?}), Channels {channels}, Rate {rate}")] #[error("<PulseAudioSink> Unsupported Pulseaudio Sample Spec, Format {pulse_format:?} ({format:?}), Channels {channels}, Rate {rate}")]
@ -47,13 +45,18 @@ impl From<PulseError> for SinkError {
} }
pub struct PulseAudioSink { pub struct PulseAudioSink {
s: Option<Simple>, sink: Option<Simple>,
device: Option<String>, device: Option<String>,
app_name: String,
stream_desc: String,
format: AudioFormat, format: AudioFormat,
} }
impl Open for PulseAudioSink { impl Open for PulseAudioSink {
fn open(device: Option<String>, format: AudioFormat) -> Self { fn open(device: Option<String>, format: AudioFormat) -> Self {
let app_name = env::var("PULSE_PROP_application.name").unwrap_or_default();
let stream_desc = env::var("PULSE_PROP_stream.description").unwrap_or_default();
let mut actual_format = format; let mut actual_format = format;
if actual_format == AudioFormat::F64 { if actual_format == AudioFormat::F64 {
@ -64,8 +67,10 @@ impl Open for PulseAudioSink {
info!("Using PulseAudioSink with format: {:?}", actual_format); info!("Using PulseAudioSink with format: {:?}", actual_format);
Self { Self {
s: None, sink: None,
device, device,
app_name,
stream_desc,
format: actual_format, format: actual_format,
} }
} }
@ -73,7 +78,7 @@ impl Open for PulseAudioSink {
impl Sink for PulseAudioSink { impl Sink for PulseAudioSink {
fn start(&mut self) -> SinkResult<()> { fn start(&mut self) -> SinkResult<()> {
if self.s.is_none() { if self.sink.is_none() {
// PulseAudio calls S24 and S24_3 different from the rest of the world // PulseAudio calls S24 and S24_3 different from the rest of the world
let pulse_format = match self.format { let pulse_format = match self.format {
AudioFormat::F32 => pulse::sample::Format::FLOAT32NE, AudioFormat::F32 => pulse::sample::Format::FLOAT32NE,
@ -84,13 +89,13 @@ impl Sink for PulseAudioSink {
_ => unreachable!(), _ => unreachable!(),
}; };
let ss = pulse::sample::Spec { let sample_spec = pulse::sample::Spec {
format: pulse_format, format: pulse_format,
channels: NUM_CHANNELS, channels: NUM_CHANNELS,
rate: SAMPLE_RATE, rate: SAMPLE_RATE,
}; };
if !ss.is_valid() { if !sample_spec.is_valid() {
let pulse_error = PulseError::InvalidSampleSpec { let pulse_error = PulseError::InvalidSampleSpec {
pulse_format, pulse_format,
format: self.format, format: self.format,
@ -101,30 +106,28 @@ impl Sink for PulseAudioSink {
return Err(SinkError::from(pulse_error)); return Err(SinkError::from(pulse_error));
} }
let s = Simple::new( let sink = Simple::new(
None, // Use the default server. None, // Use the default server.
APP_NAME, // Our application's name. &self.app_name, // Our application's name.
Direction::Playback, // Direction. Direction::Playback, // Direction.
self.device.as_deref(), // Our device (sink) name. self.device.as_deref(), // Our device (sink) name.
STREAM_NAME, // Description of our stream. &self.stream_desc, // Description of our stream.
&ss, // Our sample format. &sample_spec, // Our sample format.
None, // Use default channel map. None, // Use default channel map.
None, // Use default buffering attributes. None, // Use default buffering attributes.
) )
.map_err(PulseError::ConnectionRefused)?; .map_err(PulseError::ConnectionRefused)?;
self.s = Some(s); self.sink = Some(sink);
} }
Ok(()) Ok(())
} }
fn stop(&mut self) -> SinkResult<()> { fn stop(&mut self) -> SinkResult<()> {
let s = self.s.as_mut().ok_or(PulseError::NotConnected)?; let sink = self.sink.take().ok_or(PulseError::NotConnected)?;
s.drain().map_err(PulseError::DrainFailure)?; sink.drain().map_err(PulseError::DrainFailure)?;
self.s = None;
Ok(()) Ok(())
} }
@ -133,9 +136,9 @@ impl Sink for PulseAudioSink {
impl SinkAsBytes for PulseAudioSink { impl SinkAsBytes for PulseAudioSink {
fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> { fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> {
let s = self.s.as_mut().ok_or(PulseError::NotConnected)?; let sink = self.sink.as_mut().ok_or(PulseError::NotConnected)?;
s.write(data).map_err(PulseError::OnWrite)?; sink.write(data).map_err(PulseError::OnWrite)?;
Ok(()) Ok(())
} }

View file

@ -596,7 +596,7 @@ fn get_setup() -> Setup {
let stripped_env_key = |k: &str| { let stripped_env_key = |k: &str| {
k.trim_start_matches("LIBRESPOT_") k.trim_start_matches("LIBRESPOT_")
.replace("_", "-") .replace('_', "-")
.to_lowercase() .to_lowercase()
}; };
@ -1143,6 +1143,43 @@ fn get_setup() -> Setup {
exit(1); exit(1);
} }
#[cfg(feature = "pulseaudio-backend")]
{
if env::var("PULSE_PROP_application.name").is_err() {
let pulseaudio_name = if name != connect_default_config.name {
format!("{} - {}", connect_default_config.name, name)
} else {
name.clone()
};
env::set_var("PULSE_PROP_application.name", pulseaudio_name);
}
if env::var("PULSE_PROP_application.version").is_err() {
env::set_var("PULSE_PROP_application.version", version::SEMVER);
}
if env::var("PULSE_PROP_application.icon_name").is_err() {
env::set_var("PULSE_PROP_application.icon_name", "audio-x-generic");
}
if env::var("PULSE_PROP_application.process.binary").is_err() {
env::set_var("PULSE_PROP_application.process.binary", "librespot");
}
if env::var("PULSE_PROP_stream.description").is_err() {
env::set_var("PULSE_PROP_stream.description", "Spotify Connect endpoint");
}
if env::var("PULSE_PROP_media.software").is_err() {
env::set_var("PULSE_PROP_media.software", "Spotify");
}
if env::var("PULSE_PROP_media.role").is_err() {
env::set_var("PULSE_PROP_media.role", "music");
}
}
let initial_volume = opt_str(INITIAL_VOLUME) let initial_volume = opt_str(INITIAL_VOLUME)
.map(|initial_volume| { .map(|initial_volume| {
let volume = match initial_volume.parse::<u16>() { let volume = match initial_volume.parse::<u16>() {