Skip to content
Rymflux Documentation
Esc
navigateopen⌘Jpreview
On this page

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

  1. Engine receives PlaySource from frontend
  2. Engine calls Plugin Runtime’s stream(plugin_id, content_id) internally
  3. Plugin Runtime returns StreamInfo with URL, content type, optional expiry
  4. Engine checks expires_at — if expired, re-resolve via stream() before fetching
  5. Engine fetches URL via reqwest (streaming, supports redirects, no auth headers)
  6. 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 advance or track end: increment index, call play() 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)

Was this page helpful?