Looping fetch of audio file chunks
This commit is contained in:
parent
6b0a53e96f
commit
3122e2559e
6 changed files with 60 additions and 43 deletions
|
@ -12,7 +12,9 @@ use pipewire::PipewireContext;
|
||||||
|
|
||||||
use crate::{config::Settings, player::PlayerEvent, utils::Error};
|
use crate::{config::Settings, player::PlayerEvent, utils::Error};
|
||||||
|
|
||||||
pub enum AudioEvent {}
|
pub enum AudioEvent {
|
||||||
|
DecodeChunk(Vec<u8>),
|
||||||
|
}
|
||||||
|
|
||||||
pub struct SoundManager {
|
pub struct SoundManager {
|
||||||
settings: Arc<Settings>,
|
settings: Arc<Settings>,
|
||||||
|
|
|
@ -29,6 +29,10 @@ impl Metadata {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn bytes_pending(&self) -> bool {
|
||||||
|
self.bytes_received < self.size
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_cover(&mut self, cover: DynamicImage) {
|
pub fn set_cover(&mut self, cover: DynamicImage) {
|
||||||
self.cover = cover.thumbnail(200, 200);
|
self.cover = cover.thumbnail(200, 200);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ pub enum PlayerEvent {
|
||||||
UpdateCover(DynamicImage),
|
UpdateCover(DynamicImage),
|
||||||
UserQuit,
|
UserQuit,
|
||||||
PlayNext,
|
PlayNext,
|
||||||
AddAudioChunks(Vec<u8>),
|
AddAudioChunk(Vec<u8>),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Player {
|
pub struct Player {
|
||||||
|
|
|
@ -45,6 +45,7 @@ impl Player {
|
||||||
PlayerEvent::AddSongList(list) => self.playlist.append(list),
|
PlayerEvent::AddSongList(list) => self.playlist.append(list),
|
||||||
PlayerEvent::PlayNext => self.play_next()?,
|
PlayerEvent::PlayNext => self.play_next()?,
|
||||||
PlayerEvent::UpdateCover(cover) => self.tui_root.update_cover(cover),
|
PlayerEvent::UpdateCover(cover) => self.tui_root.update_cover(cover),
|
||||||
|
PlayerEvent::AddAudioChunk(chunk) => self.recv_chunk(chunk)?,
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,18 +82,28 @@ impl Player {
|
||||||
.send(PlayerEvent::UpdateCover(default_cover()))?;
|
.send(PlayerEvent::UpdateCover(default_cover()))?;
|
||||||
}
|
}
|
||||||
self.tui_root.metadata.update_metadata(song);
|
self.tui_root.metadata.update_metadata(song);
|
||||||
self.fetch_audio_chunk(self.tui_root.metadata.id.clone())?;
|
self.fetch_audio_chunk()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch_audio_chunk(&mut self, id: String) -> Result<(), Error> {
|
fn fetch_audio_chunk(&mut self) -> Result<(), Error> {
|
||||||
let (mut start, mut end) = (0, 0);
|
let start = self.tui_root.metadata.bytes_received;
|
||||||
if self.tui_root.metadata.bytes_received < self.tui_root.metadata.size {
|
let diff = self.tui_root.metadata.size - self.tui_root.metadata.bytes_received;
|
||||||
start = self.tui_root.metadata.bytes_received;
|
let end = std::cmp::min(diff, MAX_CHUNK_SIZE) + start - 1;
|
||||||
let diff = self.tui_root.metadata.size - self.tui_root.metadata.bytes_received;
|
self.api_chan.send(APIEvent::StreamSong(
|
||||||
end = std::cmp::min(diff, MAX_CHUNK_SIZE);
|
self.tui_root.metadata.id.clone(),
|
||||||
self.api_chan.send(APIEvent::StreamSong(id, start, end))?;
|
start,
|
||||||
|
end,
|
||||||
|
))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recv_chunk(&mut self, chunk: Vec<u8>) -> Result<(), Error> {
|
||||||
|
self.tui_root.metadata.bytes_received += chunk.len() as u32;
|
||||||
|
self.audio_chan.send(AudioEvent::DecodeChunk(chunk))?;
|
||||||
|
if self.tui_root.metadata.bytes_pending() {
|
||||||
|
self.fetch_audio_chunk()?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,45 +42,41 @@ impl APIClient {
|
||||||
header.insert(RANGE, range_header.parse()?);
|
header.insert(RANGE, range_header.parse()?);
|
||||||
let mut response = self.request("/stream", Some(&[("id", id)]), Some(header))?;
|
let mut response = self.request("/stream", Some(&[("id", id)]), Some(header))?;
|
||||||
|
|
||||||
if !response.status().is_success() {
|
response = self.validate_media(response, "/stream", "audio")?;
|
||||||
return Err(Box::new(APIError::StatusError(
|
|
||||||
response.status(),
|
|
||||||
"/stream",
|
|
||||||
)));
|
|
||||||
};
|
|
||||||
|
|
||||||
if response.headers()[CONTENT_TYPE].to_str()? == "application/json" {
|
let mut audio_chunk = Vec::new();
|
||||||
self.validate(response, "/stream")?; // will never error if not image
|
response.read_to_end(&mut audio_chunk)?;
|
||||||
} else {
|
return Ok(PlayerEvent::AddAudioChunk(audio_chunk));
|
||||||
eprintln!("Fetched chunk");
|
}
|
||||||
let mut audio_chunk = Vec::new();
|
|
||||||
response.read_to_end(&mut audio_chunk)?;
|
fn validate_media(
|
||||||
return Ok(PlayerEvent::AddAudioChunks(audio_chunk));
|
&mut self,
|
||||||
|
response: Response,
|
||||||
|
path: &'static str,
|
||||||
|
expected_mime_class: &'static str,
|
||||||
|
) -> Result<Response, Error> {
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(Box::new(APIError::StatusError(response.status(), path)));
|
||||||
}
|
}
|
||||||
unreachable!();
|
let mime = response.headers()[CONTENT_TYPE].to_str()?;
|
||||||
|
match mime {
|
||||||
|
"application/json" => drop(self.validate(response, path)?), // must error on binary endpoints
|
||||||
|
_ if mime.starts_with(expected_mime_class) => return Ok(response),
|
||||||
|
_ => return Err(Box::new(APIError::InvalidResponseType(String::from(mime)))),
|
||||||
|
}
|
||||||
|
unreachable!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_cover_art(&mut self, cover_id: String) -> Result<PlayerEvent, Error> {
|
fn get_cover_art(&mut self, cover_id: String) -> Result<PlayerEvent, Error> {
|
||||||
let mut response = self.request("/getCoverArt", Some(&[("id", cover_id)]), None)?;
|
let mut response = self.request("/getCoverArt", Some(&[("id", cover_id)]), None)?;
|
||||||
if !response.status().is_success() {
|
response = self.validate_media(response, "/getCoverArt", "image")?;
|
||||||
return Err(Box::new(APIError::StatusError(
|
let mut image = Vec::new();
|
||||||
response.status(),
|
response.read_to_end(&mut image)?;
|
||||||
"/getCoverArt",
|
let cover = image::load_from_memory_with_format(
|
||||||
)));
|
&mut image,
|
||||||
};
|
image::ImageFormat::from_mime_type(response.headers()[CONTENT_TYPE].to_str()?).unwrap(),
|
||||||
if response.headers()[CONTENT_TYPE].to_str()? == "application/json" {
|
)?;
|
||||||
self.validate(response, "/getCoverArt")?; // will never error if not image
|
return Ok(PlayerEvent::UpdateCover(cover));
|
||||||
} else {
|
|
||||||
let mut image = Vec::new();
|
|
||||||
response.read_to_end(&mut image)?;
|
|
||||||
let cover = image::load_from_memory_with_format(
|
|
||||||
&mut image,
|
|
||||||
image::ImageFormat::from_mime_type(response.headers()[CONTENT_TYPE].to_str()?)
|
|
||||||
.unwrap(),
|
|
||||||
)?;
|
|
||||||
return Ok(PlayerEvent::UpdateCover(cover));
|
|
||||||
}
|
|
||||||
unreachable!();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_random(&mut self, n: i32) -> Result<PlayerEvent, Error> {
|
fn get_random(&mut self, n: i32) -> Result<PlayerEvent, Error> {
|
||||||
|
|
|
@ -3,6 +3,7 @@ use reqwest::StatusCode;
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum APIError {
|
pub enum APIError {
|
||||||
StatusError(StatusCode, &'static str),
|
StatusError(StatusCode, &'static str),
|
||||||
|
InvalidResponseType(String),
|
||||||
SubsonicError(String),
|
SubsonicError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +18,9 @@ impl std::fmt::Display for APIError {
|
||||||
code, path
|
code, path
|
||||||
),
|
),
|
||||||
Self::SubsonicError(message) => write!(f, "Subsonic API error: {}", message),
|
Self::SubsonicError(message) => write!(f, "Subsonic API error: {}", message),
|
||||||
|
Self::InvalidResponseType(mime) => {
|
||||||
|
write!(f, "Expected media mime-type, found: {}", mime)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue