Remedia

Playback sessions

Manage playback state, output delivery, position, and worker cleanup.

A Rust PlaybackSession owns a source, an output, and a blocking playback worker. Use it to coordinate decoding, pause/resume, seeking, and cleanup. Output adapters consume its PCM or Opus packets.

Node.js integrations should start with the binding reference. Contributor measurements and worker limits are in Performance.

Start and observe playback

Choose an explicit file or HTTP(S) source and move an output into the session. play() blocks until FFmpeg has opened the input and initialized the audio decoder. It returns the selected stream metadata only after that succeeds. Decoding and frame delivery then run on the session worker.

play_with_cancellation(source, output, token) accepts a caller-owned CancellationToken. A token cancelled before the worker starts prevents the source from opening. After startup it interrupts decoder I/O. Call stop() as well to join the worker and release its output. Ordinary callers can use play(), which creates its own token and is cancelled by stop().

use remedia_core::{InputSource, NullOutput, PlaybackSession, PlaybackState};

let session = PlaybackSession::new();
let output = NullOutput::new();
let stats = output.stats_handle();
let info = session.play(InputSource::file("media/track.wav"), output)?;

println!("{}; duration: {:?}", info.codec, session.duration());
println!("state: {}", session.state());
let terminal_state = session.wait();
assert_eq!(terminal_state, PlaybackState::Completed);

let position = session.position();
println!("media: {:?}; submitted: {:?}; consumed: {:?}", position.media, position.submitted, position.played);
println!("output statistics: {:?}", stats.snapshot());
session.stop()?; // joins and releases the finished worker/output
# Ok::<(), Box<dyn std::error::Error>>(())

The null output consumes frames as fast as decoding permits. It is useful for tests, examples, and benchmarks. It does not pace playback in real time. See crates/remedia-core/examples/playback.rs for a runnable example that uses an example-only paced null output so pause and seek are observable with short files.

States and operations

StateMeaning
IdleNo source has been started.
StartingThe worker is opening the source and decoder.
PlayingThe worker may decode and deliver frames.
PausingPause was requested and is waiting for the current operation to finish.
ResumingThe output is resuming before frame delivery continues.
PausedThe worker waits on a condition variable and does not decode ahead.
StoppingCancellation is in progress and the worker is being joined.
StoppedActive work was cancelled and its worker joined.
CompletedNatural end-of-stream and output finish both succeeded.
FailedA decoder, output, or worker error stopped playback.

pause() waits until the worker reaches a frame boundary. It can wait for the current native input read or output write to return. resume() wakes the worker. Repeating pause while paused or resume while playing is harmless. stop() cancels network reads and output work, clears pending seek requests, and joins the worker. Stopping an idle session changes it to Stopped. Stopping a completed or failed session releases retained output resources while preserving its terminal state.

A session holds only one active source. A second play() call while it is starting, playing, pausing, paused, or stopping returns PlaybackError::InvalidState. To replace a source, call stop() and then play() with the new source and a new output value. Applications own playlists. Opted-in sessions can promote a staged next source through crossfade playback.

PlaybackSession::wait() blocks the calling thread until the current source reaches a terminal state. Async adapters should capture PlaybackSession::completion() before dispatching that wait to a blocking worker. Its PlaybackWaiter::wait() remains bound to that source and returns Stopped if a replacement starts first. Adapters that cross an async scheduling boundary can capture completion_generation() synchronously and later create the corresponding waiter with completion_for_generation(generation). The Node wrapper does this internally so an old completion cannot attach to a new track.

Seeking and position

seek(Duration) is supported while playing or paused. For files, buffers, and seekable HTTP inputs, the decoder asks FFmpeg's demuxer to seek, flushes codec/resampler state, and discards samples before the target. The session flushes pre-seek output before resuming delivery. HTTP seeking depends on the server and container, and a known source duration is checked before the request.

For one-shot InputSource::byte_stream, seeking is forward only. The decoder reads and discards audio up to the requested media timestamp, retaining only its bounded input queue and decoder buffers. It never rewinds the producer or estimates byte offsets from bitrate. Large jumps require consuming the intervening encoded data and can take time or incur CPU/network work. Backward requests return PlaybackError::UnsupportedSeeking without flushing output or stopping the session. The earliest decodable sample can be slightly ahead of the last submitted position because FFmpeg and the playback worker read ahead. A seek past EOF returns SeekOutOfRange. A stalled producer keeps the seek waiting until data arrives or the operation is cancelled.

Structured diagnostics

PlaybackSession::with_observability(handle) attaches a session to the shared local tracing dispatcher and metrics provider. Each playback start receives its own internal playback_id. Events include lifecycle transitions and failures without recording source paths or URLs. See the observability guide for subscriber ownership, event routing, environment configuration, and OTLP export.

Errors

Startup errors are returned from play(). Errors after startup set the state to Failed and remain available from last_error(). PlaybackError distinguishes decoder failures, output failures, invalid state operations, worker startup or panic, cancellation, seek range errors, and unsupported seeking. The original decoder or output error remains available as the error source where possible.

Runnable example

From the repository root, run:

cargo run -p remedia-core --example playback -- file ./media/track.wav
cargo run -p remedia-core --example playback -- url https://example.com/audio.mp3

The example prints metadata, state, submitted/played position, and null-output statistics. It requests pause, attempts a midpoint seek, resumes, observes completion, and stops the session. It reports unsupported seeking for sources that cannot seek.

Detailed references

On this page