Playback Engine service specification
Playback Engine service specification
Responsibility
Accept audio sources as URL or file path and play them. Source-agnostic — no awareness of plugins, rooms, or content types. Manage PlaybackHandle. Emit playback state events. Auto-advance queue.
Public API
impl PlaybackEngine {
/// Play a resolved source (search result, queue item).
pub async fn play(&self, source: PlaySource) -> Result<(), PlaybackError>;
/// Pause current playback.
pub fn pause(&self) -> Result<()>;
/// Resume from paused state.
pub fn resume(&self) -> Result<()>;
/// Seek to a position in seconds from start.
pub fn seek(&self, position_secs: f64) -> Result<()>;
/// Set volume (0.0–1.0).
pub fn set_volume(&self, level: f32) -> Result<()>;
/// Stop playback and reset to idle.
pub fn stop(&self) -> Result<()>;
/// Skip to next track in the working queue.
pub fn advance(&self) -> Result<()>;
/// Sync the engine's working queue with the frontend's authoritative queue.
pub fn sync_queue(&self, items: Vec<QueueItem>, current_index: usize) -> Result<()>;
/// Get current playback state.
pub fn state(&self) -> PlaybackState;
}
PlaySource
pub struct PlaySource {
pub plugin_id: String,
pub content_id: String,
pub metadata: TrackStub, // passed at invoke time so engine can emit track_changed
// without a plugin round-trip
}
PlaybackState
pub struct PlaybackState {
pub status: PlaybackStatus,
pub position_ms: u64,
pub duration_ms: Option<u64>,
pub volume: f32, // 0.0–1.0
pub track: Option<TrackStub>,
}
pub enum PlaybackStatus {
Idle,
Loading,
Playing,
Paused,
Seeking,
Error,
}
Stream URL handling
- Engine receives
PlaySourcefrom frontend - Engine calls Plugin Runtime’s
stream(plugin_id, content_id)internally - Plugin Runtime returns
StreamInfowith URL, content type, optional expiry - Engine checks
expires_at— if expired, re-resolve viastream()before fetching - Engine fetches URL via
reqwest(streaming, supports redirects, no auth headers) - On fetch failure (404, DNS, timeout): emit
playback_error, auto-advance queue
Do not retry the same URL on fetch failure. A 404 won’t become a 200 on retry. Expired signed URLs need re-resolution via stream().
Queue integration
The engine holds a working copy of the queue. The frontend is the authoritative source.
- On
sync_queue: store the item list and current index - On
advanceor track end: increment index, callplay()on the next item - When working copy runs low (less than 5 items remaining): request next batch from frontend (via Tauri event)
- Engine restart without app restart: frontend re-pushes queue via
sync_queue
Phonic integration
The engine wraps phonic::Player:
use phonic::{DefaultOutputDevice, Player, FilePlaybackOptions, PlaybackStatusEvent};
let (status_tx, status_rx) = sync_channel(32);
let mut player = Player::new(DefaultOutputDevice::open()?, Some(status_tx));
// Play a stream URL
let handle = player.play_file(url, FilePlaybackOptions::default().streamed())?;
// Monitor playback position and advance events
while let Ok(event) = status_rx.recv() {
match event {
PlaybackStatusEvent::Position { id, position, .. } => {
emit("playback_state_changed", { status: "playing", position_ms: position.as_millis(), ... });
}
PlaybackStatusEvent::Stopped { id, exhausted, .. } => {
if exhausted { self.advance(); }
}
}
}
References
- ADR-004 (phonic audio backend)
- ADR-010 (queue management)
- FR-CORE-1 (source-agnostic playback)
- FR-CORE-2 (queue management)