diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c20fe1c6..9d1250f9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,7 @@ jobs: matrix: os: [ubuntu-latest] toolchain: - - 1.42.0 # MSRV (Minimum supported rust version) + - 1.41.1 # MSRV (Minimum supported rust version) - stable - beta experimental: [false] diff --git a/COMPILING.md b/COMPILING.md index 40eefb39..4320cdbb 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -13,7 +13,7 @@ curl https://sh.rustup.rs -sSf | sh Follow any prompts it gives you to install Rust. Once that’s done, Rust's standard tools should be setup and ready to use. -*Note: The current minimum required Rust version at the time of writing is 1.40.0, you can find the current minimum version specified in the `.github/workflow/test.yml` file.* +*Note: The current minimum required Rust version at the time of writing is 1.41, you can find the current minimum version specified in the `.github/workflow/test.yml` file.* #### Additional Rust tools - `rustfmt` To ensure a consistent codebase, we utilise [`rustfmt`](https://github.com/rust-lang/rustfmt), which is installed by default with `rustup` these days, else it can be installed manually with: diff --git a/Cargo.lock b/Cargo.lock index 7908dde9..a43939d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -139,9 +139,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "backtrace" -version = "0.3.56" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d117600f438b1707d4e4ae15d3595657288f8235a0eb593e80ecc98ab34e1bc" +checksum = "ef5140344c85b01f9bbb4d4b7288a8aa4b3287ccef913a14bcc78a1063623598" dependencies = [ "addr2line", "cfg-if 1.0.0", @@ -1718,9 +1718,9 @@ dependencies = [ [[package]] name = "object" -version = "0.23.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4" +checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397" [[package]] name = "oboe" diff --git a/audio/src/passthrough_decoder.rs b/audio/src/passthrough_decoder.rs index 25802e4b..d519baf8 100644 --- a/audio/src/passthrough_decoder.rs +++ b/audio/src/passthrough_decoder.rs @@ -5,75 +5,32 @@ use std::fmt; use std::io::{Read, Seek}; use std::time::{SystemTime, UNIX_EPOCH}; -fn write_headers( - rdr: &mut PacketReader, - wtr: &mut PacketWriter>, -) -> Result { - let mut stream_serial: u32 = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as u32; - - // search for ident, comment, setup - get_header(1, rdr, wtr, &mut stream_serial, PacketWriteEndInfo::EndPage)?; - get_header( - 3, - rdr, - wtr, - &mut stream_serial, - PacketWriteEndInfo::NormalPacket, - )?; - get_header(5, rdr, wtr, &mut stream_serial, PacketWriteEndInfo::EndPage)?; - - // remove un-needed packets - rdr.delete_unread_packets(); - Ok(stream_serial) -} - -fn get_header( - code: u8, - rdr: &mut PacketReader, - wtr: &mut PacketWriter>, - stream_serial: &mut u32, - info: PacketWriteEndInfo, -) -> Result +fn get_header(code: u8, rdr: &mut PacketReader) -> Result, PassthroughError> where T: Read + Seek, { let pck: Packet = rdr.read_packet_expected()?; - // set a unique serial number - if pck.stream_serial() != 0 { - *stream_serial = pck.stream_serial(); - } - let pkt_type = pck.data[0]; debug!("Vorbis header type{}", &pkt_type); - // all headers are mandatory if pkt_type != code { return Err(PassthroughError(OggReadError::InvalidData)); } - // headers keep original granule number - let absgp_page = pck.absgp_page(); - wtr.write_packet( - pck.data.into_boxed_slice(), - *stream_serial, - info, - absgp_page, - ) - .unwrap(); - - Ok(*stream_serial) + Ok(pck.data.into_boxed_slice()) } pub struct PassthroughDecoder { rdr: PacketReader, wtr: PacketWriter>, - lastgp_page: Option, - absgp_page: u64, + eos: bool, + bos: bool, + ofsgp_page: u64, stream_serial: u32, + ident: Box<[u8]>, + comment: Box<[u8]>, + setup: Box<[u8]>, } pub struct PassthroughError(ogg::OggReadError); @@ -82,17 +39,31 @@ impl PassthroughDecoder { /// Constructs a new Decoder from a given implementation of `Read + Seek`. pub fn new(rdr: R) -> Result { let mut rdr = PacketReader::new(rdr); - let mut wtr = PacketWriter::new(Vec::new()); + let stream_serial = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as u32; - let stream_serial = write_headers(&mut rdr, &mut wtr)?; info!("Starting passthrough track with serial {}", stream_serial); + // search for ident, comment, setup + let ident = get_header(1, &mut rdr)?; + let comment = get_header(3, &mut rdr)?; + let setup = get_header(5, &mut rdr)?; + + // remove un-needed packets + rdr.delete_unread_packets(); + Ok(PassthroughDecoder { rdr, - wtr, - lastgp_page: Some(0), - absgp_page: 0, + wtr: PacketWriter::new(Vec::new()), + ofsgp_page: 0, stream_serial, + ident, + comment, + setup, + eos: false, + bos: false, }) } } @@ -100,52 +71,94 @@ impl PassthroughDecoder { impl AudioDecoder for PassthroughDecoder { fn seek(&mut self, ms: i64) -> Result<(), AudioError> { info!("Seeking to {}", ms); - self.lastgp_page = match ms { - 0 => Some(0), - _ => None, - }; + + // add an eos to previous stream if missing + if self.bos && !self.eos { + match self.rdr.read_packet() { + Ok(Some(pck)) => { + let absgp_page = pck.absgp_page() - self.ofsgp_page; + self.wtr + .write_packet( + pck.data.into_boxed_slice(), + self.stream_serial, + PacketWriteEndInfo::EndStream, + absgp_page, + ) + .unwrap(); + } + _ => warn! {"Cannot write EoS after seeking"}, + }; + } + + self.eos = false; + self.bos = false; + self.ofsgp_page = 0; + self.stream_serial += 1; // hard-coded to 44.1 kHz match self.rdr.seek_absgp(None, (ms * 44100 / 1000) as u64) { - Ok(_) => Ok(()), + Ok(_) => { + // need to set some offset for next_page() + let pck = self.rdr.read_packet().unwrap().unwrap(); + self.ofsgp_page = pck.absgp_page(); + debug!("Seek to offset page {}", self.ofsgp_page); + Ok(()) + } Err(err) => Err(AudioError::PassthroughError(err.into())), } } fn next_packet(&mut self) -> Result, AudioError> { - let mut skip = self.lastgp_page.is_none(); + // write headers if we are (re)starting + if self.bos == false { + self.wtr + .write_packet( + self.ident.clone(), + self.stream_serial, + PacketWriteEndInfo::EndPage, + 0, + ) + .unwrap(); + self.wtr + .write_packet( + self.comment.clone(), + self.stream_serial, + PacketWriteEndInfo::NormalPacket, + 0, + ) + .unwrap(); + self.wtr + .write_packet( + self.setup.clone(), + self.stream_serial, + PacketWriteEndInfo::EndPage, + 0, + ) + .unwrap(); + self.bos = true; + debug!("Wrote Ogg headers"); + } + loop { let pck = match self.rdr.read_packet() { Ok(Some(pck)) => pck, - Ok(None) | Err(OggReadError::NoCapturePatternFound) => { info!("end of streaming"); return Ok(None); } - Err(err) => return Err(AudioError::PassthroughError(err.into())), }; let pckgp_page = pck.absgp_page(); - let lastgp_page = self.lastgp_page.get_or_insert(pckgp_page); - // consume packets till next page to get a granule reference - if skip { - if *lastgp_page == pckgp_page { - debug!("skipping packet"); - continue; - } - skip = false; - info!("skipped at {}", pckgp_page); + // skip till we have audio and a calculable granule position + if pckgp_page == 0 || pckgp_page == self.ofsgp_page { + continue; } - // now we can calculate absolute granule - self.absgp_page += pckgp_page - *lastgp_page; - self.lastgp_page = Some(pckgp_page); - // set packet type let inf = if pck.last_in_stream() { - self.lastgp_page = Some(0); + self.eos = true; PacketWriteEndInfo::EndStream } else if pck.last_in_page() { PacketWriteEndInfo::EndPage @@ -158,7 +171,7 @@ impl AudioDecoder for PassthroughDecoder { pck.data.into_boxed_slice(), self.stream_serial, inf, - self.absgp_page, + pckgp_page - self.ofsgp_page, ) .unwrap(); diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index f111e541..1a7f64ec 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -106,7 +106,7 @@ fn initial_state() -> State { fn initial_device_state(config: ConnectConfig) -> DeviceState { { let mut msg = DeviceState::new(); - msg.set_sw_version(version::version_string()); + msg.set_sw_version(version::VERSION_STRING.to_string()); msg.set_is_active(false); msg.set_can_play(true); msg.set_volume(0); diff --git a/core/build.rs b/core/build.rs index 0fc29335..8e61c912 100644 --- a/core/build.rs +++ b/core/build.rs @@ -12,5 +12,6 @@ fn main() { .take(8) .map(char::from) .collect(); - println!("cargo:rustc-env=VERGEN_BUILD_ID={}", build_id); + + println!("cargo:rustc-env=LIBRESPOT_BUILD_ID={}", build_id); } diff --git a/core/src/config.rs b/core/src/config.rs index 26924640..a9e5ea2c 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -14,7 +14,7 @@ impl Default for SessionConfig { fn default() -> SessionConfig { let device_id = uuid::Uuid::new_v4().to_hyphenated().to_string(); SessionConfig { - user_agent: crate::version::version_string(), + user_agent: crate::version::VERSION_STRING.to_string(), device_id, proxy: None, ap_port: None, diff --git a/core/src/connection/mod.rs b/core/src/connection/mod.rs index ab353669..56d579bd 100644 --- a/core/src/connection/mod.rs +++ b/core/src/connection/mod.rs @@ -131,13 +131,13 @@ pub async fn authenticate( .mut_system_info() .set_system_information_string(format!( "librespot_{}_{}", - version::short_sha(), - version::build_id() + version::SHA_SHORT, + version::BUILD_ID )); packet .mut_system_info() .set_device_id(device_id.to_string()); - packet.set_version_string(version::version_string()); + packet.set_version_string(version::VERSION_STRING.to_string()); let cmd = 0xab; let data = packet.write_to_bytes().unwrap(); diff --git a/core/src/version.rs b/core/src/version.rs index cd7fa042..ef553463 100644 --- a/core/src/version.rs +++ b/core/src/version.rs @@ -1,44 +1,17 @@ -pub fn version_string() -> String { - format!("librespot-{}", short_sha()) -} +/// Version string of the form "librespot-" +pub const VERSION_STRING: &str = concat!("librespot-", env!("VERGEN_SHA_SHORT")); -// Generate a timestamp representing now (UTC) in RFC3339 format. -pub fn now() -> &'static str { - env!("VERGEN_BUILD_TIMESTAMP") -} +/// Generate a timestamp string representing the build date (UTC). +pub const BUILD_DATE: &str = env!("VERGEN_BUILD_DATE"); -// Generate a timstamp string representing now (UTC). -pub fn short_now() -> &'static str { - env!("VERGEN_BUILD_DATE") -} +/// Short sha of the latest git commit. +pub const SHA_SHORT: &str = env!("VERGEN_SHA_SHORT"); -// Generate a SHA string -pub fn sha() -> &'static str { - env!("VERGEN_SHA") -} +/// Date of the latest git commit. +pub const COMMIT_DATE: &str = env!("VERGEN_COMMIT_DATE"); -// Generate a short SHA string -pub fn short_sha() -> &'static str { - env!("VERGEN_SHA_SHORT") -} +/// Librespot crate version. +pub const SEMVER: &str = env!("CARGO_PKG_VERSION"); -// Generate the commit date string -pub fn commit_date() -> &'static str { - env!("VERGEN_COMMIT_DATE") -} - -// Generate the target triple string -pub fn target() -> &'static str { - env!("VERGEN_TARGET_TRIPLE") -} - -// Generate a semver string -pub fn semver() -> &'static str { - // env!("VERGEN_SEMVER") - env!("CARGO_PKG_VERSION") -} - -// Generate a random build id. -pub fn build_id() -> &'static str { - env!("VERGEN_BUILD_ID") -} +/// A random build id. +pub const BUILD_ID: &str = env!("LIBRESPOT_BUILD_ID"); diff --git a/src/main.rs b/src/main.rs index 5b445b2c..1f67793e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -93,6 +93,16 @@ pub fn get_credentials Option>( } } +fn print_version() { + println!( + "librespot {semver} {sha} (Built on {build_date}, Build ID: {build_id})", + semver = version::SEMVER, + sha = version::SHA_SHORT, + build_date = version::BUILD_DATE, + build_id = version::BUILD_ID + ); +} + #[derive(Clone)] struct Setup { backend: fn(Option) -> Box, @@ -125,7 +135,7 @@ fn setup(args: &[String]) -> Setup { "Path to a directory where system files (credentials, volume) will be cached. Can be different from cache option value", "SYTEMCACHE", ).optflag("", "disable-audio-cache", "Disable caching of the audio data.") - .reqopt("n", "name", "Device name", "NAME") + .optopt("n", "name", "Device name", "NAME") .optopt("", "device-type", "Displayed device type", "DEVICE_TYPE") .optopt( "b", @@ -141,6 +151,7 @@ fn setup(args: &[String]) -> Setup { ) .optflag("", "emit-sink-events", "Run program set by --onevent before sink is opened and after it is closed.") .optflag("v", "verbose", "Enable verbose output") + .optflag("V", "version", "Display librespot version string") .optopt("u", "username", "Username to sign in with", "USERNAME") .optopt("p", "password", "Password", "PASSWORD") .optopt("", "proxy", "HTTP proxy to use when connecting", "PROXY") @@ -241,15 +252,20 @@ fn setup(args: &[String]) -> Setup { } }; + if matches.opt_present("version") { + print_version(); + exit(0); + } + let verbose = matches.opt_present("verbose"); setup_logging(verbose); info!( - "librespot {} ({}). Built on {}. Build ID: {}", - version::short_sha(), - version::commit_date(), - version::short_now(), - version::build_id() + "librespot {semver} {sha} (Built on {build_date}, Build ID: {build_id})", + semver = version::SEMVER, + sha = version::SHA_SHORT, + build_date = version::BUILD_DATE, + build_id = version::BUILD_ID ); let backend_name = matches.opt_str("backend"); @@ -329,7 +345,7 @@ fn setup(args: &[String]) -> Setup { .map(|port| port.parse::().unwrap()) .unwrap_or(0); - let name = matches.opt_str("name").unwrap(); + let name = matches.opt_str("name").unwrap_or("Librespot".to_string()); let credentials = { let cached_credentials = cache.as_ref().and_then(Cache::credentials); @@ -352,7 +368,7 @@ fn setup(args: &[String]) -> Setup { let device_id = device_id(&name); SessionConfig { - user_agent: version::version_string(), + user_agent: version::VERSION_STRING.to_string(), device_id, proxy: matches.opt_str("proxy").or_else(|| std::env::var("http_proxy").ok()).map( |s| {