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
| State | Meaning |
|---|---|
Idle | No source has been started. |
Starting | The worker is opening the source and decoder. |
Playing | The worker may decode and deliver frames. |
Pausing | Pause was requested and is waiting for the current operation to finish. |
Resuming | The output is resuming before frame delivery continues. |
Paused | The worker waits on a condition variable and does not decode ahead. |
Stopping | Cancellation is in progress and the worker is being joined. |
Stopped | Active work was cancelled and its worker joined. |
Completed | Natural end-of-stream and output finish both succeeded. |
Failed | A 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.mp3The 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.