mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Merge pull request #981 from JasonLG1979/port-976
Port PulseAudio enhancements
This commit is contained in:
commit
6a98a0138c
6 changed files with 81 additions and 45 deletions
|
@ -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.
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,29 +116,23 @@ 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(
|
||||||
|
Some(name.as_ref()),
|
||||||
|
"_spotify-connect._tcp",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
port,
|
||||||
|
&["VERSION=1.0", "CPath=/"],
|
||||||
|
)?;
|
||||||
|
|
||||||
cfg_if! {
|
#[cfg(not(feature = "with-dns-sd"))]
|
||||||
if #[cfg(feature = "with-dns-sd")] {
|
let svc = libmdns::Responder::spawn(&tokio::runtime::Handle::current())?.register(
|
||||||
svc = dns_sd::DNSService::register(
|
"_spotify-connect._tcp".to_owned(),
|
||||||
Some(name.as_ref()),
|
name,
|
||||||
"_spotify-connect._tcp",
|
port,
|
||||||
None,
|
&["VERSION=1.0", "CPath=/"],
|
||||||
None,
|
);
|
||||||
port,
|
|
||||||
&["VERSION=1.0", "CPath=/"],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
let responder = libmdns::Responder::spawn(&tokio::runtime::Handle::current())?;
|
|
||||||
svc = responder.register(
|
|
||||||
"_spotify-connect._tcp".to_owned(),
|
|
||||||
name,
|
|
||||||
port,
|
|
||||||
&["VERSION=1.0", "CPath=/"],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Discovery { server, _svc: svc })
|
Ok(Discovery { server, _svc: svc })
|
||||||
}
|
}
|
||||||
|
|
|
@ -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!(
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
39
src/main.rs
39
src/main.rs
|
@ -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>() {
|
||||||
|
|
Loading…
Reference in a new issue