mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
2708aa4fef
13 changed files with 220 additions and 77 deletions
1
.dockerignore
Normal file
1
.dockerignore
Normal file
|
@ -0,0 +1 @@
|
||||||
|
target
|
|
@ -16,6 +16,7 @@ path = "src/lib.rs"
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "librespot"
|
name = "librespot"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
doc = false
|
||||||
|
|
||||||
[dependencies.librespot-protocol]
|
[dependencies.librespot-protocol]
|
||||||
path = "protocol"
|
path = "protocol"
|
||||||
|
@ -86,5 +87,5 @@ section = "sound"
|
||||||
priority = "optional"
|
priority = "optional"
|
||||||
assets = [
|
assets = [
|
||||||
["target/release/librespot", "usr/bin/", "755"],
|
["target/release/librespot", "usr/bin/", "755"],
|
||||||
["assets/librespot.service", "lib/systemd/system/", "644"]
|
["contrib/librespot.service", "lib/systemd/system/", "644"]
|
||||||
]
|
]
|
||||||
|
|
34
README.md
34
README.md
|
@ -4,10 +4,6 @@ applications to use Spotify's service, without using the official but
|
||||||
closed-source libspotify. Additionally, it will provide extra features
|
closed-source libspotify. Additionally, it will provide extra features
|
||||||
which are not available in the official library.
|
which are not available in the official library.
|
||||||
|
|
||||||
## Status
|
|
||||||
*librespot* is currently under development and is not fully functional yet. You
|
|
||||||
are however welcome to experiment with it.
|
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
Rust 1.7.0 or later is required to build librespot.
|
Rust 1.7.0 or later is required to build librespot.
|
||||||
|
|
||||||
|
@ -60,6 +56,36 @@ The following backends are currently available :
|
||||||
- PortAudio
|
- PortAudio
|
||||||
- PulseAudio
|
- PulseAudio
|
||||||
|
|
||||||
|
## Cross-compiling
|
||||||
|
A cross compilation environment is provided as a docker image.
|
||||||
|
Build the image from the root of the project with the following command :
|
||||||
|
|
||||||
|
```
|
||||||
|
$ docker build -t librespot-cross -f contrib/Dockerfile .
|
||||||
|
```
|
||||||
|
|
||||||
|
The resulting image can be used to build librespot for linux x86_64, armhf and armel.
|
||||||
|
The compiled binaries will be located in /tmp/librespot-build
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run -v /tmp/librespot-build:/build librespot-cross
|
||||||
|
```
|
||||||
|
|
||||||
|
If only one architecture is desired, cargo can be invoked directly with the appropriate options :
|
||||||
|
```shell
|
||||||
|
docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --no-default-features --features "with-syntex alsa-backend"
|
||||||
|
docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "with-syntex alsa-backend"
|
||||||
|
docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features "with-syntex alsa-backend"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
When developing *librespot*, it is preferable to use Rust nightly, and build it using the following :
|
||||||
|
```shell
|
||||||
|
cargo build --no-default-features --features "nightly portaudio-backend"
|
||||||
|
```
|
||||||
|
|
||||||
|
This produces better compilation error messages than with the default configuration.
|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
Using this code to connect to Spotify's API is probably forbidden by them.
|
Using this code to connect to Spotify's API is probably forbidden by them.
|
||||||
Use at your own risk.
|
Use at your own risk.
|
||||||
|
|
40
contrib/Dockerfile
Normal file
40
contrib/Dockerfile
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
# Cross compilation environment for librespot
|
||||||
|
# Build the docker image from the root of the project with the following command :
|
||||||
|
# $ docker build -t librespot-cross -f contrib/Dockerfile .
|
||||||
|
#
|
||||||
|
# The resulting image can be used to build librespot for linux x86_64, armhf and armel.
|
||||||
|
# $ docker run -v /tmp/librespot-build:/build librespot-cross
|
||||||
|
#
|
||||||
|
# The compiled binaries will be located in /tmp/librespot-build
|
||||||
|
#
|
||||||
|
# If only one architecture is desired, cargo can be invoked directly with the appropriate options :
|
||||||
|
# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --no-default-features --features "with-syntex alsa-backend"
|
||||||
|
# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "with-syntex alsa-backend"
|
||||||
|
# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features "with-syntex alsa-backend"
|
||||||
|
#
|
||||||
|
|
||||||
|
FROM debian:stretch
|
||||||
|
|
||||||
|
RUN dpkg --add-architecture armhf
|
||||||
|
RUN dpkg --add-architecture armel
|
||||||
|
RUN apt-get update
|
||||||
|
|
||||||
|
RUN apt-get install -y curl build-essential crossbuild-essential-armhf crossbuild-essential-armel
|
||||||
|
RUN apt-get install -y libasound2-dev libasound2-dev:armhf libasound2-dev:armel
|
||||||
|
|
||||||
|
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||||
|
ENV PATH="/root/.cargo/bin/:${PATH}"
|
||||||
|
RUN rustup target add arm-unknown-linux-gnueabi
|
||||||
|
RUN rustup target add arm-unknown-linux-gnueabihf
|
||||||
|
|
||||||
|
RUN mkdir /.cargo && \
|
||||||
|
echo '[target.arm-unknown-linux-gnueabihf]\nlinker = "arm-linux-gnueabihf-gcc"' > /.cargo/config && \
|
||||||
|
echo '[target.arm-unknown-linux-gnueabi]\nlinker = "arm-linux-gnueabi-gcc"' >> /.cargo/config
|
||||||
|
|
||||||
|
RUN mkdir /build
|
||||||
|
ENV CARGO_TARGET_DIR /build
|
||||||
|
ENV CARGO_HOME /build/cache
|
||||||
|
|
||||||
|
ADD . /src
|
||||||
|
WORKDIR /src
|
||||||
|
CMD ["/src/contrib/docker-build.sh"]
|
6
contrib/docker-build.sh
Executable file
6
contrib/docker-build.sh
Executable file
|
@ -0,0 +1,6 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eux
|
||||||
|
|
||||||
|
cargo build --release --no-default-features --features "with-syntex alsa-backend"
|
||||||
|
cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "with-syntex alsa-backend"
|
||||||
|
cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features "with-syntex alsa-backend"
|
|
@ -73,20 +73,19 @@ use self::pipe::StdoutSink;
|
||||||
|
|
||||||
declare_backends! {
|
declare_backends! {
|
||||||
pub const BACKENDS : &'static [
|
pub const BACKENDS : &'static [
|
||||||
(&'static str,
|
(&'static str, fn(Option<String>) -> Box<Sink>)
|
||||||
&'static (Fn(Option<String>) -> Box<Sink> + Sync + Send + 'static))
|
|
||||||
] = &[
|
] = &[
|
||||||
#[cfg(feature = "alsa-backend")]
|
#[cfg(feature = "alsa-backend")]
|
||||||
("alsa", &mk_sink::<AlsaSink>),
|
("alsa", mk_sink::<AlsaSink>),
|
||||||
#[cfg(feature = "portaudio-backend")]
|
#[cfg(feature = "portaudio-backend")]
|
||||||
("portaudio", &mk_sink::<PortAudioSink>),
|
("portaudio", mk_sink::<PortAudioSink>),
|
||||||
#[cfg(feature = "pulseaudio-backend")]
|
#[cfg(feature = "pulseaudio-backend")]
|
||||||
("pulseaudio", &mk_sink::<PulseAudioSink>),
|
("pulseaudio", mk_sink::<PulseAudioSink>),
|
||||||
("pipe", &mk_sink::<StdoutSink>),
|
("pipe", mk_sink::<StdoutSink>),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find<T: AsRef<str>>(name: Option<T>) -> Option<&'static (Fn(Option<String>) -> Box<Sink> + Send + Sync)> {
|
pub fn find<T: AsRef<str>>(name: Option<T>) -> Option<fn(Option<String>) -> Box<Sink>> {
|
||||||
if let Some(name) = name.as_ref().map(AsRef::as_ref) {
|
if let Some(name) = name.as_ref().map(AsRef::as_ref) {
|
||||||
BACKENDS.iter().find(|backend| name == backend.0).map(|backend| backend.1)
|
BACKENDS.iter().find(|backend| name == backend.0).map(|backend| backend.1)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -64,6 +64,7 @@ pub mod player;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
pub mod version;
|
pub mod version;
|
||||||
|
pub mod mixer;
|
||||||
|
|
||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/lib.rs"));
|
include!(concat!(env!("OUT_DIR"), "/lib.rs"));
|
||||||
|
|
22
src/main.rs
22
src/main.rs
|
@ -21,6 +21,8 @@ use librespot::audio_backend::{self, Sink, BACKENDS};
|
||||||
use librespot::cache::Cache;
|
use librespot::cache::Cache;
|
||||||
use librespot::player::Player;
|
use librespot::player::Player;
|
||||||
use librespot::session::{Bitrate, Config, Session};
|
use librespot::session::{Bitrate, Config, Session};
|
||||||
|
use librespot::mixer::{self, Mixer};
|
||||||
|
|
||||||
use librespot::version;
|
use librespot::version;
|
||||||
|
|
||||||
fn usage(program: &str, opts: &getopts::Options) -> String {
|
fn usage(program: &str, opts: &getopts::Options) -> String {
|
||||||
|
@ -62,9 +64,9 @@ fn list_backends() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct Setup {
|
struct Setup {
|
||||||
backend: &'static (Fn(Option<String>) -> Box<Sink> + Send + Sync),
|
backend: fn(Option<String>) -> Box<Sink>,
|
||||||
|
mixer: Box<Mixer + Send>,
|
||||||
cache: Option<Cache>,
|
cache: Option<Cache>,
|
||||||
config: Config,
|
config: Config,
|
||||||
credentials: Credentials,
|
credentials: Credentials,
|
||||||
|
@ -82,7 +84,8 @@ fn setup(args: &[String]) -> Setup {
|
||||||
.optopt("u", "username", "Username to sign in with", "USERNAME")
|
.optopt("u", "username", "Username to sign in with", "USERNAME")
|
||||||
.optopt("p", "password", "Password", "PASSWORD")
|
.optopt("p", "password", "Password", "PASSWORD")
|
||||||
.optopt("", "backend", "Audio backend to use. Use '?' to list options", "BACKEND")
|
.optopt("", "backend", "Audio backend to use. Use '?' to list options", "BACKEND")
|
||||||
.optopt("", "device", "Audio device to use. Use '?' to list options", "DEVICE");
|
.optopt("", "device", "Audio device to use. Use '?' to list options", "DEVICE")
|
||||||
|
.optopt("", "mixer", "Mixer to use", "MIXER");
|
||||||
|
|
||||||
let matches = match opts.parse(&args[1..]) {
|
let matches = match opts.parse(&args[1..]) {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
|
@ -109,6 +112,10 @@ fn setup(args: &[String]) -> Setup {
|
||||||
let backend = audio_backend::find(backend_name.as_ref())
|
let backend = audio_backend::find(backend_name.as_ref())
|
||||||
.expect("Invalid backend");
|
.expect("Invalid backend");
|
||||||
|
|
||||||
|
let mixer_name = matches.opt_str("mixer");
|
||||||
|
let mixer = mixer::find(mixer_name.as_ref())
|
||||||
|
.expect("Invalid mixer");
|
||||||
|
|
||||||
let bitrate = matches.opt_str("b").as_ref()
|
let bitrate = matches.opt_str("b").as_ref()
|
||||||
.map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate"))
|
.map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate"))
|
||||||
.unwrap_or(Bitrate::Bitrate160);
|
.unwrap_or(Bitrate::Bitrate160);
|
||||||
|
@ -140,6 +147,7 @@ fn setup(args: &[String]) -> Setup {
|
||||||
|
|
||||||
Setup {
|
Setup {
|
||||||
backend: backend,
|
backend: backend,
|
||||||
|
mixer: mixer,
|
||||||
cache: cache,
|
cache: cache,
|
||||||
config: config,
|
config: config,
|
||||||
credentials: credentials,
|
credentials: credentials,
|
||||||
|
@ -153,16 +161,18 @@ fn main() {
|
||||||
|
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let args: Vec<String> = std::env::args().collect();
|
||||||
|
|
||||||
let Setup { backend, cache, config, credentials, device } = setup(&args);
|
let Setup { backend, mixer, cache, config, credentials, device }
|
||||||
|
= setup(&args);
|
||||||
|
|
||||||
let connection = Session::connect(config, credentials, cache, handle);
|
let connection = Session::connect(config, credentials, cache, handle);
|
||||||
|
|
||||||
let task = connection.and_then(move |session| {
|
let task = connection.and_then(move |session| {
|
||||||
let player = Player::new(session.clone(), move || {
|
let audio_filter = mixer.get_audio_filter();
|
||||||
|
let player = Player::new(session.clone(), audio_filter, move || {
|
||||||
(backend)(device)
|
(backend)(device)
|
||||||
});
|
});
|
||||||
|
|
||||||
let (spirc, task) = Spirc::new(session.clone(), player);
|
let (spirc, task) = Spirc::new(session.clone(), player, mixer);
|
||||||
let spirc = ::std::cell::RefCell::new(spirc);
|
let spirc = ::std::cell::RefCell::new(spirc);
|
||||||
|
|
||||||
ctrlc::set_handler(move || {
|
ctrlc::set_handler(move || {
|
||||||
|
|
23
src/mixer/mod.rs
Normal file
23
src/mixer/mod.rs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
pub trait Mixer {
|
||||||
|
fn start(&self);
|
||||||
|
fn stop(&self);
|
||||||
|
fn set_volume(&self, volume: u16);
|
||||||
|
fn volume(&self) -> u16;
|
||||||
|
fn get_audio_filter(&self) -> Option<Box<AudioFilter + Send>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait AudioFilter {
|
||||||
|
fn modify_stream(&self, data: &mut [i16]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod softmixer;
|
||||||
|
use self::softmixer::SoftMixer;
|
||||||
|
|
||||||
|
pub fn find<T: AsRef<str>>(name: Option<T>) -> Option<Box<Mixer + Send>> {
|
||||||
|
match name.as_ref().map(AsRef::as_ref) {
|
||||||
|
None | Some("softvol") => Some(Box::new(SoftMixer::new())),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
48
src/mixer/softmixer.rs
Normal file
48
src/mixer/softmixer.rs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
|
||||||
|
use super::Mixer;
|
||||||
|
use super::AudioFilter;
|
||||||
|
|
||||||
|
pub struct SoftMixer {
|
||||||
|
volume: Arc<AtomicUsize>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SoftMixer {
|
||||||
|
pub fn new() -> SoftMixer {
|
||||||
|
SoftMixer {
|
||||||
|
volume: Arc::new(AtomicUsize::new(0xFFFF))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mixer for SoftMixer {
|
||||||
|
fn start(&self) {
|
||||||
|
}
|
||||||
|
fn stop(&self) {
|
||||||
|
}
|
||||||
|
fn volume(&self) -> u16 {
|
||||||
|
self.volume.load(Ordering::Relaxed) as u16
|
||||||
|
}
|
||||||
|
fn set_volume(&self, volume: u16) {
|
||||||
|
self.volume.store(volume as usize, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
fn get_audio_filter(&self) -> Option<Box<AudioFilter + Send>> {
|
||||||
|
Some(Box::new(SoftVolumeApplier { volume: self.volume.clone() }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SoftVolumeApplier {
|
||||||
|
volume: Arc<AtomicUsize>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioFilter for SoftVolumeApplier {
|
||||||
|
fn modify_stream(&self, data: &mut [i16]) {
|
||||||
|
let volume = self.volume.load(Ordering::Relaxed) as u16;
|
||||||
|
if volume != 0xFFFF {
|
||||||
|
for x in data.iter_mut() {
|
||||||
|
*x = (*x as i32 * volume as i32 / 0xFFFF) as i16;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ use audio_decrypt::AudioDecrypt;
|
||||||
use audio_file::AudioFile;
|
use audio_file::AudioFile;
|
||||||
use metadata::{FileFormat, Track};
|
use metadata::{FileFormat, Track};
|
||||||
use session::{Bitrate, Session};
|
use session::{Bitrate, Session};
|
||||||
|
use mixer::AudioFilter;
|
||||||
use util::{self, SpotifyId, Subfile};
|
use util::{self, SpotifyId, Subfile};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -25,21 +26,20 @@ struct PlayerInternal {
|
||||||
commands: std::sync::mpsc::Receiver<PlayerCommand>,
|
commands: std::sync::mpsc::Receiver<PlayerCommand>,
|
||||||
|
|
||||||
state: PlayerState,
|
state: PlayerState,
|
||||||
volume: u16,
|
|
||||||
sink: Box<Sink>,
|
sink: Box<Sink>,
|
||||||
|
audio_filter: Option<Box<AudioFilter + Send>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PlayerCommand {
|
enum PlayerCommand {
|
||||||
Load(SpotifyId, bool, u32, oneshot::Sender<()>),
|
Load(SpotifyId, bool, u32, oneshot::Sender<()>),
|
||||||
Play,
|
Play,
|
||||||
Pause,
|
Pause,
|
||||||
Volume(u16),
|
|
||||||
Stop,
|
Stop,
|
||||||
Seek(u32),
|
Seek(u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Player {
|
impl Player {
|
||||||
pub fn new<F>(session: Session, sink_builder: F) -> Player
|
pub fn new<F>(session: Session, audio_filter: Option<Box<AudioFilter + Send>>, sink_builder: F) -> Player
|
||||||
where F: FnOnce() -> Box<Sink> + Send + 'static {
|
where F: FnOnce() -> Box<Sink> + Send + 'static {
|
||||||
let (cmd_tx, cmd_rx) = std::sync::mpsc::channel();
|
let (cmd_tx, cmd_rx) = std::sync::mpsc::channel();
|
||||||
|
|
||||||
|
@ -49,8 +49,8 @@ impl Player {
|
||||||
commands: cmd_rx,
|
commands: cmd_rx,
|
||||||
|
|
||||||
state: PlayerState::Stopped,
|
state: PlayerState::Stopped,
|
||||||
volume: 0xFFFF,
|
|
||||||
sink: sink_builder(),
|
sink: sink_builder(),
|
||||||
|
audio_filter: audio_filter,
|
||||||
};
|
};
|
||||||
|
|
||||||
internal.run();
|
internal.run();
|
||||||
|
@ -89,10 +89,6 @@ impl Player {
|
||||||
pub fn seek(&self, position_ms: u32) {
|
pub fn seek(&self, position_ms: u32) {
|
||||||
self.command(PlayerCommand::Seek(position_ms));
|
self.command(PlayerCommand::Seek(position_ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn volume(&self, vol: u16) {
|
|
||||||
self.command(PlayerCommand::Volume(vol));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Decoder = vorbis::Decoder<Subfile<AudioDecrypt<AudioFile>>>;
|
type Decoder = vorbis::Decoder<Subfile<AudioDecrypt<AudioFile>>>;
|
||||||
|
@ -203,11 +199,9 @@ impl PlayerInternal {
|
||||||
fn handle_packet(&mut self, packet: Option<Result<vorbis::Packet, VorbisError>>) {
|
fn handle_packet(&mut self, packet: Option<Result<vorbis::Packet, VorbisError>>) {
|
||||||
match packet {
|
match packet {
|
||||||
Some(Ok(mut packet)) => {
|
Some(Ok(mut packet)) => {
|
||||||
if self.volume < 0xFFFF {
|
if let Some(ref editor) = self.audio_filter {
|
||||||
for x in &mut packet.data {
|
editor.modify_stream(&mut packet.data)
|
||||||
*x = (*x as i32 * self.volume as i32 / 0xFFFF) as i16;
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.sink.write(&packet.data).unwrap();
|
self.sink.write(&packet.data).unwrap();
|
||||||
}
|
}
|
||||||
|
@ -313,10 +307,6 @@ impl PlayerInternal {
|
||||||
PlayerState::Invalid => panic!("invalid state"),
|
PlayerState::Invalid => panic!("invalid state"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PlayerCommand::Volume(vol) => {
|
|
||||||
self.volume = vol;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -419,11 +409,6 @@ impl ::std::fmt::Debug for PlayerCommand {
|
||||||
PlayerCommand::Pause => {
|
PlayerCommand::Pause => {
|
||||||
f.debug_tuple("Pause").finish()
|
f.debug_tuple("Pause").finish()
|
||||||
}
|
}
|
||||||
PlayerCommand::Volume(volume) => {
|
|
||||||
f.debug_tuple("Volume")
|
|
||||||
.field(&volume)
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
PlayerCommand::Stop => {
|
PlayerCommand::Stop => {
|
||||||
f.debug_tuple("Stop").finish()
|
f.debug_tuple("Stop").finish()
|
||||||
}
|
}
|
||||||
|
|
77
src/spirc.rs
77
src/spirc.rs
|
@ -7,6 +7,7 @@ use protobuf::{self, Message};
|
||||||
|
|
||||||
use mercury::MercuryError;
|
use mercury::MercuryError;
|
||||||
use player::Player;
|
use player::Player;
|
||||||
|
use mixer::Mixer;
|
||||||
use session::Session;
|
use session::Session;
|
||||||
use util::{now_ms, SpotifyId, SeqGenerator};
|
use util::{now_ms, SpotifyId, SeqGenerator};
|
||||||
use version;
|
use version;
|
||||||
|
@ -16,6 +17,7 @@ use protocol::spirc::{PlayStatus, State, MessageType, Frame, DeviceState};
|
||||||
|
|
||||||
pub struct SpircTask {
|
pub struct SpircTask {
|
||||||
player: Player,
|
player: Player,
|
||||||
|
mixer: Box<Mixer + Send>,
|
||||||
|
|
||||||
sequence: SeqGenerator<u32>,
|
sequence: SeqGenerator<u32>,
|
||||||
|
|
||||||
|
@ -43,7 +45,6 @@ fn initial_state() -> State {
|
||||||
protobuf_init!(protocol::spirc::State::new(), {
|
protobuf_init!(protocol::spirc::State::new(), {
|
||||||
repeat: false,
|
repeat: false,
|
||||||
shuffle: false,
|
shuffle: false,
|
||||||
|
|
||||||
status: PlayStatus::kPlayStatusStop,
|
status: PlayStatus::kPlayStatusStop,
|
||||||
position_ms: 0,
|
position_ms: 0,
|
||||||
position_measured_at: 0,
|
position_measured_at: 0,
|
||||||
|
@ -109,7 +110,9 @@ fn initial_device_state(name: String, volume: u16) -> DeviceState {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Spirc {
|
impl Spirc {
|
||||||
pub fn new(session: Session, player: Player) -> (Spirc, SpircTask) {
|
pub fn new(session: Session, player: Player, mixer: Box<Mixer + Send>)
|
||||||
|
-> (Spirc, SpircTask)
|
||||||
|
{
|
||||||
let ident = session.device_id().to_owned();
|
let ident = session.device_id().to_owned();
|
||||||
let name = session.config().name.clone();
|
let name = session.config().name.clone();
|
||||||
|
|
||||||
|
@ -130,10 +133,11 @@ impl Spirc {
|
||||||
|
|
||||||
let volume = 0xFFFF;
|
let volume = 0xFFFF;
|
||||||
let device = initial_device_state(name, volume);
|
let device = initial_device_state(name, volume);
|
||||||
player.volume(volume);
|
mixer.set_volume(volume);
|
||||||
|
|
||||||
let mut task = SpircTask {
|
let mut task = SpircTask {
|
||||||
player: player,
|
player: player,
|
||||||
|
mixer: mixer,
|
||||||
|
|
||||||
sequence: SeqGenerator::new(1),
|
sequence: SeqGenerator::new(1),
|
||||||
|
|
||||||
|
@ -269,6 +273,7 @@ impl SpircTask {
|
||||||
|
|
||||||
MessageType::kMessageTypePlay => {
|
MessageType::kMessageTypePlay => {
|
||||||
if self.state.get_status() == PlayStatus::kPlayStatusPause {
|
if self.state.get_status() == PlayStatus::kPlayStatusPause {
|
||||||
|
self.mixer.start();
|
||||||
self.player.play();
|
self.player.play();
|
||||||
self.state.set_status(PlayStatus::kPlayStatusPlay);
|
self.state.set_status(PlayStatus::kPlayStatusPlay);
|
||||||
self.state.set_position_measured_at(now_ms() as u64);
|
self.state.set_position_measured_at(now_ms() as u64);
|
||||||
|
@ -280,6 +285,7 @@ impl SpircTask {
|
||||||
MessageType::kMessageTypePause => {
|
MessageType::kMessageTypePause => {
|
||||||
if self.state.get_status() == PlayStatus::kPlayStatusPlay {
|
if self.state.get_status() == PlayStatus::kPlayStatusPlay {
|
||||||
self.player.pause();
|
self.player.pause();
|
||||||
|
self.mixer.stop();
|
||||||
self.state.set_status(PlayStatus::kPlayStatusPause);
|
self.state.set_status(PlayStatus::kPlayStatusPause);
|
||||||
|
|
||||||
let now = now_ms() as u64;
|
let now = now_ms() as u64;
|
||||||
|
@ -349,7 +355,7 @@ impl SpircTask {
|
||||||
MessageType::kMessageTypeVolume => {
|
MessageType::kMessageTypeVolume => {
|
||||||
let volume = frame.get_volume();
|
let volume = frame.get_volume();
|
||||||
self.device.set_volume(volume);
|
self.device.set_volume(volume);
|
||||||
self.player.volume(volume as u16);
|
self.mixer.set_volume(frame.get_volume() as u16);
|
||||||
self.notify(None);
|
self.notify(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -360,6 +366,7 @@ impl SpircTask {
|
||||||
self.device.set_is_active(false);
|
self.device.set_is_active(false);
|
||||||
self.state.set_status(PlayStatus::kPlayStatusStop);
|
self.state.set_status(PlayStatus::kPlayStatusStop);
|
||||||
self.player.stop();
|
self.player.stop();
|
||||||
|
self.mixer.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -398,7 +405,6 @@ impl SpircTask {
|
||||||
let gid = self.state.get_track()[index as usize].get_gid();
|
let gid = self.state.get_track()[index as usize].get_gid();
|
||||||
SpotifyId::from_raw(gid)
|
SpotifyId::from_raw(gid)
|
||||||
};
|
};
|
||||||
|
|
||||||
let position = self.state.get_position_ms();
|
let position = self.state.get_position_ms();
|
||||||
|
|
||||||
let end_of_track = self.player.load(track, play, position);
|
let end_of_track = self.player.load(track, play, position);
|
||||||
|
@ -423,52 +429,49 @@ impl SpircTask {
|
||||||
}
|
}
|
||||||
cs.send();
|
cs.send();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spirc_state(&self) -> protocol::spirc::State {
|
|
||||||
self.state.clone()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CommandSender<'a> {
|
struct CommandSender<'a> {
|
||||||
spirc: &'a mut SpircTask,
|
spirc: &'a mut SpircTask,
|
||||||
cmd: MessageType,
|
frame: protocol::spirc::Frame,
|
||||||
recipient: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> CommandSender<'a> {
|
impl<'a> CommandSender<'a> {
|
||||||
fn new(spirc: &'a mut SpircTask, cmd: MessageType) -> CommandSender {
|
fn new(spirc: &'a mut SpircTask, cmd: MessageType) -> CommandSender {
|
||||||
CommandSender {
|
let frame = protobuf_init!(protocol::spirc::Frame::new(), {
|
||||||
spirc: spirc,
|
|
||||||
cmd: cmd,
|
|
||||||
recipient: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn recipient(mut self, r: &str) -> CommandSender<'a> {
|
|
||||||
self.recipient = Some(r.to_owned());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send(self) {
|
|
||||||
let mut frame = protobuf_init!(Frame::new(), {
|
|
||||||
version: 1,
|
version: 1,
|
||||||
ident: self.spirc.ident.clone(),
|
|
||||||
protocol_version: "2.0.0",
|
protocol_version: "2.0.0",
|
||||||
seq_nr: self.spirc.sequence.get(),
|
ident: spirc.ident.clone(),
|
||||||
typ: self.cmd,
|
seq_nr: spirc.sequence.get(),
|
||||||
device_state: self.spirc.device.clone(),
|
typ: cmd,
|
||||||
|
|
||||||
|
device_state: spirc.device.clone(),
|
||||||
state_update_id: now_ms(),
|
state_update_id: now_ms(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(recipient) = self.recipient {
|
CommandSender {
|
||||||
frame.mut_recipient().push(recipient.to_owned());
|
spirc: spirc,
|
||||||
|
frame: frame,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recipient(mut self, recipient: &'a str) -> CommandSender {
|
||||||
|
self.frame.mut_recipient().push(recipient.to_owned());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn state(mut self, state: protocol::spirc::State) -> CommandSender<'a> {
|
||||||
|
self.frame.set_state(state);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send(mut self) {
|
||||||
|
if !self.frame.has_state() && self.spirc.device.get_is_active() {
|
||||||
|
self.frame.set_state(self.spirc.state.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.spirc.device.get_is_active() {
|
let send = self.spirc.sender.start_send(self.frame).unwrap();
|
||||||
frame.set_state(self.spirc.spirc_state());
|
assert!(send.is_ready());
|
||||||
}
|
|
||||||
|
|
||||||
let ready = self.spirc.sender.start_send(frame).unwrap().is_ready();
|
|
||||||
assert!(ready);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue