EARHORNMUSIC UPnP · Android TV · Phones · Android Auto · GNU/Linux
Release 0.5.0

Renderer.kt

package dev.stave.domain.renderer

import dev.stave.domain.model.DidlItem
import kotlinx.coroutines.flow.StateFlow
import kotlin.time.Duration

/**
 * The thing that actually plays audio. v0.0.1 ships [dev.stave.playback.LocalRenderer] only.
 * Cast (play flavor) and UPnP AV/OpenHome plug in behind this interface without UI changes.
 */
interface Renderer {
    val state: StateFlow<PlaybackState>

    suspend fun load(tracks: List<DidlItem.Track>, startIndex: Int = 0, autoplay: Boolean = true)
    /** Append [tracks] to the end of the queue. If the queue is empty, they become the queue
     *  but are NOT auto-started — the user explicitly asked to queue, not to play. */
    suspend fun enqueue(tracks: List<DidlItem.Track>)
    /** Insert [tracks] immediately after the currently-playing track. If the queue is empty,
     *  the tracks become the queue AND playback begins. */
    suspend fun playNext(tracks: List<DidlItem.Track>)
    /** Jump directly to queue position [index] and start playing. Out-of-range is a no-op. */
    suspend fun skipToQueueItem(index: Int)
    /** Remove queue item at [index]. Removing the current item advances to the next; removing
     *  the last remaining item stops playback. */
    suspend fun removeFromQueue(index: Int)
    /** Reorder the queue by moving the item at [from] to position [to]. */
    suspend fun moveInQueue(from: Int, to: Int)
    /** Empty the queue and stop playback. */
    suspend fun clearQueue()
    suspend fun play()
    suspend fun pause()
    suspend fun stop()
    suspend fun seekTo(position: Duration)
    suspend fun next()
    suspend fun previous()
    suspend fun setVolume(volumePercent: Int)
    /** Cycle repeat mode: OFF → ALL → ONE → OFF. */
    suspend fun setRepeatMode(mode: RepeatMode)
    /** Shuffle toggle. Turning on saves the current queue order; turning off restores it.
     *  Implementation is responsible for the save/restore — UI just flips the flag. */
    suspend fun setShuffle(enabled: Boolean)
}

enum class RepeatMode { OFF, ALL, ONE }

data class PlaybackState(
    val current: DidlItem.Track? = null,
    val queue: List<DidlItem.Track> = emptyList(),
    val queueIndex: Int = -1,
    val isPlaying: Boolean = false,
    val isBuffering: Boolean = false,
    /** True when the user has explicitly stopped (or nothing has ever been loaded).
     *  Distinguishes from paused-mid-track. Used by the NowPlayingBar visibility rule:
     *  the bar shows while playing OR paused, but hides while stopped even if the queue
     *  still has tracks loaded. */
    val isStopped: Boolean = true,
    val position: Duration = Duration.ZERO,
    val duration: Duration = Duration.ZERO,
    val volumePercent: Int = 100,
    val repeatMode: RepeatMode = RepeatMode.OFF,
    val shuffleEnabled: Boolean = false,
    val error: String? = null,
)