diff --git a/Cargo.toml b/Cargo.toml index aca8607c..62fc1ab8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ description = "Audio playback library" keywords = ["audio", "playback", "gamedev"] repository = "https://github.com/RustAudio/rodio" documentation = "http://docs.rs/rodio" +autoexamples = true [dependencies] claxon = { version = "0.4.2", optional = true } @@ -15,6 +16,7 @@ hound = { version = "3.3.1", optional = true } lazy_static = "1.0.0" lewton = { version = "0.10", optional = true } minimp3 = { version = "0.3.2", optional = true } +reqwest = { version = "0.10.0", default-features = false, features = ["blocking"], optional = true } [features] default = ["flac", "vorbis", "wav", "mp3"] @@ -23,3 +25,26 @@ flac = ["claxon"] vorbis = ["lewton"] wav = ["hound"] mp3 = ["minimp3"] +http = ["reqwest"] +https-rustls = ["http", "reqwest/rustls-tls"] +https-native = ["http", "reqwest/native-tls"] + +[[example]] +name = "http_flac" +path = "examples/http_flac.rs" +required-features = ["https-rustls"] + +[[example]] +name = "http_mp3" +path = "examples/http_mp3.rs" +required-features = ["https-rustls"] + +[[example]] +name = "http_ogg" +path = "examples/http_ogg.rs" +required-features = ["https-rustls"] + +[[example]] +name = "http_wav" +path = "examples/http_wav.rs" +required-features = ["https-rustls"] diff --git a/examples/http_flac.rs b/examples/http_flac.rs new file mode 100644 index 00000000..495e98fa --- /dev/null +++ b/examples/http_flac.rs @@ -0,0 +1,12 @@ +fn main() { + let device = rodio::default_output_device().unwrap(); + let sink = rodio::Sink::new(&device); + + let url = "https://github.com/RustAudio/rodio/raw/master/examples/music.flac"; + let request = rodio::SeekableRequest::get(url); + let buffer = rodio::SeekableBufReader::new(request); + let source = rodio::Decoder::new(buffer).unwrap(); + + sink.append(source); + sink.sleep_until_end(); +} diff --git a/examples/http_mp3.rs b/examples/http_mp3.rs new file mode 100644 index 00000000..9fb131c2 --- /dev/null +++ b/examples/http_mp3.rs @@ -0,0 +1,12 @@ +fn main() { + let device = rodio::default_output_device().unwrap(); + let sink = rodio::Sink::new(&device); + + let url = "https://github.com/RustAudio/rodio/raw/master/examples/music.mp3"; + let request = rodio::SeekableRequest::get(url); + let buffer = rodio::SeekableBufReader::new(request); + let source = rodio::Decoder::new(buffer).unwrap(); + + sink.append(source); + sink.sleep_until_end(); +} diff --git a/examples/http_ogg.rs b/examples/http_ogg.rs new file mode 100644 index 00000000..283cb80b --- /dev/null +++ b/examples/http_ogg.rs @@ -0,0 +1,12 @@ +fn main() { + let device = rodio::default_output_device().unwrap(); + let sink = rodio::Sink::new(&device); + + let url = "https://github.com/RustAudio/rodio/raw/master/examples/music.ogg"; + let request = rodio::SeekableRequest::get(url); + let buffer = rodio::SeekableBufReader::new(request); + let source = rodio::Decoder::new(buffer).unwrap(); + + sink.append(source); + sink.sleep_until_end(); +} diff --git a/examples/http_wav.rs b/examples/http_wav.rs new file mode 100644 index 00000000..df52c665 --- /dev/null +++ b/examples/http_wav.rs @@ -0,0 +1,12 @@ +fn main() { + let device = rodio::default_output_device().unwrap(); + let sink = rodio::Sink::new(&device); + + let url = "https://github.com/RustAudio/rodio/raw/master/examples/music.wav"; + let request = rodio::SeekableRequest::get(url); + let buffer = rodio::SeekableBufReader::new(request); + let source = rodio::Decoder::new(buffer).unwrap(); + + sink.append(source); + sink.sleep_until_end(); +} diff --git a/examples/music_flac.rs b/examples/music_flac.rs index 28596103..976ae604 100644 --- a/examples/music_flac.rs +++ b/examples/music_flac.rs @@ -1,13 +1,14 @@ -extern crate rodio; - -use std::io::BufReader; +use std::{fs::File, io::BufReader}; fn main() { - let device = rodio::default_output_device().unwrap(); - let sink = rodio::Sink::new(&device); + let device = rodio::default_output_device().unwrap(); + let sink = rodio::Sink::new(&device); - let file = std::fs::File::open("examples/music.flac").unwrap(); - sink.append(rodio::Decoder::new(BufReader::new(file)).unwrap()); + let path = "examples/music.flac"; + let file = File::open(path).unwrap(); + let buffer = BufReader::new(file); + let source = rodio::Decoder::new(buffer).unwrap(); - sink.sleep_until_end(); + sink.append(source); + sink.sleep_until_end(); } diff --git a/examples/music_mp3.rs b/examples/music_mp3.rs index a77d2486..923977dd 100644 --- a/examples/music_mp3.rs +++ b/examples/music_mp3.rs @@ -1,13 +1,14 @@ -extern crate rodio; - -use std::io::BufReader; +use std::{fs::File, io::BufReader}; fn main() { - let device = rodio::default_output_device().unwrap(); - let sink = rodio::Sink::new(&device); + let device = rodio::default_output_device().unwrap(); + let sink = rodio::Sink::new(&device); - let file = std::fs::File::open("examples/music.mp3").unwrap(); - sink.append(rodio::Decoder::new(BufReader::new(file)).unwrap()); + let path = "examples/music.mp3"; + let file = File::open(path).unwrap(); + let buffer = BufReader::new(file); + let source = rodio::Decoder::new(buffer).unwrap(); - sink.sleep_until_end(); + sink.append(source); + sink.sleep_until_end(); } diff --git a/examples/music_ogg.rs b/examples/music_ogg.rs index e31e421e..ccc67600 100644 --- a/examples/music_ogg.rs +++ b/examples/music_ogg.rs @@ -1,13 +1,14 @@ -extern crate rodio; - -use std::io::BufReader; +use std::{fs::File, io::BufReader}; fn main() { - let device = rodio::default_output_device().unwrap(); - let sink = rodio::Sink::new(&device); + let device = rodio::default_output_device().unwrap(); + let sink = rodio::Sink::new(&device); - let file = std::fs::File::open("examples/music.ogg").unwrap(); - sink.append(rodio::Decoder::new(BufReader::new(file)).unwrap()); + let path = "examples/music.ogg"; + let file = File::open(path).unwrap(); + let buffer = BufReader::new(file); + let source = rodio::Decoder::new(buffer).unwrap(); - sink.sleep_until_end(); + sink.append(source); + sink.sleep_until_end(); } diff --git a/examples/music_wav.rs b/examples/music_wav.rs index eb1e13ef..6872dafd 100644 --- a/examples/music_wav.rs +++ b/examples/music_wav.rs @@ -1,13 +1,14 @@ -extern crate rodio; - -use std::io::BufReader; +use std::{fs::File, io::BufReader}; fn main() { - let device = rodio::default_output_device().unwrap(); - let sink = rodio::Sink::new(&device); + let device = rodio::default_output_device().unwrap(); + let sink = rodio::Sink::new(&device); - let file = std::fs::File::open("examples/music.wav").unwrap(); - sink.append(rodio::Decoder::new(BufReader::new(file)).unwrap()); + let path = "examples/music.wav"; + let file = File::open(path).unwrap(); + let buffer = BufReader::new(file); + let source = rodio::Decoder::new(buffer).unwrap(); - sink.sleep_until_end(); + sink.append(source); + sink.sleep_until_end(); } diff --git a/src/lib.rs b/src/lib.rs index e4296cfa..b248f009 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -94,6 +94,8 @@ extern crate lazy_static; extern crate lewton; #[cfg(feature = "mp3")] extern crate minimp3; +#[cfg(feature = "http")] +extern crate reqwest; pub use cpal::{ traits::DeviceTrait, Device, Devices, DevicesError, Format, InputDevices, OutputDevices @@ -105,6 +107,11 @@ pub use engine::play_raw; pub use sink::Sink; pub use source::Source; pub use spatial_sink::SpatialSink; +#[cfg(feature = "http")] +pub use utils::{ + buffer::seekable_bufreader::SeekableBufReader, + source::http::SeekableRequest, +}; use cpal::traits::HostTrait; use std::io::{Read, Seek}; @@ -117,6 +124,7 @@ mod spatial_sink; pub mod buffer; pub mod decoder; pub mod dynamic_mixer; +pub mod utils; pub mod queue; pub mod source; pub mod static_buffer; diff --git a/src/utils/buffer/mod.rs b/src/utils/buffer/mod.rs new file mode 100644 index 00000000..df1657c4 --- /dev/null +++ b/src/utils/buffer/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "http")] +pub mod seekable_bufreader; diff --git a/src/utils/buffer/seekable_bufreader.rs b/src/utils/buffer/seekable_bufreader.rs new file mode 100644 index 00000000..7ce9fe8b --- /dev/null +++ b/src/utils/buffer/seekable_bufreader.rs @@ -0,0 +1,50 @@ +use std::io::{BufRead, Error, Read, Seek, SeekFrom}; + +use crate::utils::Cache; + +pub struct SeekableBufReader { + buffer: B, +} + +impl SeekableBufReader { + pub fn new(buffer: B) -> Self { + SeekableBufReader { + buffer, + } + } +} + +impl Read for SeekableBufReader { + fn read(&mut self, buf: &mut [u8]) -> Result { + let position = self.buffer.position(); + let cache = self.buffer.slice( + position, + position + buf.len(), + ); + let amt = cache.len(); + + buf[..amt].copy_from_slice(cache); + self.consume(amt); + Ok(amt) + } +} + +impl Seek for SeekableBufReader { + fn seek(&mut self, pos: SeekFrom) -> Result { + self.buffer.seek(pos) + } +} + +impl BufRead for SeekableBufReader { + fn fill_buf(&mut self) -> Result<&[u8], Error> { + Ok(self.buffer.slice( + self.buffer.position(), + self.buffer.available(), + )) + } + + #[allow(unused_must_use)] + fn consume(&mut self, amt: usize) { + self.buffer.seek(SeekFrom::Current(amt as i64)); + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 00000000..9a0747a7 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,19 @@ +use std::io::{Read, Seek}; + +pub mod buffer; +pub mod source; + +#[cfg(feature = "http")] +pub trait Cache: Read + Seek { + fn available(&self) -> usize; + + fn position(&self) -> usize; + + fn get(&mut self, index: usize) -> Option<&u8>; + + fn slice(&mut self, from: usize, to: usize) -> &[u8]; + + fn cache_to_index(&mut self, index: usize); + + fn cache_to_end(&mut self); +} diff --git a/src/utils/source/http.rs b/src/utils/source/http.rs new file mode 100644 index 00000000..ece5dcaf --- /dev/null +++ b/src/utils/source/http.rs @@ -0,0 +1,106 @@ +use std::io::{Error, ErrorKind, Read, Seek, SeekFrom}; + +use reqwest::blocking::{get, Response}; + +use crate::utils::Cache; + +pub struct SeekableRequest {} + +impl SeekableRequest { + pub fn get(url: &str) -> SeekableResponse { + SeekableResponse::from(get(url).unwrap()) + } +} + +pub struct SeekableResponse { + inner: Response, + position: usize, + buffer: Vec, +} + +impl From for SeekableResponse { + fn from(inner: Response) -> Self { + SeekableResponse { + inner, + position: 0, + buffer: Vec::default(), + } + } +} + +impl Cache for SeekableResponse { + fn available(&self) -> usize { + self.buffer.len() + } + + fn position(&self) -> usize { + self.position + } + + fn get(&mut self, index: usize) -> Option<&u8> { + if self.buffer.len() <= index { + self.cache_to_index(index); + } + self.buffer.get(index) + } + + fn slice(&mut self, from: usize, to: usize) -> &[u8] { + if self.buffer.len() <= to { + self.cache_to_index(to); + } + if self.buffer.len() <= from { + return &[]; + } + if self.buffer.len() <= to { + return &self.buffer[from..]; + } + &self.buffer[from..to] + } + + #[allow(unused_must_use)] + fn cache_to_index(&mut self, index: usize) { + let available = self.buffer.len(); + if index >= available { + self.read(&mut vec![0u8; index - available]); + } + } + + #[allow(unused_must_use)] + fn cache_to_end(&mut self) { + self.read_to_end(&mut Vec::default()); + } +} + +impl Read for SeekableResponse { + fn read(&mut self, buf: &mut [u8]) -> Result { + Ok(self.inner.read(buf).map(|len| { + self.buffer.extend(&buf[..len]); + len + })?) + } +} + +impl Seek for SeekableResponse { + fn seek(&mut self, pos: SeekFrom) -> Result { + let (position, offset) = match pos { + SeekFrom::Start(position) => (0, position as i64), + SeekFrom::Current(position) => (self.position, position), + SeekFrom::End(position) => (self.buffer.len(), position), + }; + let position = if offset < 0 { + position.checked_sub(offset.wrapping_neg() as usize) + } else { + position.checked_add(offset as usize) + }; + match position { + Some(position) => { + self.position = position; + Ok(position as u64) + } + None => Err(Error::new( + ErrorKind::InvalidInput, + "invalid seek to a negative or overflowing position", + )), + } + } +} diff --git a/src/utils/source/mod.rs b/src/utils/source/mod.rs new file mode 100644 index 00000000..d56a2d39 --- /dev/null +++ b/src/utils/source/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "http")] +pub mod http;