kithara-queue
AVQueuePlayer-analogue orchestration layer on top of kithara-play. Owns
the queue (ordered tracks), an async track loader with a configurable
parallelism cap, navigation (shuffle / repeat / history), and
crossfade-aware track selection. Replaces the bespoke queue / controller
code previously duplicated across kithara-app and future iOS / Android
SDK surfaces.
Overview
[Queue] composes an Arc<PlayerImpl> (from kithara-play) with:
- an ordered
Vec<TrackEntry>indexed by stable [TrackId]s, - an async [
Loader] (internal) that caps in-flightResource::newcalls via atokio::sync::Semaphore, - [
NavigationState] for shuffle / repeat / history, - a
pending_selectslot soQueue::select(id)can be called before the track has finished loading.
[Queue] emits [QueueEvent] on the shared EventBus from
kithara-events, so subscribers receive queue-level signals and the
underlying player / audio / hls / file events through a single stream.
Public API
- [
Queue::new(QueueConfig)] - CRUD:
append,insert(source, after),remove,clear,set_tracks - Query:
tracks,track(id),current,current_index,len,is_empty - Navigation:
select(id),advance_to_next,return_to_previous,set_shuffle/is_shuffle_enabled,set_repeat/repeat_mode,seek(seconds) - Delegated to
PlayerImpl:play,pause,is_playing,crossfade_duration/set_crossfade_duration,default_rate/set_default_rate,volume/set_volume,is_muted/set_muted,eq_band_count,eq_gain,set_eq_gain,reset_eq,position_seconds,duration_seconds - Lifecycle:
tick()— call from the host loop to drivePlayerImpl::tickand drain engine events intoQueueEvents.
[TrackSource] is the input to append / insert / set_tracks. It
has two shapes:
TrackSource::Uri(String)— the [Queue] builds a defaultResourceConfigfrom theQueueConfignet/storetemplates.TrackSource::Config(Box<ResourceConfig>)— the caller provides a pre-builtResourceConfig(useful for DRM keys, custom headers, format hints). [Queue] leaves caller-set fields intact.
From<&str>, From<String>, From<ResourceConfig>, and
From<Box<ResourceConfig>> are implemented.
Event Flow
[Queue::subscribe] returns an EventReceiver that sees everything
published on the underlying [EventBus]: Event::Queue(QueueEvent::..)
plus Event::Player(..), Event::Audio(..), Event::Hls(..), and
Event::File(..).
[QueueEvent] variants:
TrackAdded { id, index }TrackRemoved { id }TrackStatusChanged { id, status }—Pending→Loading/Slow→Loaded→Consumed(after the engine takes theResourceduringselect_item) →Failed(reason)on errorCurrentTrackChanged { id }— forwarded fromPlayerEvent::CurrentItemChangedQueueEnded— emitted byadvance_to_nextwhen navigation returnsNoneand [RepeatMode::Off] is activeCrossfadeStarted { duration_seconds }— emitted when the engine is about to fade from a playing track to the newly-selected oneCrossfadeDurationChanged { seconds }
Auto-Advance Contract
Queue is the sole auto-advance orchestrator: Queue::new calls
PlayerImpl::set_auto_advance_enabled(false) to disable the player's
built-in linear handler, then drives transitions from
PlayerEvent::PrefetchRequested / HandoverRequested:
- on
PrefetchRequested: resolve the next index viaNavigationState::peek_next(honouring shuffle / repeat); if the resolved entry isTrackStatus::Loaded, callarm_next(idx). Tracks that are still loading are picked up via theTrackStatusChanged { Loaded }retry path. - on
HandoverRequested(cf>0 only): callcommit_next(idx), advance navigation, mark the just-promoted trackConsumed, publishQueueEvent::CrossfadeStarted. - on
ItemDidPlayToEnd: the audio thread already advanced (cf=0 arena handover) or the queue did (cf>0 commit).sync_navigation_after_handoverbringsNavigationState::current_indexin line with the player and emitsQueueEvent::QueueEndedif no further track is reachable.
set_repeat, set_shuffle, Queue::remove, and Queue::clear call
PlayerImpl::unarm_next so a stale arm cannot survive a navigation /
queue mutation. The previous Queue::tick-based polling
(maybe_arm_crossfade, should_arm_crossfade) is removed; tick now
only ticks the player and drains events.
Loading Lifecycle
Each append allocates a monotonic [TrackId] and a queue entry with
status Pending, then spawns a background task:
- Acquire a semaphore permit (up to
QueueConfig::max_concurrent_loads). - Publish
TrackStatusChanged { Loading }. - Build the
ResourceConfig(either fromTrackSource::Uritemplates or the caller-suppliedConfig) and callResource::new. - Spawn a LoadSlow listener on the config's
EventBus— ifFileEvent::LoadSloworHlsEvent::LoadSlowfires,TrackStatusChanged { Slow }is published before completion. - On success:
PlayerImpl::replace_item(index, resource),TrackStatusChanged { Loaded }. - If the loaded track was stashed in
pending_select(aselect(id)arrived before loading finished), callselect_item(index, true). Otherwise the track staysLoadedand does nothing until the caller explicitly selects it.
After select_item succeeds the engine has consumed items[index], so
the Queue immediately transitions the entry to TrackStatus::Consumed.
Re-selecting a Consumed track respawns the load.
The Queue never starts playback on its own: there is no autoplay. The
caller drives the first select / play explicitly so playback order
is deterministic and independent of which load finishes first.
Minimal Usage
use Arc;
use ;
async
Queue::set_tracks must run inside an active tokio runtime because the
loader uses tokio::spawn.
Migration From kithara-app
The previous kithara-app::{playlist, controls} combination collapses
into a single [Queue]:
Old (kithara-app) |
New (kithara-queue) |
|---|---|
Arc<PlayerImpl> + Arc<Playlist> + AppController + TrackLoadParams |
Arc<Queue> |
AppController::load_params.load_and_apply |
queue.set_tracks(sources) / queue.append(src) |
playlist.track_name(i) |
queue.tracks()[i].name |
playlist.track_status(i) |
queue.tracks()[i].status |
playlist.get_next_track() + switch |
queue.advance_to_next() |
playlist.get_prev_track() + switch |
queue.return_to_previous() |
player.seek_seconds(s) |
queue.seek(s) |
player.select_item(idx, true) |
queue.select(id) |
player.tick() |
queue.tick() |
DRM stays in the caller. kithara-queue is DRM-agnostic; apps that
need zvuk DRM keys build a ResourceConfig::new(url).with_keys(..) and
pass it via TrackSource::Config(Box::new(cfg)). See
kithara-app::sources::build_source for a reference implementation.