From 84e45b0f1d9eb420544fae4399b56653a1eeebf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Gaillard?= Date: Tue, 31 Mar 2020 16:18:06 +0800 Subject: [PATCH] feat: playing sound from remote source --- Cargo.toml | 8 ++ examples/http_flac.rs | 12 +++ examples/music_flac.rs | 17 ++-- examples/music_mp3.rs | 17 ++-- examples/music_ogg.rs | 17 ++-- examples/music_wav.rs | 17 ++-- src/lib.rs | 8 ++ src/utils/buffer/mod.rs | 2 + src/utils/buffer/seekable_bufreader.rs | 106 +++++++++++++++++++++++++ src/utils/mod.rs | 2 + src/utils/source/http.rs | 56 +++++++++++++ src/utils/source/mod.rs | 2 + 12 files changed, 232 insertions(+), 32 deletions(-) create mode 100644 examples/http_flac.rs create mode 100644 src/utils/buffer/mod.rs create mode 100644 src/utils/buffer/seekable_bufreader.rs create mode 100644 src/utils/mod.rs create mode 100644 src/utils/source/http.rs create mode 100644 src/utils/source/mod.rs diff --git a/Cargo.toml b/Cargo.toml index aca8607c..6d92211e 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.9.0", default-features = false, features = ["rustls-tls"], optional = true } [features] default = ["flac", "vorbis", "wav", "mp3"] @@ -23,3 +25,9 @@ flac = ["claxon"] vorbis = ["lewton"] wav = ["hound"] mp3 = ["minimp3"] +http = ["reqwest"] + +[[example]] +name = "http_flac" +path = "examples/http_flac.rs" +required-features = ["http"] diff --git a/examples/http_flac.rs b/examples/http_flac.rs new file mode 100644 index 00000000..2518f593 --- /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 data = rodio::SeekableReqwest::get(url); + let buffer = rodio::SeekableBufReader::new(data); + 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..84496cc1 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::SeekableReqwest, +}; 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..8d9e460b --- /dev/null +++ b/src/utils/buffer/seekable_bufreader.rs @@ -0,0 +1,106 @@ +use std::io::{BufRead, Error, ErrorKind, Read, Seek, SeekFrom}; + +pub struct SeekableBufReader { + inner: R, + position: usize, + cache: Vec, +} + +impl SeekableBufReader { + pub fn new(inner: R) -> SeekableBufReader { + SeekableBufReader { + inner, + position: 0, + cache: Vec::default(), + } + } + + pub fn capacity(&self) -> usize { + self.cache.capacity() + } + + pub fn available(&self) -> usize { + self.cache.len() + } + + pub fn position(&self) -> usize { + self.position + } + + pub fn at(&mut self, index: usize) -> Option<&u8> { + if self.available() <= index { + self.cache_to(index + 1); + } + self.cache.get(index) + } + + pub fn get(&mut self, from: usize, to: usize) -> &[u8] { + let to = from + to; + if self.available() <= to { + self.cache_to(to + 1); + } + &self.cache[from..to] + } + + fn cache_to(&mut self, _position: usize) { + // TODO: lazy_download + self.cache_to_end(); + } + + #[allow(unused_must_use)] + fn cache_to_end(&mut self) { + let mut buffer = Vec::default(); + self.inner.read_to_end(&mut buffer); + self.cache.extend(buffer); + } +} + +impl Seek for SeekableBufReader { + fn seek(&mut self, seek: SeekFrom) -> Result { + let (position, offset) = match seek { + SeekFrom::Start(position) => (0, position as i64), + SeekFrom::Current(position) => (self.position, position), + SeekFrom::End(position) => { + self.cache_to_end(); + (self.available(), 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", + )), + } + } +} + +impl Read for SeekableBufReader { + fn read(&mut self, buffer: &mut [u8]) -> Result { + let size = match self.at(self.position + buffer.len()) { + Some(_) => buffer.len(), + None => self.available() - self.position, + }; + buffer[..size].clone_from_slice(self.get(self.position, size)); + self.consume(size); + Ok(size) + } +} + +impl BufRead for SeekableBufReader { + fn fill_buf(&mut self) -> Result<&[u8], Error> { + Ok(self.get(self.position, self.available())) + } + + fn consume(&mut self, amt: usize) { + self.position += amt; + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 00000000..e05c077a --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod buffer; +pub mod source; diff --git a/src/utils/source/http.rs b/src/utils/source/http.rs new file mode 100644 index 00000000..7f7a1762 --- /dev/null +++ b/src/utils/source/http.rs @@ -0,0 +1,56 @@ +use std::io::{Error, ErrorKind, Read, Seek, SeekFrom}; + +use reqwest::{get, Response}; + +pub struct SeekableReqwest {} + +impl SeekableReqwest { + pub fn get(url: &str) -> SeekableResponse { + SeekableResponse::from(get(url).unwrap()) + } +} + +pub struct SeekableResponse { + inner: Response, + position: usize, +} + +impl From for SeekableResponse { + fn from(inner: Response) -> SeekableResponse { + SeekableResponse { + inner, + position: 0, + } + } +} + +impl Seek for SeekableResponse { + fn seek(&mut self, seek: SeekFrom) -> Result { + let (position, offset) = match seek { + SeekFrom::Start(position) => (0, position as i64), + SeekFrom::Current(position) => (self.position, position), + SeekFrom::End(position) => (0, position), // TODO: real end index + }; + 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", + )), + } + } +} + +impl Read for SeekableResponse { + fn read(&mut self, buffer: &mut [u8]) -> Result { + self.inner.read(buffer) + } +} 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;