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)
}