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