CastObserverApi.kt
79 lines · 3.7 KB
Changed in: 0.5.0
| 1 | package dev.stave.cast.raw |
| 2 | |
| 3 | import dev.stave.domain.renderer.PlaybackState |
| 4 | import kotlinx.coroutines.flow.StateFlow |
| 5 | import kotlin.time.Duration |
| 6 | |
| 7 | /** |
| 8 | * Platform-agnostic surface for raw-cast observer mode — the "follow a |
| 9 | * Chromecast group" capability that lets the desktop UI render what some other |
| 10 | * sender (typically the phone) is playing on a CCA group. |
| 11 | * |
| 12 | * Desktop and Android both have concrete implementations: |
| 13 | * - Desktop: `dev.stave.cast.raw.RawCastObserver` (in `desktopMain`), |
| 14 | * delegates discovery to `CastIntegration`. |
| 15 | * - Android: the Hilt-injected `RawCastObserver` (in `androidMain`), uses |
| 16 | * `NsdManager` for discovery. |
| 17 | * |
| 18 | * This interface is what `NowPlayingViewModel` depends on, so the VM stays in |
| 19 | * `jvmCommonMain` and works on both targets without dragging the platform- |
| 20 | * specific discovery wiring up to the shared layer. Surfaces tied to a |
| 21 | * platform-specific device type (like the picker's `observe(device)` call) are |
| 22 | * NOT on the interface — those live in the platform-specific UI layer that |
| 23 | * already has the concrete observer in hand. |
| 24 | */ |
| 25 | interface CastObserverApi { |
| 26 | /** True while connected to a Cast device in observer mode. UI's `tvTrack` |
| 27 | * / `tvState` resolution flips on this flag. */ |
| 28 | val observing: StateFlow<Boolean> |
| 29 | |
| 30 | /** Domain [PlaybackState] mapped from the observed `MEDIA_STATUS` stream. |
| 31 | * Shape is identical to `Renderer.state` — UI surfaces can render either |
| 32 | * one without branching on which kind of source it is. */ |
| 33 | val playbackState: StateFlow<PlaybackState> |
| 34 | |
| 35 | /** Friendly name of the device being followed, or null. Picker uses this to |
| 36 | * show the active follow target alongside the device list. */ |
| 37 | val connectedDeviceName: StateFlow<String?> |
| 38 | |
| 39 | /** Positive evidence the followed session is alive: the receiver reports |
| 40 | * a media session that is not IDLE, its position isn't frozen past the |
| 41 | * zombie latch, and statuses haven't gone silent. Raw, NOT debounced — |
| 42 | * it drops to false for under a second at every track boundary, so |
| 43 | * consumers that paint from it must wait before believing "dead" |
| 44 | * (Android parity, 2026-09-03: 20 s). False when not observing. */ |
| 45 | val sessionLive: StateFlow<Boolean> |
| 46 | |
| 47 | suspend fun play() |
| 48 | suspend fun pause() |
| 49 | suspend fun stop() |
| 50 | suspend fun next() |
| 51 | suspend fun previous() |
| 52 | suspend fun seek(position: Duration) |
| 53 | suspend fun setVolume(percent: Int) |
| 54 | /** Repeat / shuffle of the OBSERVED session (2026-09-04, the maintainer's |
| 55 | * ruling: while following, these act on the group, like every other |
| 56 | * transport control). */ |
| 57 | suspend fun setRepeatMode(mode: dev.stave.domain.renderer.RepeatMode) |
| 58 | suspend fun setShuffle(on: Boolean) |
| 59 | |
| 60 | /** |
| 61 | * Jump the observed session to the queue row at [index] (an index into |
| 62 | * [playbackState].queue). Position-only — QUEUE_UPDATE with currentItemId |
| 63 | * never alters queue contents, so it's safe for a follower to send: the |
| 64 | * driving sender's index tracker follows along via its own status |
| 65 | * callbacks. This is the ONLY queue operation a follower may perform; |
| 66 | * editing (remove/reorder/insert) is deliberately prohibited because the |
| 67 | * sender's local queue is authoritative and would silently clobber (and |
| 68 | * desync from) any follower-side edit on its next reload. |
| 69 | */ |
| 70 | suspend fun jumpToQueueItem(index: Int) |
| 71 | |
| 72 | /** Force a status refresh. Implementations may also do this on a timer; this |
| 73 | * is the manual trigger for surfaces that want fresh data right now (e.g. |
| 74 | * after the window regains focus). */ |
| 75 | suspend fun requestStatusRefresh() |
| 76 | |
| 77 | /** Stop observing. Idempotent. */ |
| 78 | fun disconnect() |
| 79 | } |


