Implement seek for audio-player

This commit is contained in:
chmanie 2023-06-08 19:00:37 +02:00
parent 51cdf13953
commit 905d412644
4 changed files with 45 additions and 15 deletions

View File

@ -8,7 +8,7 @@ rodio = { version = "0.17.1", default-features = false, features = [
"symphonia-all", "symphonia-all",
] } ] }
symphonia = { version = "0.5.3", features = ["all"] } symphonia = { version = "0.5.3", features = ["all"] }
stream-download = { git = "https://github.com/aschey/stream-download-rs.git" } stream-download = { path = "../stream-download" }
anyhow = "1.0.71" anyhow = "1.0.71"
url = "2.4.0" url = "2.4.0"
flume = "0.10.14" flume = "0.10.14"

View File

@ -35,7 +35,11 @@ async fn main() {
.await .await
.unwrap(); .unwrap();
tokio::time::sleep(Duration::from_secs(5)).await; tokio::time::sleep(Duration::from_secs(10)).await;
player.seek_to(Duration::from_secs(20)).await.unwrap();
tokio::time::sleep(Duration::from_secs(10)).await;
player player
.play("https://www2.cs.uic.edu/~i101/SoundFiles/PinkPanther60.wav") .play("https://www2.cs.uic.edu/~i101/SoundFiles/PinkPanther60.wav")

View File

@ -69,6 +69,10 @@ impl Default for Player {
tx.send(player.elapsed()) tx.send(player.elapsed())
.unwrap_or_else(|e| warn!("Send error {}", e)); .unwrap_or_else(|e| warn!("Send error {}", e));
} }
Ok(PlayerEngineCommand::SeekTo(time, tx)) => {
tx.send(player.seek_to(time))
.unwrap_or_else(|e| warn!("Send error {}", e));
}
Ok(PlayerEngineCommand::GetVolume(tx)) => { Ok(PlayerEngineCommand::GetVolume(tx)) => {
tx.send(player.volume()) tx.send(player.volume())
.unwrap_or_else(|e| warn!("Send error {}", e)); .unwrap_or_else(|e| warn!("Send error {}", e));
@ -127,6 +131,12 @@ impl Player {
rx.recv_async().await? rx.recv_async().await?
} }
pub async fn seek_to(&self, time: Duration) -> Result<Duration> {
let (tx, rx) = flume::bounded(1);
self.tx_engine.send(PlayerEngineCommand::SeekTo(time, tx))?;
rx.recv_async().await?
}
pub async fn volume(&self) -> Result<f32> { pub async fn volume(&self) -> Result<f32> {
let (tx, rx) = flume::bounded(1); let (tx, rx) = flume::bounded(1);
self.tx_engine.send(PlayerEngineCommand::GetVolume(tx))?; self.tx_engine.send(PlayerEngineCommand::GetVolume(tx))?;

View File

@ -1,7 +1,8 @@
use flume::Sender; use flume::Sender;
use std::fs::File;
use std::path::Path; use std::path::Path;
use std::sync::atomic::AtomicU64;
use std::time::Duration; use std::time::Duration;
use std::{fs::File, sync::atomic::Ordering};
use symphonia::core::probe::Hint; use symphonia::core::probe::Hint;
use tracing::warn; use tracing::warn;
use url::Url; use url::Url;
@ -10,9 +11,7 @@ use crate::decoder::{MediaInfo, SymphoniaDecoder};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use rodio::{OutputStream, Sink, Source}; use rodio::{OutputStream, Sink, Source};
use stream_download::StreamDownload; use stream_download::StreamDownload;
use symphonia::core::io::{ use symphonia::core::io::{MediaSource, MediaSourceStream, MediaSourceStreamOptions};
MediaSource, MediaSourceStream, MediaSourceStreamOptions, ReadOnlySource,
};
use thiserror::Error; use thiserror::Error;
pub enum PlayerEngineCommand { pub enum PlayerEngineCommand {
@ -25,6 +24,7 @@ pub enum PlayerEngineCommand {
Stop(Sender<Result<()>>), Stop(Sender<Result<()>>),
GetDuration(Sender<Result<Duration>>), GetDuration(Sender<Result<Duration>>),
GetElapsed(Sender<Result<Duration>>), GetElapsed(Sender<Result<Duration>>),
SeekTo(Duration, Sender<Result<Duration>>),
GetVolume(Sender<f32>), GetVolume(Sender<f32>),
GetPaused(Sender<Result<bool>>), GetPaused(Sender<Result<bool>>),
Eos, Eos,
@ -48,10 +48,17 @@ pub enum PlayerMessage {
// TODO: // TODO:
// * Emit buffering // * Emit buffering
#[derive(Debug, Error)]
pub enum PlayerEngineError {
#[error("Sink is not playing")]
NotPlaying,
}
// Used for seeking in the stream
static SEEK_TO: AtomicU64 = AtomicU64::new(0);
pub struct PlayerEngine { pub struct PlayerEngine {
elapsed: Duration, elapsed: Duration,
// FIXME: We only need this to re-start a track
// Might do that using seeking in the future
current_source: Option<String>, current_source: Option<String>,
media_info: Option<MediaInfo>, media_info: Option<MediaInfo>,
sink: Sink, sink: Sink,
@ -61,12 +68,6 @@ pub struct PlayerEngine {
tx_player: Sender<PlayerMessage>, tx_player: Sender<PlayerMessage>,
} }
#[derive(Debug, Error)]
pub enum PlayerEngineError {
#[error("Sink is not playing")]
NotPlaying,
}
impl PlayerEngine { impl PlayerEngine {
pub fn init( pub fn init(
tx_engine: Sender<PlayerEngineCommand>, tx_engine: Sender<PlayerEngineCommand>,
@ -110,6 +111,11 @@ impl PlayerEngine {
// FIXME: regularly update metadata revision // FIXME: regularly update metadata revision
let decoder = decoder.periodic_access(Duration::from_millis(250), move |src| { let decoder = decoder.periodic_access(Duration::from_millis(250), move |src| {
let seek = SEEK_TO.load(Ordering::SeqCst);
if seek > 0 {
src.seek(Duration::from_secs(seek));
SEEK_TO.store(0, Ordering::SeqCst);
}
let elapsed = src.elapsed(); let elapsed = src.elapsed();
tx_engine tx_engine
.send(PlayerEngineCommand::SetElapsed(elapsed)) .send(PlayerEngineCommand::SetElapsed(elapsed))
@ -208,6 +214,16 @@ impl PlayerEngine {
Ok(self.elapsed) Ok(self.elapsed)
} }
pub fn seek_to(&self, time: Duration) -> Result<Duration> {
// We can seek between 1 second and the total duration of the track
let duration = self.duration().unwrap_or(self.elapsed);
let time = time.clamp(Duration::from_secs(1), duration);
SEEK_TO.store(time.as_secs(), Ordering::SeqCst);
// FIXME: ideally we would like to return once the seeking is successful
// then return the current elapsed time
Ok(time)
}
pub fn volume(&self) -> f32 { pub fn volume(&self) -> f32 {
self.sink.volume() self.sink.volume()
} }
@ -243,7 +259,7 @@ impl PlayerEngine {
let path = Path::new(url.path()); let path = Path::new(url.path());
let hint = self.get_hint(path); let hint = self.get_hint(path);
Ok((Box::new(ReadOnlySource::new(reader)), hint)) Ok((Box::new(reader), hint))
} else { } else {
Err(anyhow!("Not a valid URL scheme: {}", url.scheme())) Err(anyhow!("Not a valid URL scheme: {}", url.scheme()))
} }