Looping fetch of audio file chunks

This commit is contained in:
Muaz Ahmad 2024-11-29 14:43:37 +05:00
parent 6b0a53e96f
commit 3122e2559e
6 changed files with 60 additions and 43 deletions

View file

@ -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>,

View file

@ -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);
} }

View file

@ -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 {

View file

@ -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 {
start = self.tui_root.metadata.bytes_received;
let diff = self.tui_root.metadata.size - self.tui_root.metadata.bytes_received; let diff = self.tui_root.metadata.size - self.tui_root.metadata.bytes_received;
end = std::cmp::min(diff, MAX_CHUNK_SIZE); let end = std::cmp::min(diff, MAX_CHUNK_SIZE) + start - 1;
self.api_chan.send(APIEvent::StreamSong(id, start, end))?; self.api_chan.send(APIEvent::StreamSong(
self.tui_root.metadata.id.clone(),
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(())
} }

View file

@ -42,46 +42,42 @@ 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" {
self.validate(response, "/stream")?; // will never error if not image
} else {
eprintln!("Fetched chunk");
let mut audio_chunk = Vec::new(); let mut audio_chunk = Vec::new();
response.read_to_end(&mut audio_chunk)?; response.read_to_end(&mut audio_chunk)?;
return Ok(PlayerEvent::AddAudioChunks(audio_chunk)); return Ok(PlayerEvent::AddAudioChunk(audio_chunk));
} }
unreachable!();
fn validate_media(
&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)));
}
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(
response.status(),
"/getCoverArt",
)));
};
if response.headers()[CONTENT_TYPE].to_str()? == "application/json" {
self.validate(response, "/getCoverArt")?; // will never error if not image
} else {
let mut image = Vec::new(); let mut image = Vec::new();
response.read_to_end(&mut image)?; response.read_to_end(&mut image)?;
let cover = image::load_from_memory_with_format( let cover = image::load_from_memory_with_format(
&mut image, &mut image,
image::ImageFormat::from_mime_type(response.headers()[CONTENT_TYPE].to_str()?) image::ImageFormat::from_mime_type(response.headers()[CONTENT_TYPE].to_str()?).unwrap(),
.unwrap(),
)?; )?;
return Ok(PlayerEvent::UpdateCover(cover)); 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> {
let response = self.request("/getRandomSongs", Some(&[("size", n.to_string())]), None)?; let response = self.request("/getRandomSongs", Some(&[("size", n.to_string())]), None)?;

View file

@ -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)
}
} }
} }
} }