Renderer.kt
73 lines · 3.3 KB
Changed in: 0.5.0
| 1 | package dev.stave.domain.renderer |
| 2 | |
| 3 | import dev.stave.domain.model.DidlItem |
| 4 | import kotlinx.coroutines.flow.StateFlow |
| 5 | import kotlin.time.Duration |
| 6 | |
| 7 | /** |
| 8 | * The thing that actually plays audio. Implemented by |
| 9 | * [dev.stave.playback.LocalRenderer] (ExoPlayer; also mirrors to a Cast |
| 10 | * session in the play flavour) and |
| 11 | * [dev.stave.upnp.renderer.RemoteUpnpRenderer]; selected by |
| 12 | * [dev.stave.upnp.renderer.RendererCoordinator]. |
| 13 | */ |
| 14 | interface Renderer { |
| 15 | val state: StateFlow<PlaybackState> |
| 16 | |
| 17 | /** |
| 18 | * Replace the queue with [tracks], position at [startIndex], and start playback. |
| 19 | * |
| 20 | * @param autoplay When true (default), playback begins immediately — the normal |
| 21 | * "user clicked track / album" flow. When false, the queue is set and the |
| 22 | * transport prepares but stays Stopped, waiting for an explicit [play] call. |
| 23 | * Used by the UPnP MediaRenderer's setAVTransportURI handler: the AVTransport |
| 24 | * spec says SetURI loads media into Stopped and the control point follows up |
| 25 | * with Play(), and we have to honor that ordering or buggy control points |
| 26 | * double-toggle and end up in their own Paused state out of sync with ours. |
| 27 | */ |
| 28 | suspend fun load(tracks: List<DidlItem.Track>, startIndex: Int = 0, autoplay: Boolean = true) |
| 29 | /** Append [tracks] to the end of the queue. If the queue is empty, they become the queue |
| 30 | * but are NOT auto-started — the user explicitly asked to queue, not to play. */ |
| 31 | suspend fun enqueue(tracks: List<DidlItem.Track>) |
| 32 | /** Insert [tracks] immediately after the currently-playing track. If the queue is empty, |
| 33 | * the tracks become the queue AND playback begins. */ |
| 34 | suspend fun playNext(tracks: List<DidlItem.Track>) |
| 35 | /** Jump directly to queue position [index] and start playing. Out-of-range is a no-op. */ |
| 36 | suspend fun skipToQueueItem(index: Int) |
| 37 | /** Remove queue item at [index]. Removing the current item advances to the next; removing |
| 38 | * the last remaining item stops playback. */ |
| 39 | suspend fun removeFromQueue(index: Int) |
| 40 | /** Reorder the queue by moving the item at [from] to position [to]. */ |
| 41 | suspend fun moveInQueue(from: Int, to: Int) |
| 42 | /** Empty the queue and stop playback. */ |
| 43 | suspend fun clearQueue() |
| 44 | suspend fun play() |
| 45 | suspend fun pause() |
| 46 | suspend fun stop() |
| 47 | suspend fun seekTo(position: Duration) |
| 48 | suspend fun next() |
| 49 | suspend fun previous() |
| 50 | suspend fun setVolume(volumePercent: Int) |
| 51 | /** Cycle repeat mode: OFF → ALL → ONE → OFF. */ |
| 52 | suspend fun setRepeatMode(mode: RepeatMode) |
| 53 | /** Shuffle toggle. Turning on saves the current queue order; turning off restores it. |
| 54 | * Implementation is responsible for the save/restore — UI just flips the flag. */ |
| 55 | suspend fun setShuffle(enabled: Boolean) |
| 56 | } |
| 57 | |
| 58 | enum class RepeatMode { OFF, ALL, ONE } |
| 59 | |
| 60 | data class PlaybackState( |
| 61 | val current: DidlItem.Track? = null, |
| 62 | val queue: List<DidlItem.Track> = emptyList(), |
| 63 | val queueIndex: Int = -1, |
| 64 | val isPlaying: Boolean = false, |
| 65 | val isBuffering: Boolean = false, |
| 66 | val isStopped: Boolean = true, |
| 67 | val position: Duration = Duration.ZERO, |
| 68 | val duration: Duration = Duration.ZERO, |
| 69 | val volumePercent: Int = 100, |
| 70 | val repeatMode: RepeatMode = RepeatMode.OFF, |
| 71 | val shuffleEnabled: Boolean = false, |
| 72 | val error: String? = null, |
| 73 | ) |


