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

UpnpServiceHolder.kt

package dev.stave.upnp

import dev.stave.domain.model.MediaServer
import dev.stave.domain.model.RemoteRenderer
import kotlinx.coroutines.flow.StateFlow
import org.jupnp.controlpoint.ControlPoint
import org.jupnp.model.meta.LocalDevice
import org.jupnp.model.meta.RemoteDevice

/**
 * Platform-agnostic UPnP service surface.
 *
 * Android and Desktop holders differ in *how* they start jUPnP:
 *  - Android binds to [org.jupnp.android.AndroidUpnpServiceImpl] through a bound Service and
 *    holds a WifiManager MulticastLock while the app is foregrounded.
 *  - Desktop constructs a plain [org.jupnp.UpnpServiceImpl] directly and doesn't need any
 *    multicast lock — stock Linux networking handles SSDP fine.
 *
 * Everything downstream of this (ContentDirectoryClient, MediaCollector, viewmodels)
 * depends on this interface, not the platform-specific class.
 */
interface UpnpServiceHolder {

    /** UPnP MediaServer devices discovered on the LAN — what we BROWSE from. */
    val servers: StateFlow<List<MediaServer>>

    /** UPnP MediaRenderer devices discovered on the LAN — what we can DRIVE to.
     *  Other Earhorn instances with broadcast on, plus third-party renderers.
     *  Used by the (future) renderer-picker UI. */
    val renderers: StateFlow<List<RemoteRenderer>>

    val controlPoint: ControlPoint?

    fun findDevice(udn: String): RemoteDevice?

    /** Start the jUPnP stack and begin SSDP discovery. Idempotent. */
    fun start()

    /** Shut the jUPnP stack down cleanly. Sends BYEBYE messages. */
    fun stop()

    /** Force an SSDP M-SEARCH. Called from pull-to-refresh / "Rescan" / window focus. */
    fun rescan()

    /** Register a [LocalDevice] with the registry so this machine advertises itself
     *  on the LAN. Used by [dev.stave.upnp.renderer.EarhornRendererService] when the
     *  user toggles "act as renderer" on. Returns true on success; false if the stack
     *  isn't up or registration failed. Calling with an already-registered device
     *  (same UDN) is a no-op. */
    fun registerLocalDevice(device: LocalDevice): Boolean

    /** Remove a previously-registered [LocalDevice]. Sends ssdp:byebye so remote
     *  control points know we've gone away. No-op if the device isn't registered. */
    fun unregisterLocalDevice(device: LocalDevice)
}