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

CastObserverApi.kt

79 lines · 3.7 KB

Changed in: 0.5.0

1package dev.stave.cast.raw
2
3import dev.stave.domain.renderer.PlaybackState
4import kotlinx.coroutines.flow.StateFlow
5import 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 */
25interface 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}