Driver/Input: Migrate audio backend to Symphonia (#89)
This extensive PR rewrites the internal mixing logic of the driver to use symphonia for parsing and decoding audio data, and rubato to resample audio. Existing logic to decode DCA and Opus formats/data have been reworked as plugins for symphonia. The main benefit is that we no longer need to keep yt-dlp and ffmpeg processes alive, saving a lot of memory and CPU: all decoding can be done in Rust! In exchange, we now need to do a lot of the HTTP handling and resumption ourselves, but this is still a huge net positive. `Input`s have been completely reworked such that all default (non-cached) sources are lazy by default, and are no longer covered by a special-case `Restartable`. These now span a gamut from a `Compose` (lazy), to a live source, to a fully `Parsed` source. As mixing is still sync, this includes adapters for `AsyncRead`/`AsyncSeek`, and HTTP streams. `Track`s have been reworked so that they only contain initialisation state for each track. `TrackHandles` are only created once a `Track`/`Input` has been handed over to the driver, replacing `create_player` and related functions. `TrackHandle::action` now acts on a `View` of (im)mutable state, and can request seeks/readying via `Action`. Per-track event handling has also been improved -- we can now determine and propagate the reason behind individual track errors due to the new backend. Some `TrackHandle` commands (seek etc.) benefit from this, and now use internal callbacks to signal completion. Due to associated PRs on felixmcfelix/songbird from avid testers, this includes general clippy tweaks, API additions, and other repo-wide cleanup. Thanks go out to the below co-authors. Co-authored-by: Gnome! <45660393+GnomedDev@users.noreply.github.com> Co-authored-by: Alakh <36898190+alakhpc@users.noreply.github.com>
This commit is contained in:
43
src/tracks/action.rs
Normal file
43
src/tracks/action.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use flume::Sender;
|
||||
use std::time::Duration;
|
||||
|
||||
use super::{PlayError, SeekRequest};
|
||||
|
||||
/// Actions for the mixer to take after inspecting track state via
|
||||
/// [`TrackHandle::action`].
|
||||
///
|
||||
/// [`TrackHandle::action`]: super::TrackHandle::action
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Action {
|
||||
pub(crate) make_playable: Option<Sender<Result<(), PlayError>>>,
|
||||
pub(crate) seek_point: Option<SeekRequest>,
|
||||
}
|
||||
|
||||
impl Action {
|
||||
/// Requests a seek to the given time for this track.
|
||||
#[must_use]
|
||||
pub fn seek(mut self, time: Duration) -> Self {
|
||||
let (callback, _) = flume::bounded(1);
|
||||
self.seek_point = Some(SeekRequest { time, callback });
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Readies the track to be playable, if this is not already the case.
|
||||
#[must_use]
|
||||
pub fn make_playable(mut self) -> Self {
|
||||
let (tx, _) = flume::bounded(1);
|
||||
self.make_playable = Some(tx);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn combine(&mut self, other: Self) {
|
||||
if other.make_playable.is_some() {
|
||||
self.make_playable = other.make_playable;
|
||||
}
|
||||
if other.seek_point.is_some() {
|
||||
self.seek_point = other.seek_point;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
use super::*;
|
||||
use crate::events::EventData;
|
||||
use flume::Sender;
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
fmt::{Debug, Formatter, Result as FmtResult},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
/// A request from external code using a [`TrackHandle`] to modify
|
||||
/// or act upon an [`Track`] object.
|
||||
@@ -21,37 +24,42 @@ pub enum TrackCommand {
|
||||
/// Seek to the given duration.
|
||||
///
|
||||
/// On unsupported input types, this can be fatal.
|
||||
Seek(Duration),
|
||||
Seek(SeekRequest),
|
||||
/// Register an event on this track.
|
||||
AddEvent(EventData),
|
||||
/// Run some closure on this track, with direct access to the core object.
|
||||
Do(Box<dyn FnOnce(&mut Track) + Send + Sync + 'static>),
|
||||
Do(Box<dyn FnOnce(View) -> Option<Action> + Send + Sync + 'static>),
|
||||
/// Request a copy of this track's state.
|
||||
Request(Sender<TrackState>),
|
||||
/// Change the loop count/strategy of this track.
|
||||
Loop(LoopState),
|
||||
/// Prompts a track's input to become live and usable, if it is not already.
|
||||
MakePlayable,
|
||||
MakePlayable(Sender<Result<(), PlayError>>),
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for TrackCommand {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
use TrackCommand::*;
|
||||
impl Debug for TrackCommand {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
write!(
|
||||
f,
|
||||
"TrackCommand::{}",
|
||||
match self {
|
||||
Play => "Play".to_string(),
|
||||
Pause => "Pause".to_string(),
|
||||
Stop => "Stop".to_string(),
|
||||
Volume(vol) => format!("Volume({})", vol),
|
||||
Seek(d) => format!("Seek({:?})", d),
|
||||
AddEvent(evt) => format!("AddEvent({:?})", evt),
|
||||
Do(_f) => "Do([function])".to_string(),
|
||||
Request(tx) => format!("Request({:?})", tx),
|
||||
Loop(loops) => format!("Loop({:?})", loops),
|
||||
MakePlayable => "MakePlayable".to_string(),
|
||||
Self::Play => "Play".to_string(),
|
||||
Self::Pause => "Pause".to_string(),
|
||||
Self::Stop => "Stop".to_string(),
|
||||
Self::Volume(vol) => format!("Volume({})", vol),
|
||||
Self::Seek(s) => format!("Seek({:?})", s.time),
|
||||
Self::AddEvent(evt) => format!("AddEvent({:?})", evt),
|
||||
Self::Do(_f) => "Do([function])".to_string(),
|
||||
Self::Request(tx) => format!("Request({:?})", tx),
|
||||
Self::Loop(loops) => format!("Loop({:?})", loops),
|
||||
Self::MakePlayable(_) => "MakePlayable".to_string(),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SeekRequest {
|
||||
pub time: Duration,
|
||||
pub callback: Sender<Result<Duration, PlayError>>,
|
||||
}
|
||||
|
||||
@@ -1,40 +1,110 @@
|
||||
use std::{error::Error, fmt};
|
||||
use crate::input::AudioStreamError;
|
||||
use flume::RecvError;
|
||||
use std::{
|
||||
error::Error,
|
||||
fmt::{Display, Formatter, Result as FmtResult},
|
||||
sync::Arc,
|
||||
};
|
||||
use symphonia_core::errors::Error as SymphoniaError;
|
||||
|
||||
/// Errors associated with control and manipulation of tracks.
|
||||
///
|
||||
/// Unless otherwise stated, these don't invalidate an existing track,
|
||||
/// but do advise on valid operations and commands.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum TrackError {
|
||||
pub enum ControlError {
|
||||
/// The operation failed because the track has ended, has been removed
|
||||
/// due to call closure, or some error within the driver.
|
||||
Finished,
|
||||
/// The supplied event listener can never be fired by a track, and should
|
||||
/// be attached to the driver instead.
|
||||
InvalidTrackEvent,
|
||||
/// The track's underlying [`Input`] doesn't support seeking operations.
|
||||
/// A command to seek or ready the target track failed when parsing or creating the stream.
|
||||
///
|
||||
/// [`Input`]: crate::input::Input
|
||||
SeekUnsupported,
|
||||
/// This is a fatal error, and the track will be removed.
|
||||
Play(PlayError),
|
||||
/// Another `seek`/`make_playable` request was made, and so this callback handler was dropped.
|
||||
Dropped,
|
||||
}
|
||||
|
||||
impl fmt::Display for TrackError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
impl Display for ControlError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
write!(f, "failed to operate on track (handle): ")?;
|
||||
match self {
|
||||
TrackError::Finished => write!(f, "track ended"),
|
||||
TrackError::InvalidTrackEvent => {
|
||||
ControlError::Finished => write!(f, "track ended"),
|
||||
ControlError::InvalidTrackEvent => {
|
||||
write!(f, "given event listener can't be fired on a track")
|
||||
},
|
||||
TrackError::SeekUnsupported => write!(f, "track did not support seeking"),
|
||||
ControlError::Play(p) => {
|
||||
write!(f, "i/o request on track failed: {}", p)
|
||||
},
|
||||
ControlError::Dropped => write!(f, "request was replaced by another of same type"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for TrackError {}
|
||||
impl Error for ControlError {}
|
||||
|
||||
impl From<RecvError> for ControlError {
|
||||
fn from(_: RecvError) -> Self {
|
||||
ControlError::Dropped
|
||||
}
|
||||
}
|
||||
|
||||
/// Alias for most calls to a [`TrackHandle`].
|
||||
///
|
||||
/// [`TrackHandle`]: super::TrackHandle
|
||||
pub type TrackResult<T> = Result<T, TrackError>;
|
||||
pub type TrackResult<T> = Result<T, ControlError>;
|
||||
|
||||
/// Errors reported by the mixer while attempting to play (or ready) a [`Track`].
|
||||
///
|
||||
/// [`Track`]: super::Track
|
||||
#[derive(Clone, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum PlayError {
|
||||
/// Failed to create a live bytestream from the lazy [`Compose`].
|
||||
///
|
||||
/// [`Compose`]: crate::input::Compose
|
||||
Create(Arc<AudioStreamError>),
|
||||
/// Failed to read headers, codecs, or a valid stream from an [`Input`].
|
||||
///
|
||||
/// [`Input`]: crate::input::Input
|
||||
Parse(Arc<SymphoniaError>),
|
||||
/// Failed to decode a frame received from an [`Input`].
|
||||
///
|
||||
/// [`Input`]: crate::input::Input
|
||||
Decode(Arc<SymphoniaError>),
|
||||
/// Failed to seek to the requested location.
|
||||
Seek(Arc<SymphoniaError>),
|
||||
}
|
||||
|
||||
impl Display for PlayError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
f.write_str("runtime error while playing track: ")?;
|
||||
match self {
|
||||
Self::Create(c) => {
|
||||
f.write_str("input creation [")?;
|
||||
f.write_fmt(format_args!("{}", &c))?;
|
||||
f.write_str("]")
|
||||
},
|
||||
Self::Parse(p) => {
|
||||
f.write_str("parsing formats/codecs [")?;
|
||||
f.write_fmt(format_args!("{}", &p))?;
|
||||
f.write_str("]")
|
||||
},
|
||||
Self::Decode(d) => {
|
||||
f.write_str("decoding packets [")?;
|
||||
f.write_fmt(format_args!("{}", &d))?;
|
||||
f.write_str("]")
|
||||
},
|
||||
Self::Seek(s) => {
|
||||
f.write_str("seeking along input [")?;
|
||||
f.write_fmt(format_args!("{}", &s))?;
|
||||
f.write_str("]")
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for PlayError {}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use super::*;
|
||||
use crate::{
|
||||
events::{Event, EventData, EventHandler},
|
||||
input::Metadata,
|
||||
};
|
||||
use flume::Sender;
|
||||
use crate::events::{Event, EventData, EventHandler};
|
||||
use flume::{Receiver, Sender};
|
||||
use std::{fmt, sync::Arc, time::Duration};
|
||||
use tokio::sync::RwLock;
|
||||
use typemap_rev::TypeMap;
|
||||
@@ -17,8 +14,7 @@ use uuid::Uuid;
|
||||
///
|
||||
/// Many method calls here are fallible; in most cases, this will be because
|
||||
/// the underlying [`Track`] object has been discarded. Those which aren't refer
|
||||
/// to immutable properties of the underlying stream, or shared data not used
|
||||
/// by the driver.
|
||||
/// to shared data not used by the driver.
|
||||
///
|
||||
/// [`Track`]: Track
|
||||
pub struct TrackHandle {
|
||||
@@ -27,9 +23,7 @@ pub struct TrackHandle {
|
||||
|
||||
struct InnerHandle {
|
||||
command_channel: Sender<TrackCommand>,
|
||||
seekable: bool,
|
||||
uuid: Uuid,
|
||||
metadata: Box<Metadata>,
|
||||
typemap: RwLock<TypeMap>,
|
||||
}
|
||||
|
||||
@@ -37,30 +31,21 @@ impl fmt::Debug for InnerHandle {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("InnerHandle")
|
||||
.field("command_channel", &self.command_channel)
|
||||
.field("seekable", &self.seekable)
|
||||
.field("uuid", &self.uuid)
|
||||
.field("metadata", &self.metadata)
|
||||
.field("typemap", &"<LOCK>")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TrackHandle {
|
||||
/// Creates a new handle, using the given command sink and hint as to whether
|
||||
/// the underlying [`Input`] supports seek operations.
|
||||
/// Creates a new handle, using the given command sink.
|
||||
///
|
||||
/// [`Input`]: crate::input::Input
|
||||
pub fn new(
|
||||
command_channel: Sender<TrackCommand>,
|
||||
seekable: bool,
|
||||
uuid: Uuid,
|
||||
metadata: Box<Metadata>,
|
||||
) -> Self {
|
||||
#[must_use]
|
||||
pub(crate) fn new(command_channel: Sender<TrackCommand>, uuid: Uuid) -> Self {
|
||||
let inner = Arc::new(InnerHandle {
|
||||
command_channel,
|
||||
seekable,
|
||||
uuid,
|
||||
metadata,
|
||||
typemap: RwLock::new(TypeMap::new()),
|
||||
});
|
||||
|
||||
@@ -92,53 +77,64 @@ impl TrackHandle {
|
||||
self.send(TrackCommand::Volume(volume))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Ready a track for playing if it is lazily initialised.
|
||||
///
|
||||
/// Currently, only [`Restartable`] sources support lazy setup.
|
||||
/// This call is a no-op for all others.
|
||||
///
|
||||
/// [`Restartable`]: crate::input::restartable::Restartable
|
||||
pub fn make_playable(&self) -> TrackResult<()> {
|
||||
self.send(TrackCommand::MakePlayable)
|
||||
/// If a track is already playable, the callback will instantly succeed.
|
||||
pub fn make_playable(&self) -> TrackCallback<()> {
|
||||
let (tx, rx) = flume::bounded(1);
|
||||
let fail = self.send(TrackCommand::MakePlayable(tx)).is_err();
|
||||
|
||||
TrackCallback { fail, rx }
|
||||
}
|
||||
|
||||
/// Denotes whether the underlying [`Input`] stream is compatible with arbitrary seeking.
|
||||
/// Ready a track for playing if it is lazily initialised.
|
||||
///
|
||||
/// If this returns `false`, all calls to [`seek_time`] will fail, and the track is
|
||||
/// incapable of looping.
|
||||
/// This folds [`Self::make_playable`] into a single `async` result, but must
|
||||
/// be awaited for the command to be sent.
|
||||
pub async fn make_playable_async(&self) -> TrackResult<()> {
|
||||
self.make_playable().result_async().await
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Seeks along the track to the specified position.
|
||||
///
|
||||
/// If the underlying [`Input`] does not support seeking,
|
||||
/// forward seeks will succeed. Backward seeks will recreate the
|
||||
/// track using the lazy [`Compose`] if present. The returned callback
|
||||
/// will indicate whether the seek succeeded.
|
||||
///
|
||||
/// [`seek_time`]: TrackHandle::seek_time
|
||||
/// [`Input`]: crate::input::Input
|
||||
pub fn is_seekable(&self) -> bool {
|
||||
self.inner.seekable
|
||||
/// [`Compose`]: crate::input::Compose
|
||||
pub fn seek(&self, position: Duration) -> TrackCallback<Duration> {
|
||||
let (tx, rx) = flume::bounded(1);
|
||||
let fail = self
|
||||
.send(TrackCommand::Seek(SeekRequest {
|
||||
time: position,
|
||||
callback: tx,
|
||||
}))
|
||||
.is_err();
|
||||
|
||||
TrackCallback { fail, rx }
|
||||
}
|
||||
|
||||
/// Seeks along the track to the specified position.
|
||||
///
|
||||
/// If the underlying [`Input`] does not support seeking,
|
||||
/// then all calls will fail with [`TrackError::SeekUnsupported`].
|
||||
///
|
||||
/// [`Input`]: crate::input::Input
|
||||
/// [`TrackError::SeekUnsupported`]: TrackError::SeekUnsupported
|
||||
pub fn seek_time(&self, position: Duration) -> TrackResult<()> {
|
||||
if self.is_seekable() {
|
||||
self.send(TrackCommand::Seek(position))
|
||||
} else {
|
||||
Err(TrackError::SeekUnsupported)
|
||||
}
|
||||
/// This folds [`Self::seek`] into a single `async` result, but must
|
||||
/// be awaited for the command to be sent.
|
||||
pub async fn seek_async(&self, position: Duration) -> TrackResult<Duration> {
|
||||
self.seek(position).result_async().await
|
||||
}
|
||||
|
||||
/// Attach an event handler to an audio track. These will receive [`EventContext::Track`].
|
||||
///
|
||||
/// Events which can only be fired by the global context return [`TrackError::InvalidTrackEvent`]
|
||||
/// Events which can only be fired by the global context return [`ControlError::InvalidTrackEvent`]
|
||||
///
|
||||
/// [`Track`]: Track
|
||||
/// [`EventContext::Track`]: crate::events::EventContext::Track
|
||||
/// [`TrackError::InvalidTrackEvent`]: TrackError::InvalidTrackEvent
|
||||
pub fn add_event<F: EventHandler + 'static>(&self, event: Event, action: F) -> TrackResult<()> {
|
||||
let cmd = TrackCommand::AddEvent(EventData::new(event, action));
|
||||
if event.is_global_only() {
|
||||
Err(TrackError::InvalidTrackEvent)
|
||||
Err(ControlError::InvalidTrackEvent)
|
||||
} else {
|
||||
self.send(cmd)
|
||||
}
|
||||
@@ -146,14 +142,17 @@ impl TrackHandle {
|
||||
|
||||
/// Perform an arbitrary synchronous action on a raw [`Track`] object.
|
||||
///
|
||||
/// This will give access to a [`View`] of the current track state and [`Metadata`],
|
||||
/// which can be used to take an [`Action`].
|
||||
///
|
||||
/// Users **must** ensure that no costly work or blocking occurs
|
||||
/// within the supplied function or closure. *Taking excess time could prevent
|
||||
/// timely sending of packets, causing audio glitches and delays*.
|
||||
///
|
||||
/// [`Track`]: Track
|
||||
/// [`Metadata`]: crate::input::Metadata
|
||||
pub fn action<F>(&self, action: F) -> TrackResult<()>
|
||||
where
|
||||
F: FnOnce(&mut Track) + Send + Sync + 'static,
|
||||
F: FnOnce(View) -> Option<Action> + Send + Sync + 'static,
|
||||
{
|
||||
self.send(TrackCommand::Do(Box::new(action)))
|
||||
}
|
||||
@@ -163,77 +162,52 @@ impl TrackHandle {
|
||||
let (tx, rx) = flume::bounded(1);
|
||||
self.send(TrackCommand::Request(tx))?;
|
||||
|
||||
rx.recv_async().await.map_err(|_| TrackError::Finished)
|
||||
rx.recv_async().await.map_err(|_| ControlError::Finished)
|
||||
}
|
||||
|
||||
/// Set an audio track to loop indefinitely.
|
||||
///
|
||||
/// If the underlying [`Input`] does not support seeking,
|
||||
/// then all calls will fail with [`TrackError::SeekUnsupported`].
|
||||
/// This requires either a [`Compose`] to be present or for the
|
||||
/// input stream to be seekable.
|
||||
///
|
||||
/// [`Input`]: crate::input::Input
|
||||
/// [`TrackError::SeekUnsupported`]: TrackError::SeekUnsupported
|
||||
/// [`Compose`]: crate::input::Compose
|
||||
pub fn enable_loop(&self) -> TrackResult<()> {
|
||||
if self.is_seekable() {
|
||||
self.send(TrackCommand::Loop(LoopState::Infinite))
|
||||
} else {
|
||||
Err(TrackError::SeekUnsupported)
|
||||
}
|
||||
self.send(TrackCommand::Loop(LoopState::Infinite))
|
||||
}
|
||||
|
||||
/// Set an audio track to no longer loop.
|
||||
///
|
||||
/// If the underlying [`Input`] does not support seeking,
|
||||
/// then all calls will fail with [`TrackError::SeekUnsupported`].
|
||||
/// This follows the same rules as [`enable_loop`].
|
||||
///
|
||||
/// [`Input`]: crate::input::Input
|
||||
/// [`TrackError::SeekUnsupported`]: TrackError::SeekUnsupported
|
||||
/// [`enable_loop`]: Self::enable_loop
|
||||
pub fn disable_loop(&self) -> TrackResult<()> {
|
||||
if self.is_seekable() {
|
||||
self.send(TrackCommand::Loop(LoopState::Finite(0)))
|
||||
} else {
|
||||
Err(TrackError::SeekUnsupported)
|
||||
}
|
||||
self.send(TrackCommand::Loop(LoopState::Finite(0)))
|
||||
}
|
||||
|
||||
/// Set an audio track to loop a set number of times.
|
||||
///
|
||||
/// If the underlying [`Input`] does not support seeking,
|
||||
/// then all calls will fail with [`TrackError::SeekUnsupported`].
|
||||
/// This follows the same rules as [`enable_loop`].
|
||||
///
|
||||
/// [`Input`]: crate::input::Input
|
||||
/// [`TrackError::SeekUnsupported`]: TrackError::SeekUnsupported
|
||||
/// [`enable_loop`]: Self::enable_loop
|
||||
pub fn loop_for(&self, count: usize) -> TrackResult<()> {
|
||||
if self.is_seekable() {
|
||||
self.send(TrackCommand::Loop(LoopState::Finite(count)))
|
||||
} else {
|
||||
Err(TrackError::SeekUnsupported)
|
||||
}
|
||||
self.send(TrackCommand::Loop(LoopState::Finite(count)))
|
||||
}
|
||||
|
||||
/// Returns this handle's (and track's) unique identifier.
|
||||
#[must_use]
|
||||
pub fn uuid(&self) -> Uuid {
|
||||
self.inner.uuid
|
||||
}
|
||||
|
||||
/// Returns the metadata stored in the handle.
|
||||
/// Allows access to this track's attached [`TypeMap`].
|
||||
///
|
||||
/// Metadata is cloned from the inner [`Input`] at
|
||||
/// the time a track/handle is created, and is effectively
|
||||
/// read-only from then on.
|
||||
///
|
||||
/// [`Input`]: crate::input::Input
|
||||
pub fn metadata(&self) -> &Metadata {
|
||||
&self.inner.metadata
|
||||
}
|
||||
|
||||
/// Allows access to this track's attached TypeMap.
|
||||
///
|
||||
/// TypeMaps allow additional, user-defined data shared by all handles
|
||||
/// [`TypeMap`]s allow additional, user-defined data shared by all handles
|
||||
/// to be attached to any track.
|
||||
///
|
||||
/// Driver code will never attempt to lock access to this map,
|
||||
/// preventing deadlock/stalling.
|
||||
#[must_use]
|
||||
pub fn typemap(&self) -> &RwLock<TypeMap> {
|
||||
&self.inner.typemap
|
||||
}
|
||||
@@ -242,12 +216,95 @@ impl TrackHandle {
|
||||
/// Send a raw command to the [`Track`] object.
|
||||
///
|
||||
/// [`Track`]: Track
|
||||
pub fn send(&self, cmd: TrackCommand) -> TrackResult<()> {
|
||||
pub(crate) fn send(&self, cmd: TrackCommand) -> TrackResult<()> {
|
||||
// As the send channels are unbounded, we can be reasonably certain
|
||||
// that send failure == cancellation.
|
||||
self.inner
|
||||
.command_channel
|
||||
.send(cmd)
|
||||
.map_err(|_e| TrackError::Finished)
|
||||
.map_err(|_e| ControlError::Finished)
|
||||
}
|
||||
}
|
||||
|
||||
/// Asynchronous reply for an operation applied to a [`TrackHandle`].
|
||||
///
|
||||
/// This object does not need to be `.await`ed for the driver to perform an action.
|
||||
/// Async threads can then call, e.g., [`TrackHandle::make_playable`], and safely drop
|
||||
/// this callback if the result isn't needed.
|
||||
pub struct TrackCallback<T> {
|
||||
fail: bool,
|
||||
rx: Receiver<Result<T, PlayError>>,
|
||||
}
|
||||
|
||||
impl<T> TrackCallback<T> {
|
||||
/// Consumes this handle to await a reply from the driver, blocking the current thread.
|
||||
pub fn result(self) -> TrackResult<T> {
|
||||
if self.fail {
|
||||
Err(ControlError::Finished)
|
||||
} else {
|
||||
self.rx.recv()?.map_err(ControlError::Play)
|
||||
}
|
||||
}
|
||||
|
||||
/// Consumes this handle to await a reply from the driver asynchronously.
|
||||
pub async fn result_async(self) -> TrackResult<T> {
|
||||
if self.fail {
|
||||
Err(ControlError::Finished)
|
||||
} else {
|
||||
self.rx.recv_async().await?.map_err(ControlError::Play)
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Returns `true` if the operation instantly failed due to the target track being
|
||||
/// removed.
|
||||
pub fn is_hung_up(&self) -> bool {
|
||||
self.fail
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
constants::test_data::FILE_WAV_TARGET,
|
||||
driver::Driver,
|
||||
input::File,
|
||||
tracks::Track,
|
||||
Config,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
#[ntest::timeout(10_000)]
|
||||
async fn make_playable_callback_fires() {
|
||||
let (t_handle, config) = Config::test_cfg(true);
|
||||
let mut driver = Driver::new(config.clone());
|
||||
|
||||
let file = File::new(FILE_WAV_TARGET);
|
||||
let handle = driver.play(Track::from(file).pause());
|
||||
|
||||
let callback = handle.make_playable();
|
||||
t_handle.spawn_ticker().await;
|
||||
assert!(callback.result_async().await.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ntest::timeout(10_000)]
|
||||
async fn seek_callback_fires() {
|
||||
let (t_handle, config) = Config::test_cfg(true);
|
||||
let mut driver = Driver::new(config.clone());
|
||||
|
||||
let file = File::new(FILE_WAV_TARGET);
|
||||
let handle = driver.play(Track::from(file).pause());
|
||||
|
||||
let target = Duration::from_millis(500);
|
||||
let callback = handle.seek(target);
|
||||
t_handle.spawn_ticker().await;
|
||||
|
||||
let answer = callback.result_async().await;
|
||||
assert!(answer.is_ok());
|
||||
let answer = answer.unwrap();
|
||||
let delta = Duration::from_millis(100);
|
||||
assert!(answer > target - delta && answer < target + delta);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,3 +20,109 @@ impl Default for LoopState {
|
||||
Self::Finite(0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
constants::test_data::FILE_WAV_TARGET,
|
||||
driver::Driver,
|
||||
input::File,
|
||||
tracks::{PlayMode, Track, TrackState},
|
||||
Config,
|
||||
Event,
|
||||
EventContext,
|
||||
EventHandler,
|
||||
TrackEvent,
|
||||
};
|
||||
use flume::Sender;
|
||||
|
||||
struct Looper {
|
||||
tx: Sender<TrackState>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl EventHandler for Looper {
|
||||
async fn act(&self, ctx: &crate::EventContext<'_>) -> Option<Event> {
|
||||
if let EventContext::Track(&[(state, _)]) = ctx {
|
||||
drop(self.tx.send(state.clone()));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ntest::timeout(10_000)]
|
||||
async fn finite_track_loops_work() {
|
||||
let (t_handle, config) = Config::test_cfg(true);
|
||||
let mut driver = Driver::new(config.clone());
|
||||
|
||||
let file = File::new(FILE_WAV_TARGET);
|
||||
let handle = driver.play(Track::from(file).loops(LoopState::Finite(2)));
|
||||
|
||||
let (l_tx, l_rx) = flume::unbounded();
|
||||
let (e_tx, e_rx) = flume::unbounded();
|
||||
let _ = handle.add_event(Event::Track(TrackEvent::Loop), Looper { tx: l_tx });
|
||||
let _ = handle.add_event(Event::Track(TrackEvent::End), Looper { tx: e_tx });
|
||||
|
||||
t_handle.spawn_ticker().await;
|
||||
|
||||
// CONDITIONS:
|
||||
// 1) 2 loop events, each changes the loop count.
|
||||
// 2) Track ends.
|
||||
// 3) Playtime >> Position
|
||||
assert_eq!(
|
||||
l_rx.recv_async().await.map(|v| v.loops),
|
||||
Ok(LoopState::Finite(1))
|
||||
);
|
||||
assert_eq!(
|
||||
l_rx.recv_async().await.map(|v| v.loops),
|
||||
Ok(LoopState::Finite(0))
|
||||
);
|
||||
let ended = e_rx.recv_async().await;
|
||||
|
||||
assert!(ended.is_ok());
|
||||
|
||||
let ended = ended.unwrap();
|
||||
assert!(ended.play_time > 2 * ended.position);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ntest::timeout(10_000)]
|
||||
async fn infinite_track_loops_work() {
|
||||
let (t_handle, config) = Config::test_cfg(true);
|
||||
let mut driver = Driver::new(config.clone());
|
||||
|
||||
let file = File::new(FILE_WAV_TARGET);
|
||||
let handle = driver.play(Track::from(file).loops(LoopState::Infinite));
|
||||
|
||||
let (l_tx, l_rx) = flume::unbounded();
|
||||
let _ = handle.add_event(Event::Track(TrackEvent::Loop), Looper { tx: l_tx });
|
||||
|
||||
t_handle.spawn_ticker().await;
|
||||
|
||||
// CONDITIONS:
|
||||
// 1) 3 loop events, each does not change the loop count.
|
||||
// 2) Track still playing at final
|
||||
// 3) Playtime >> Position
|
||||
assert_eq!(
|
||||
l_rx.recv_async().await.map(|v| v.loops),
|
||||
Ok(LoopState::Infinite)
|
||||
);
|
||||
assert_eq!(
|
||||
l_rx.recv_async().await.map(|v| v.loops),
|
||||
Ok(LoopState::Infinite)
|
||||
);
|
||||
|
||||
let final_state = l_rx.recv_async().await;
|
||||
assert_eq!(
|
||||
final_state.as_ref().map(|v| v.loops),
|
||||
Ok(LoopState::Infinite)
|
||||
);
|
||||
let final_state = final_state.unwrap();
|
||||
|
||||
assert_eq!(final_state.playing, PlayMode::Play);
|
||||
assert!(final_state.play_time > 2 * final_state.position);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,383 +14,189 @@
|
||||
//! [`TrackHandle`]: struct.TrackHandle.html
|
||||
//! [`create_player`]: fn.create_player.html
|
||||
|
||||
mod action;
|
||||
mod command;
|
||||
mod error;
|
||||
mod handle;
|
||||
mod looping;
|
||||
mod mode;
|
||||
mod queue;
|
||||
mod ready;
|
||||
mod state;
|
||||
mod view;
|
||||
|
||||
pub use self::{command::*, error::*, handle::*, looping::*, mode::*, queue::*, state::*};
|
||||
pub use self::{
|
||||
action::*,
|
||||
error::*,
|
||||
handle::*,
|
||||
looping::*,
|
||||
mode::*,
|
||||
queue::*,
|
||||
ready::*,
|
||||
state::*,
|
||||
view::*,
|
||||
};
|
||||
pub(crate) use command::*;
|
||||
|
||||
use crate::{constants::*, driver::tasks::message::*, events::EventStore, input::Input};
|
||||
use flume::{Receiver, TryRecvError};
|
||||
use std::time::Duration;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Control object for audio playback.
|
||||
/// Initial state for audio playback.
|
||||
///
|
||||
/// Accessed by both commands and the playback code -- as such, access from user code is
|
||||
/// almost always guarded via a [`TrackHandle`]. You should expect to receive
|
||||
/// access to a raw object of this type via [`create_player`], for use in
|
||||
/// [`Driver::play`] or [`Driver::play_only`].
|
||||
/// [`Track`]s allow you to configure play modes, volume, event handlers, and other track state
|
||||
/// before you pass an input to the [`Driver`].
|
||||
///
|
||||
/// Live track data is accesseed via a [`TrackHandle`], returned by [`Driver::play`] and
|
||||
/// related methods.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use songbird::{driver::Driver, ffmpeg, tracks::create_player};
|
||||
/// use songbird::{driver::Driver, input::File, tracks::Track};
|
||||
///
|
||||
/// # async {
|
||||
/// // A Call is also valid here!
|
||||
/// let mut handler: Driver = Default::default();
|
||||
/// let source = ffmpeg("../audio/my-favourite-song.mp3")
|
||||
/// .await
|
||||
/// .expect("This might fail: handle this error!");
|
||||
/// let (mut audio, audio_handle) = create_player(source);
|
||||
/// let source = File::new("../audio/my-favourite-song.mp3");
|
||||
///
|
||||
/// audio.set_volume(0.5);
|
||||
///
|
||||
/// handler.play_only(audio);
|
||||
/// handler.play_only(Track::new(source.into()).volume(0.5));
|
||||
///
|
||||
/// // Future access occurs via audio_handle.
|
||||
/// # };
|
||||
/// ```
|
||||
///
|
||||
/// [`Driver::play_only`]: crate::driver::Driver::play_only
|
||||
/// [`Driver`]: crate::driver::Driver
|
||||
/// [`Driver::play`]: crate::driver::Driver::play
|
||||
/// [`TrackHandle`]: TrackHandle
|
||||
/// [`create_player`]: create_player
|
||||
#[derive(Debug)]
|
||||
pub struct Track {
|
||||
/// Whether or not this sound is currently playing.
|
||||
///
|
||||
/// Can be controlled with [`play`] or [`pause`] if chaining is desired.
|
||||
///
|
||||
/// [`play`]: Track::play
|
||||
/// [`pause`]: Track::pause
|
||||
pub(crate) playing: PlayMode,
|
||||
/// Defaults to [`PlayMode::Play`].
|
||||
pub playing: PlayMode,
|
||||
|
||||
/// The desired volume for playback.
|
||||
/// The volume for playback.
|
||||
///
|
||||
/// Sensible values fall between `0.0` and `1.0`.
|
||||
/// Sensible values fall between `0.0` and `1.0`. Values outside this range can
|
||||
/// cause clipping or other audio artefacts.
|
||||
///
|
||||
/// Can be controlled with [`volume`] if chaining is desired.
|
||||
///
|
||||
/// [`volume`]: Track::volume
|
||||
pub(crate) volume: f32,
|
||||
/// Defaults to `1.0`.
|
||||
pub volume: f32,
|
||||
|
||||
/// Underlying data access object.
|
||||
///
|
||||
/// *Calling code is not expected to use this.*
|
||||
pub(crate) source: Input,
|
||||
|
||||
/// The current playback position in the track.
|
||||
pub(crate) position: Duration,
|
||||
|
||||
/// The total length of time this track has been active.
|
||||
pub(crate) play_time: Duration,
|
||||
/// The live or lazily-initialised audio stream to be played.
|
||||
pub input: Input,
|
||||
|
||||
/// List of events attached to this audio track.
|
||||
///
|
||||
/// This may be used to add additional events to a track
|
||||
/// before it is sent to the audio context for playing.
|
||||
pub events: Option<EventStore>,
|
||||
|
||||
/// Channel from which commands are received.
|
||||
///
|
||||
/// Track commands are sent in this manner to ensure that access
|
||||
/// occurs in a thread-safe manner, without allowing any external
|
||||
/// code to lock access to audio objects and block packet generation.
|
||||
pub(crate) commands: Receiver<TrackCommand>,
|
||||
|
||||
/// Handle for safe control of this audio track from other threads.
|
||||
///
|
||||
/// Typically, this is used by internal code to supply context information
|
||||
/// to event handlers, though more may be cloned from this handle.
|
||||
pub handle: TrackHandle,
|
||||
/// Defaults to an empty set.
|
||||
pub events: EventStore,
|
||||
|
||||
/// Count of remaining loops.
|
||||
///
|
||||
/// Defaults to play a track once (i.e., [`LoopState::Finite(0)`]).
|
||||
///
|
||||
/// [`LoopState::Finite(0)`]: LoopState::Finite
|
||||
pub loops: LoopState,
|
||||
|
||||
/// Unique identifier for this track.
|
||||
pub(crate) uuid: Uuid,
|
||||
///
|
||||
/// Defaults to a random 128-bit number.
|
||||
pub uuid: Uuid,
|
||||
}
|
||||
|
||||
impl Track {
|
||||
/// Create a new track directly from an input, command source,
|
||||
/// and handle.
|
||||
///
|
||||
/// In general, you should probably use [`create_player`].
|
||||
///
|
||||
/// [`create_player`]: fn.create_player.html
|
||||
pub fn new_raw(source: Input, commands: Receiver<TrackCommand>, handle: TrackHandle) -> Self {
|
||||
let uuid = handle.uuid();
|
||||
/// Create a new track directly from an [`Input`] and a random [`Uuid`].
|
||||
#[must_use]
|
||||
pub fn new(input: Input) -> Self {
|
||||
let uuid = Uuid::new_v4();
|
||||
|
||||
Self::new_with_uuid(input, uuid)
|
||||
}
|
||||
|
||||
/// Create a new track directly from an [`Input`] with a custom [`Uuid`].
|
||||
#[must_use]
|
||||
pub fn new_with_uuid(input: Input, uuid: Uuid) -> Self {
|
||||
Self {
|
||||
playing: Default::default(),
|
||||
playing: PlayMode::default(),
|
||||
volume: 1.0,
|
||||
source,
|
||||
position: Default::default(),
|
||||
play_time: Default::default(),
|
||||
events: Some(EventStore::new_local()),
|
||||
commands,
|
||||
handle,
|
||||
input,
|
||||
events: EventStore::new_local(),
|
||||
loops: LoopState::Finite(0),
|
||||
uuid,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Sets a track to playing if it is paused.
|
||||
pub fn play(&mut self) -> &mut Self {
|
||||
self.set_playing(PlayMode::Play)
|
||||
}
|
||||
|
||||
/// Pauses a track if it is playing.
|
||||
pub fn pause(&mut self) -> &mut Self {
|
||||
self.set_playing(PlayMode::Pause)
|
||||
}
|
||||
|
||||
/// Manually stops a track.
|
||||
///
|
||||
/// This will cause the audio track to be removed, with any relevant events triggered.
|
||||
/// Stopped/ended tracks cannot be restarted.
|
||||
pub fn stop(&mut self) -> &mut Self {
|
||||
self.set_playing(PlayMode::Stop)
|
||||
}
|
||||
|
||||
pub(crate) fn end(&mut self) -> &mut Self {
|
||||
self.set_playing(PlayMode::End)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_playing(&mut self, new_state: PlayMode) -> &mut Self {
|
||||
self.playing = self.playing.change_to(new_state);
|
||||
|
||||
pub fn play(mut self) -> Self {
|
||||
self.playing = PlayMode::Play;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the current play status of this track.
|
||||
pub fn playing(&self) -> PlayMode {
|
||||
self.playing
|
||||
#[must_use]
|
||||
/// Pre-emptively pauses a track, preventing it from being automatically played.
|
||||
pub fn pause(mut self) -> Self {
|
||||
self.playing = PlayMode::Pause;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Manually stops a track.
|
||||
///
|
||||
/// This will cause the audio track to be removed by the driver almost immediately,
|
||||
/// with any relevant events triggered.
|
||||
pub fn stop(mut self) -> Self {
|
||||
self.playing = PlayMode::Stop;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Sets [`volume`] in a manner that allows method chaining.
|
||||
///
|
||||
/// [`volume`]: Track::volume
|
||||
pub fn set_volume(&mut self, volume: f32) -> &mut Self {
|
||||
pub fn volume(mut self, volume: f32) -> Self {
|
||||
self.volume = volume;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the current playback position.
|
||||
pub fn volume(&self) -> f32 {
|
||||
self.volume
|
||||
}
|
||||
|
||||
/// Returns the current playback position.
|
||||
pub fn position(&self) -> Duration {
|
||||
self.position
|
||||
}
|
||||
|
||||
/// Returns the total length of time this track has been active.
|
||||
pub fn play_time(&self) -> Duration {
|
||||
self.play_time
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Set an audio track to loop a set number of times.
|
||||
///
|
||||
/// If the underlying [`Input`] does not support seeking,
|
||||
/// then all calls will fail with [`TrackError::SeekUnsupported`].
|
||||
///
|
||||
/// [`Input`]: crate::input::Input
|
||||
/// [`TrackError::SeekUnsupported`]: TrackError::SeekUnsupported
|
||||
pub fn set_loops(&mut self, loops: LoopState) -> TrackResult<()> {
|
||||
if self.source.is_seekable() {
|
||||
self.loops = loops;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(TrackError::SeekUnsupported)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn do_loop(&mut self) -> bool {
|
||||
match self.loops {
|
||||
LoopState::Infinite => true,
|
||||
LoopState::Finite(0) => false,
|
||||
LoopState::Finite(ref mut n) => {
|
||||
*n -= 1;
|
||||
true
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Steps playback location forward by one frame.
|
||||
pub(crate) fn step_frame(&mut self) {
|
||||
self.position += TIMESTEP_LENGTH;
|
||||
self.play_time += TIMESTEP_LENGTH;
|
||||
}
|
||||
|
||||
/// Receives and acts upon any commands forwarded by TrackHandles.
|
||||
///
|
||||
/// *Used internally*, this should not be exposed to users.
|
||||
pub(crate) fn process_commands(&mut self, index: usize, ic: &Interconnect) {
|
||||
// Note: disconnection and an empty channel are both valid,
|
||||
// and should allow the audio object to keep running as intended.
|
||||
|
||||
// Note that interconnect failures are not currently errors.
|
||||
// In correct operation, the event thread should never panic,
|
||||
// but it receiving status updates is secondary do actually
|
||||
// doing the work.
|
||||
loop {
|
||||
match self.commands.try_recv() {
|
||||
Ok(cmd) => {
|
||||
use TrackCommand::*;
|
||||
match cmd {
|
||||
Play => {
|
||||
self.play();
|
||||
let _ = ic.events.send(EventMessage::ChangeState(
|
||||
index,
|
||||
TrackStateChange::Mode(self.playing),
|
||||
));
|
||||
},
|
||||
Pause => {
|
||||
self.pause();
|
||||
let _ = ic.events.send(EventMessage::ChangeState(
|
||||
index,
|
||||
TrackStateChange::Mode(self.playing),
|
||||
));
|
||||
},
|
||||
Stop => {
|
||||
self.stop();
|
||||
let _ = ic.events.send(EventMessage::ChangeState(
|
||||
index,
|
||||
TrackStateChange::Mode(self.playing),
|
||||
));
|
||||
},
|
||||
Volume(vol) => {
|
||||
self.set_volume(vol);
|
||||
let _ = ic.events.send(EventMessage::ChangeState(
|
||||
index,
|
||||
TrackStateChange::Volume(self.volume),
|
||||
));
|
||||
},
|
||||
Seek(time) =>
|
||||
if let Ok(new_time) = self.seek_time(time) {
|
||||
let _ = ic.events.send(EventMessage::ChangeState(
|
||||
index,
|
||||
TrackStateChange::Position(new_time),
|
||||
));
|
||||
},
|
||||
AddEvent(evt) => {
|
||||
let _ = ic.events.send(EventMessage::AddTrackEvent(index, evt));
|
||||
},
|
||||
Do(action) => {
|
||||
action(self);
|
||||
let _ = ic.events.send(EventMessage::ChangeState(
|
||||
index,
|
||||
TrackStateChange::Total(self.state()),
|
||||
));
|
||||
},
|
||||
Request(tx) => {
|
||||
let _ = tx.send(self.state());
|
||||
},
|
||||
Loop(loops) =>
|
||||
if self.set_loops(loops).is_ok() {
|
||||
let _ = ic.events.send(EventMessage::ChangeState(
|
||||
index,
|
||||
TrackStateChange::Loops(self.loops, true),
|
||||
));
|
||||
},
|
||||
MakePlayable => self.make_playable(),
|
||||
}
|
||||
},
|
||||
Err(TryRecvError::Disconnected) => {
|
||||
// this branch will never be visited.
|
||||
break;
|
||||
},
|
||||
Err(TryRecvError::Empty) => {
|
||||
break;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ready a track for playing if it is lazily initialised.
|
||||
///
|
||||
/// Currently, only [`Restartable`] sources support lazy setup.
|
||||
/// This call is a no-op for all others.
|
||||
///
|
||||
/// [`Restartable`]: crate::input::restartable::Restartable
|
||||
pub fn make_playable(&mut self) {
|
||||
self.source.reader.make_playable();
|
||||
}
|
||||
|
||||
/// Creates a read-only copy of the audio track's state.
|
||||
///
|
||||
/// The primary use-case of this is sending information across
|
||||
/// threads in response to a [`TrackHandle`].
|
||||
///
|
||||
/// [`TrackHandle`]: TrackHandle
|
||||
pub fn state(&self) -> TrackState {
|
||||
TrackState {
|
||||
playing: self.playing,
|
||||
volume: self.volume,
|
||||
position: self.position,
|
||||
play_time: self.play_time,
|
||||
loops: self.loops,
|
||||
}
|
||||
}
|
||||
|
||||
/// Seek to a specific point in the track.
|
||||
///
|
||||
/// If the underlying [`Input`] does not support seeking,
|
||||
/// then all calls will fail with [`TrackError::SeekUnsupported`].
|
||||
///
|
||||
/// [`Input`]: crate::input::Input
|
||||
/// [`TrackError::SeekUnsupported`]: TrackError::SeekUnsupported
|
||||
pub fn seek_time(&mut self, pos: Duration) -> TrackResult<Duration> {
|
||||
if let Some(t) = self.source.seek_time(pos) {
|
||||
self.position = t;
|
||||
Ok(t)
|
||||
} else {
|
||||
Err(TrackError::SeekUnsupported)
|
||||
}
|
||||
pub fn loops(mut self, loops: LoopState) -> Self {
|
||||
self.loops = loops;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Returns this track's unique identifier.
|
||||
pub fn uuid(&self) -> Uuid {
|
||||
self.uuid
|
||||
pub fn uuid(mut self, uuid: Uuid) -> Self {
|
||||
self.uuid = uuid;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn into_context(self) -> (TrackHandle, TrackContext) {
|
||||
let (tx, receiver) = flume::unbounded();
|
||||
let handle = TrackHandle::new(tx, self.uuid);
|
||||
|
||||
let context = TrackContext {
|
||||
handle: handle.clone(),
|
||||
track: self,
|
||||
receiver,
|
||||
};
|
||||
|
||||
(handle, context)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a [`Track`] object to pass into the audio context, and a [`TrackHandle`]
|
||||
/// for safe, lock-free access in external code.
|
||||
///
|
||||
/// Typically, this would be used if you wished to directly work on or configure
|
||||
/// the [`Track`] object before it is passed over to the driver.
|
||||
///
|
||||
/// [`Track`]: Track
|
||||
/// [`TrackHandle`]: TrackHandle
|
||||
#[inline]
|
||||
pub fn create_player(source: Input) -> (Track, TrackHandle) {
|
||||
create_player_with_uuid(source, Uuid::new_v4())
|
||||
}
|
||||
|
||||
/// Creates a [`Track`] and [`TrackHandle`] as in [`create_player`], allowing
|
||||
/// a custom UUID to be set.
|
||||
///
|
||||
/// [`create_player`]: create_player
|
||||
/// [`Track`]: Track
|
||||
/// [`TrackHandle`]: TrackHandle
|
||||
pub fn create_player_with_uuid(source: Input, uuid: Uuid) -> (Track, TrackHandle) {
|
||||
let (tx, rx) = flume::unbounded();
|
||||
let can_seek = source.is_seekable();
|
||||
let metadata = source.metadata.clone();
|
||||
let handle = TrackHandle::new(tx, can_seek, uuid, metadata);
|
||||
|
||||
let player = Track::new_raw(source, rx, handle.clone());
|
||||
|
||||
(player, handle)
|
||||
/// Any [`Input`] (or struct which can be used as one) can also be made into a [`Track`].
|
||||
impl<T: Into<Input>> From<T> for Track {
|
||||
// NOTE: this is `Into` to support user-given structs which can
|
||||
// only `impl Into<Input>`.
|
||||
fn from(val: T) -> Self {
|
||||
Track::new(val.into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use super::PlayError;
|
||||
use crate::events::TrackEvent;
|
||||
|
||||
/// Playback status of a track.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum PlayMode {
|
||||
/// The track is currently playing.
|
||||
@@ -12,32 +13,56 @@ pub enum PlayMode {
|
||||
Stop,
|
||||
/// The track has naturally ended, and cannot be restarted.
|
||||
End,
|
||||
/// The track has encountered a runtime or initialisation error, and cannot be restarted.
|
||||
Errored(PlayError),
|
||||
}
|
||||
|
||||
impl PlayMode {
|
||||
/// Returns whether the track has irreversibly stopped.
|
||||
pub fn is_done(self) -> bool {
|
||||
matches!(self, PlayMode::Stop | PlayMode::End)
|
||||
#[must_use]
|
||||
pub fn is_done(&self) -> bool {
|
||||
matches!(self, PlayMode::Stop | PlayMode::End | PlayMode::Errored(_))
|
||||
}
|
||||
|
||||
pub(crate) fn change_to(self, other: Self) -> PlayMode {
|
||||
use PlayMode::*;
|
||||
/// Returns whether the track has irreversibly stopped.
|
||||
#[must_use]
|
||||
pub(crate) fn is_playing(&self) -> bool {
|
||||
matches!(self, PlayMode::Play)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn next_state(self, other: Self) -> Self {
|
||||
// Idea: a finished track cannot be restarted -- this action is final.
|
||||
// We may want to change this in future so that seekable tracks can uncancel
|
||||
// themselves, perhaps, but this requires a bit more machinery to readd...
|
||||
match self {
|
||||
Play | Pause => other,
|
||||
Self::Play | Self::Pause => other,
|
||||
state => state,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn as_track_event(self) -> TrackEvent {
|
||||
use PlayMode::*;
|
||||
pub(crate) fn change_to(&mut self, other: Self) {
|
||||
*self = self.clone().next_state(other);
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn as_track_event(&self) -> TrackEvent {
|
||||
match self {
|
||||
Play => TrackEvent::Play,
|
||||
Pause => TrackEvent::Pause,
|
||||
Stop | End => TrackEvent::End,
|
||||
Self::Play => TrackEvent::Play,
|
||||
Self::Pause => TrackEvent::Pause,
|
||||
Self::Stop | Self::End => TrackEvent::End,
|
||||
Self::Errored(_) => TrackEvent::Error,
|
||||
}
|
||||
}
|
||||
|
||||
// The above fn COULD just return a Vec, but the below means we only allocate a Vec
|
||||
// in the rare error case.
|
||||
// Also, see discussion on bitsets in src/events/track.rs
|
||||
#[must_use]
|
||||
pub(crate) fn also_fired_track_events(&self) -> Option<Vec<TrackEvent>> {
|
||||
match self {
|
||||
Self::Errored(_) => Some(vec![TrackEvent::End]),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,3 +72,11 @@ impl Default for PlayMode {
|
||||
PlayMode::Play
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for PlayMode {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.as_track_event() == other.as_track_event()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for PlayMode {}
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{
|
||||
driver::Driver,
|
||||
events::{Event, EventContext, EventData, EventHandler, TrackEvent},
|
||||
input::Input,
|
||||
tracks::{self, Track, TrackHandle, TrackResult},
|
||||
tracks::{Track, TrackHandle, TrackResult},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use parking_lot::Mutex;
|
||||
@@ -28,28 +28,27 @@ use tracing::{info, warn};
|
||||
/// use songbird::{
|
||||
/// driver::Driver,
|
||||
/// id::GuildId,
|
||||
/// ffmpeg,
|
||||
/// tracks::{create_player, TrackQueue},
|
||||
/// input::File,
|
||||
/// tracks::TrackQueue,
|
||||
/// };
|
||||
/// use std::collections::HashMap;
|
||||
/// use std::num::NonZeroU64;
|
||||
///
|
||||
/// # async {
|
||||
/// let guild = GuildId(0);
|
||||
/// let guild = GuildId(NonZeroU64::new(1).unwrap());
|
||||
/// // A Call is also valid here!
|
||||
/// let mut driver: Driver = Default::default();
|
||||
///
|
||||
/// let mut queues: HashMap<GuildId, TrackQueue> = Default::default();
|
||||
///
|
||||
/// let source = ffmpeg("../audio/my-favourite-song.mp3")
|
||||
/// .await
|
||||
/// .expect("This might fail: handle this error!");
|
||||
/// let source = File::new("../audio/my-favourite-song.mp3");
|
||||
///
|
||||
/// // We need to ensure that this guild has a TrackQueue created for it.
|
||||
/// let queue = queues.entry(guild)
|
||||
/// .or_default();
|
||||
///
|
||||
/// // Queueing a track is this easy!
|
||||
/// queue.add_source(source, &mut driver);
|
||||
/// queue.add_source(source.into(), &mut driver);
|
||||
/// # };
|
||||
/// ```
|
||||
///
|
||||
@@ -77,6 +76,7 @@ impl Deref for Queued {
|
||||
|
||||
impl Queued {
|
||||
/// Clones the inner handle
|
||||
#[must_use]
|
||||
pub fn handle(&self) -> TrackHandle {
|
||||
self.0.clone()
|
||||
}
|
||||
@@ -147,7 +147,9 @@ impl EventHandler for SongPreloader {
|
||||
let inner = self.remote_lock.lock();
|
||||
|
||||
if let Some(track) = inner.tracks.get(1) {
|
||||
let _ = track.0.make_playable();
|
||||
// This is the sync-version so that we can fire and ignore
|
||||
// the request ASAP.
|
||||
drop(track.0.make_playable());
|
||||
}
|
||||
|
||||
None
|
||||
@@ -156,6 +158,7 @@ impl EventHandler for SongPreloader {
|
||||
|
||||
impl TrackQueue {
|
||||
/// Create a new, empty, track queue.
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Mutex::new(TrackQueueCore {
|
||||
@@ -164,72 +167,99 @@ impl TrackQueue {
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds an audio source to the queue, to be played in the channel managed by `handler`.
|
||||
pub fn add_source(&self, source: Input, handler: &mut Driver) -> TrackHandle {
|
||||
let (track, handle) = tracks::create_player(source);
|
||||
self.add(track, handler);
|
||||
/// Adds an audio source to the queue, to be played in the channel managed by `driver`.
|
||||
///
|
||||
/// This method will preload the next track 5 seconds before the current track ends, if
|
||||
/// the [`AuxMetadata`] can be successfully queried for a [`Duration`].
|
||||
///
|
||||
/// [`AuxMetadata`]: crate::input::AuxMetadata
|
||||
pub async fn add_source(&self, input: Input, driver: &mut Driver) -> TrackHandle {
|
||||
self.add(input.into(), driver).await
|
||||
}
|
||||
|
||||
/// Adds a [`Track`] object to the queue, to be played in the channel managed by `driver`.
|
||||
///
|
||||
/// This allows additional configuration or event handlers to be added
|
||||
/// before enqueueing the audio track. [`Track`]s will be paused pre-emptively.
|
||||
///
|
||||
/// This method will preload the next track 5 seconds before the current track ends, if
|
||||
/// the [`AuxMetadata`] can be successfully queried for a [`Duration`].
|
||||
///
|
||||
/// [`AuxMetadata`]: crate::input::AuxMetadata
|
||||
pub async fn add(&self, mut track: Track, driver: &mut Driver) -> TrackHandle {
|
||||
let preload_time = Self::get_preload_time(&mut track).await;
|
||||
self.add_with_preload(track, driver, preload_time)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_preload_time(track: &mut Track) -> Option<Duration> {
|
||||
let meta = match track.input {
|
||||
Input::Lazy(ref mut rec) => rec.aux_metadata().await.ok(),
|
||||
Input::Live(_, Some(ref mut rec)) => rec.aux_metadata().await.ok(),
|
||||
Input::Live(_, None) => None,
|
||||
};
|
||||
|
||||
meta.and_then(|meta| meta.duration)
|
||||
.map(|d| d.saturating_sub(Duration::from_secs(5)))
|
||||
}
|
||||
|
||||
/// Add an existing [`Track`] to the queue, using a known time to preload the next track.
|
||||
///
|
||||
/// `preload_time` can be specified to enable gapless playback: this is the
|
||||
/// playback position *in this track* when the the driver will begin to load the next track.
|
||||
/// The standard [`Self::add`] method use [`AuxMetadata`] to set this to 5 seconds before
|
||||
/// a track ends.
|
||||
///
|
||||
/// A `None` value will not ready the next track until this track ends, disabling preload.
|
||||
///
|
||||
/// [`AuxMetadata`]: crate::input::AuxMetadata
|
||||
#[inline]
|
||||
pub fn add_with_preload(
|
||||
&self,
|
||||
mut track: Track,
|
||||
driver: &mut Driver,
|
||||
preload_time: Option<Duration>,
|
||||
) -> TrackHandle {
|
||||
// Attempts to start loading the next track before this one ends.
|
||||
// Idea is to provide as close to gapless playback as possible,
|
||||
// while minimising memory use.
|
||||
info!("Track added to queue.");
|
||||
|
||||
let remote_lock = self.inner.clone();
|
||||
track.events.add_event(
|
||||
EventData::new(Event::Track(TrackEvent::End), QueueHandler { remote_lock }),
|
||||
Duration::ZERO,
|
||||
);
|
||||
|
||||
if let Some(time) = preload_time {
|
||||
let remote_lock = self.inner.clone();
|
||||
track.events.add_event(
|
||||
EventData::new(Event::Delayed(time), SongPreloader { remote_lock }),
|
||||
Duration::ZERO,
|
||||
);
|
||||
}
|
||||
|
||||
let (should_play, handle) = {
|
||||
let mut inner = self.inner.lock();
|
||||
|
||||
let handle = driver.play(track.pause());
|
||||
inner.tracks.push_back(Queued(handle.clone()));
|
||||
|
||||
(inner.tracks.len() == 1, handle)
|
||||
};
|
||||
|
||||
if should_play {
|
||||
drop(handle.play());
|
||||
}
|
||||
|
||||
handle
|
||||
}
|
||||
|
||||
/// Adds a [`Track`] object to the queue, to be played in the channel managed by `handler`.
|
||||
///
|
||||
/// This is used with [`create_player`] if additional configuration or event handlers
|
||||
/// are required before enqueueing the audio track.
|
||||
///
|
||||
/// [`Track`]: Track
|
||||
/// [`create_player`]: super::create_player
|
||||
pub fn add(&self, mut track: Track, handler: &mut Driver) {
|
||||
self.add_raw(&mut track);
|
||||
handler.play(track);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn add_raw(&self, track: &mut Track) {
|
||||
info!("Track added to queue.");
|
||||
let remote_lock = self.inner.clone();
|
||||
let mut inner = self.inner.lock();
|
||||
|
||||
let track_handle = track.handle.clone();
|
||||
|
||||
if !inner.tracks.is_empty() {
|
||||
track.pause();
|
||||
}
|
||||
|
||||
track
|
||||
.events
|
||||
.as_mut()
|
||||
.expect("Queue inspecting EventStore on new Track: did not exist.")
|
||||
.add_event(
|
||||
EventData::new(Event::Track(TrackEvent::End), QueueHandler { remote_lock }),
|
||||
track.position,
|
||||
);
|
||||
|
||||
// Attempts to start loading the next track before this one ends.
|
||||
// Idea is to provide as close to gapless playback as possible,
|
||||
// while minimising memory use.
|
||||
if let Some(time) = track.source.metadata.duration {
|
||||
let preload_time = time.checked_sub(Duration::from_secs(5)).unwrap_or_default();
|
||||
let remote_lock = self.inner.clone();
|
||||
|
||||
track
|
||||
.events
|
||||
.as_mut()
|
||||
.expect("Queue inspecting EventStore on new Track: did not exist.")
|
||||
.add_event(
|
||||
EventData::new(Event::Delayed(preload_time), SongPreloader { remote_lock }),
|
||||
track.position,
|
||||
);
|
||||
}
|
||||
|
||||
inner.tracks.push_back(Queued(track_handle));
|
||||
}
|
||||
|
||||
/// Returns a handle to the currently playing track.
|
||||
#[must_use]
|
||||
pub fn current(&self) -> Option<TrackHandle> {
|
||||
let inner = self.inner.lock();
|
||||
|
||||
inner.tracks.front().map(|h| h.handle())
|
||||
inner.tracks.front().map(Queued::handle)
|
||||
}
|
||||
|
||||
/// Attempts to remove a track from the specified index.
|
||||
@@ -237,11 +267,13 @@ impl TrackQueue {
|
||||
/// The returned entry can be readded to *this* queue via [`modify_queue`].
|
||||
///
|
||||
/// [`modify_queue`]: TrackQueue::modify_queue
|
||||
#[must_use]
|
||||
pub fn dequeue(&self, index: usize) -> Option<Queued> {
|
||||
self.modify_queue(|vq| vq.remove(index))
|
||||
}
|
||||
|
||||
/// Returns the number of tracks currently in the queue.
|
||||
#[must_use]
|
||||
pub fn len(&self) -> usize {
|
||||
let inner = self.inner.lock();
|
||||
|
||||
@@ -249,6 +281,7 @@ impl TrackQueue {
|
||||
}
|
||||
|
||||
/// Returns whether there are no tracks currently in the queue.
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
let inner = self.inner.lock();
|
||||
|
||||
@@ -296,7 +329,7 @@ impl TrackQueue {
|
||||
for track in inner.tracks.drain(..) {
|
||||
// Errors when removing tracks don't really make
|
||||
// a difference: an error just implies it's already gone.
|
||||
let _ = track.stop();
|
||||
drop(track.stop());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,10 +347,11 @@ impl TrackQueue {
|
||||
/// Use [`modify_queue`] for direct modification of the queue.
|
||||
///
|
||||
/// [`modify_queue`]: TrackQueue::modify_queue
|
||||
#[must_use]
|
||||
pub fn current_queue(&self) -> Vec<TrackHandle> {
|
||||
let inner = self.inner.lock();
|
||||
|
||||
inner.tracks.iter().map(|q| q.handle()).collect()
|
||||
inner.tracks.iter().map(Queued::handle).collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,3 +365,135 @@ impl TrackQueueCore {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "builtin-queue"))]
|
||||
mod tests {
|
||||
use crate::{
|
||||
driver::Driver,
|
||||
input::{File, HttpRequest},
|
||||
tracks::PlayMode,
|
||||
Config,
|
||||
};
|
||||
use reqwest::Client;
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test]
|
||||
#[ntest::timeout(20_000)]
|
||||
async fn next_track_plays_on_end() {
|
||||
let (t_handle, config) = Config::test_cfg(true);
|
||||
let mut driver = Driver::new(config.clone());
|
||||
|
||||
let file1 = File::new("resources/ting.wav");
|
||||
let file2 = file1.clone();
|
||||
|
||||
let h1 = driver.enqueue_input(file1.into()).await;
|
||||
let h2 = driver.enqueue_input(file2.into()).await;
|
||||
|
||||
// Get h1 in place, playing. Wait for IO to ready.
|
||||
// Fast wait here since it's all local I/O, no network.
|
||||
t_handle
|
||||
.ready_track(&h1, Some(Duration::from_millis(1)))
|
||||
.await;
|
||||
t_handle
|
||||
.ready_track(&h2, Some(Duration::from_millis(1)))
|
||||
.await;
|
||||
|
||||
// playout
|
||||
t_handle.tick(1);
|
||||
t_handle.wait(1);
|
||||
|
||||
let h1a = h1.get_info();
|
||||
let h2a = h2.get_info();
|
||||
|
||||
// allow get_info to fire for h2.
|
||||
t_handle.tick(2);
|
||||
|
||||
// post-conditions:
|
||||
// 1) track 1 is done & dropped (commands fail).
|
||||
// 2) track 2 is playing.
|
||||
assert!(h1a.await.is_err());
|
||||
assert_eq!(h2a.await.unwrap().playing, PlayMode::Play);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ntest::timeout(10_000)]
|
||||
async fn next_track_plays_on_skip() {
|
||||
let (t_handle, config) = Config::test_cfg(true);
|
||||
let mut driver = Driver::new(config.clone());
|
||||
|
||||
let file1 = File::new("resources/ting.wav");
|
||||
let file2 = file1.clone();
|
||||
|
||||
let h1 = driver.enqueue_input(file1.into()).await;
|
||||
let h2 = driver.enqueue_input(file2.into()).await;
|
||||
|
||||
// Get h1 in place, playing. Wait for IO to ready.
|
||||
// Fast wait here since it's all local I/O, no network.
|
||||
t_handle
|
||||
.ready_track(&h1, Some(Duration::from_millis(1)))
|
||||
.await;
|
||||
|
||||
assert!(driver.queue().skip().is_ok());
|
||||
|
||||
t_handle
|
||||
.ready_track(&h2, Some(Duration::from_millis(1)))
|
||||
.await;
|
||||
|
||||
// playout
|
||||
t_handle.skip(1).await;
|
||||
|
||||
let h1a = h1.get_info();
|
||||
let h2a = h2.get_info();
|
||||
|
||||
// allow get_info to fire for h2.
|
||||
t_handle.tick(2);
|
||||
|
||||
// post-conditions:
|
||||
// 1) track 1 is done & dropped (commands fail).
|
||||
// 2) track 2 is playing.
|
||||
assert!(h1a.await.is_err());
|
||||
assert_eq!(h2a.await.unwrap().playing, PlayMode::Play);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ntest::timeout(10_000)]
|
||||
async fn next_track_plays_on_err() {
|
||||
let (t_handle, config) = Config::test_cfg(true);
|
||||
let mut driver = Driver::new(config.clone());
|
||||
|
||||
// File 1 is HTML with no valid audio -- this will fail to play.
|
||||
let file1 = HttpRequest::new(
|
||||
Client::new(),
|
||||
"http://github.com/serenity-rs/songbird/".into(),
|
||||
);
|
||||
let file2 = File::new("resources/ting.wav");
|
||||
|
||||
let h1 = driver.enqueue_input(file1.into()).await;
|
||||
let h2 = driver.enqueue_input(file2.into()).await;
|
||||
|
||||
// Get h1 in place, playing. Wait for IO to ready.
|
||||
// Fast wait here since it's all local I/O, no network.
|
||||
// t_handle
|
||||
// .ready_track(&h1, Some(Duration::from_millis(1)))
|
||||
// .await;
|
||||
t_handle
|
||||
.ready_track(&h2, Some(Duration::from_millis(1)))
|
||||
.await;
|
||||
|
||||
// playout
|
||||
t_handle.tick(1);
|
||||
t_handle.wait(1);
|
||||
|
||||
let h1a = h1.get_info();
|
||||
let h2a = h2.get_info();
|
||||
|
||||
// allow get_info to fire for h2.
|
||||
t_handle.tick(2);
|
||||
|
||||
// post-conditions:
|
||||
// 1) track 1 is done & dropped (commands fail).
|
||||
// 2) track 2 is playing.
|
||||
assert!(h1a.await.is_err());
|
||||
assert_eq!(h2a.await.unwrap().playing, PlayMode::Play);
|
||||
}
|
||||
}
|
||||
|
||||
21
src/tracks/ready.rs
Normal file
21
src/tracks/ready.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
/// Whether this track has been made live, is being processed, or is
|
||||
/// currently uninitialised.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum ReadyState {
|
||||
/// This track is still a lazy [`Compose`] object, and hasn't been made playable.
|
||||
///
|
||||
/// [`Compose`]: crate::input::Compose
|
||||
Uninitialised,
|
||||
|
||||
/// The mixer is currently creating and parsing this track's bytestream.
|
||||
Preparing,
|
||||
|
||||
/// This track is fully initialised and usable.
|
||||
Playable,
|
||||
}
|
||||
|
||||
impl Default for ReadyState {
|
||||
fn default() -> Self {
|
||||
Self::Uninitialised
|
||||
}
|
||||
}
|
||||
@@ -5,21 +5,29 @@ use super::*;
|
||||
///
|
||||
/// [`Track`]: Track
|
||||
/// [`TrackHandle::get_info`]: TrackHandle::get_info
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq)]
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct TrackState {
|
||||
/// Play status (e.g., active, paused, stopped) of this track.
|
||||
pub playing: PlayMode,
|
||||
|
||||
/// Current volume of this track.
|
||||
pub volume: f32,
|
||||
|
||||
/// Current playback position in the source.
|
||||
///
|
||||
/// This is altered by loops and seeks, and represents this track's
|
||||
/// position in its underlying input stream.
|
||||
pub position: Duration,
|
||||
|
||||
/// Total playback time, increasing monotonically.
|
||||
pub play_time: Duration,
|
||||
|
||||
/// Remaining loops on this track.
|
||||
pub loops: LoopState,
|
||||
|
||||
/// Whether this track has been made live, is being processed, or is
|
||||
/// currently uninitialised.
|
||||
pub ready: ReadyState,
|
||||
}
|
||||
|
||||
impl TrackState {
|
||||
@@ -28,3 +36,35 @@ impl TrackState {
|
||||
self.play_time += TIMESTEP_LENGTH;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
constants::test_data::YTDL_TARGET,
|
||||
driver::Driver,
|
||||
input::YoutubeDl,
|
||||
tracks::Track,
|
||||
Config,
|
||||
};
|
||||
use reqwest::Client;
|
||||
|
||||
#[tokio::test]
|
||||
#[ntest::timeout(10_000)]
|
||||
async fn times_unchanged_while_not_ready() {
|
||||
let (t_handle, config) = Config::test_cfg(true);
|
||||
let mut driver = Driver::new(config.clone());
|
||||
|
||||
let file = YoutubeDl::new(Client::new(), YTDL_TARGET.into());
|
||||
let handle = driver.play(Track::from(file));
|
||||
|
||||
let state = t_handle
|
||||
.ready_track(&handle, Some(Duration::from_millis(5)))
|
||||
.await;
|
||||
|
||||
// As state is `play`, the instant we ready we'll have playout.
|
||||
// Naturally, fetching a ytdl request takes far longer than this.
|
||||
assert_eq!(state.position, Duration::from_millis(20));
|
||||
assert_eq!(state.play_time, Duration::from_millis(20));
|
||||
}
|
||||
}
|
||||
|
||||
31
src/tracks/view.rs
Normal file
31
src/tracks/view.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use super::*;
|
||||
use crate::input::Metadata;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Live track and input state exposed during [`TrackHandle::action`].
|
||||
///
|
||||
/// [`TrackHandle::action`]: super::[`TrackHandle::action`]
|
||||
#[non_exhaustive]
|
||||
pub struct View<'a> {
|
||||
/// The current position within this track.
|
||||
pub position: &'a Duration,
|
||||
|
||||
/// The total time a track has been played for.
|
||||
pub play_time: &'a Duration,
|
||||
|
||||
/// The current mixing volume of this track.
|
||||
pub volume: &'a mut f32,
|
||||
|
||||
/// In-stream metadata for this track, if it is fully readied.
|
||||
pub meta: Option<Metadata<'a>>,
|
||||
|
||||
/// The current play status of this track.
|
||||
pub playing: &'a mut PlayMode,
|
||||
|
||||
/// Whether this track has been made live, is being processed, or is
|
||||
/// currently uninitialised.
|
||||
pub ready: ReadyState,
|
||||
|
||||
/// The number of remaning loops on this track.
|
||||
pub loops: &'a mut LoopState,
|
||||
}
|
||||
Reference in New Issue
Block a user