### UPnPRegistry.shared.startDiscovery(_:) Source: https://context7.com/katoemba/swiftupnp/llms.txt Starts the discovery of UPnP devices on the local network. It sends SSDP search requests, starts an HTTP server for event callbacks, and populates the device registry. Devices can be filtered by providing an array of device type URNs. ```APIDOC ## `UPnPRegistry.shared.startDiscovery(_:)` - Start SSDP device discovery ### Description This method initiates the process of discovering UPnP devices on the network. It configures the `UPnPRegistry` to listen for device announcements and to receive event notifications. You can optionally specify which types of devices to discover by providing an array of URNs. ### Method ```swift try? registry.startDiscovery([ "urn:schemas-upnp-org:device:MediaServer:1", "urn:av-openhome-org:device:Source:1" ]) ``` ### Parameters * **deviceURNs** (Array?) - Optional - An array of URN strings specifying the types of devices to discover. If nil or empty, default device types are discovered. ``` -------------------------------- ### Start SSDP Device Discovery with Combine Source: https://context7.com/katoemba/swiftupnp/llms.txt Initiates SSDP search requests and starts an HTTP server for event callbacks. Subscribe to `deviceAdded` and `deviceRemoved` Combine publishers to receive notifications about discovered devices. Discovery can be narrowed by passing specific device type URNs. ```swift import SwiftUPnP import Combine class DeviceScanner { private let registry = UPnPRegistry.shared private var cancellables = Set() func start() { // Subscribe via Combine registry.deviceAdded .sink { device in print("Added: \(device.deviceDefinition?.device.friendlyName ?? device.uuid) [\(device.deviceType)]") } .store(in: &cancellables) registry.deviceRemoved .sink { device in print("Removed: \(device.uuid)") } .store(in: &cancellables) // Default discovers: MediaServer:1, linn Source:1, av-openhome Source:1 // Pass custom URNs to narrow discovery: try? registry.startDiscovery([ "urn:schemas-upnp-org:device:MediaServer:1", "urn:av-openhome-org:device:Source:1" ]) } func stop() { registry.stopDiscovery() // clears device list and stops HTTP server } } ``` -------------------------------- ### Observe UPnP Device Additions and Removals with Combine Source: https://github.com/katoemba/swiftupnp/blob/main/README.md Use Combine publishers to react to devices being added or removed from the network. This setup requires storing cancellables to manage subscriptions. ```swift private let openHomeRegistry = UPnPRegistry.shared private var cancellables = Set() public init() { openHomeRegistry.deviceAdded .sink { print("Detected device \($0.deviceDefinition.device.friendlyName) of type \($0.deviceType)") } .store(in: &cancellables) openHomeRegistry.deviceRemoved .sink { print("Removed device \($0.deviceDefinition.device.friendlyName) of type \($0.deviceType)") } .store(in: &cancellables) } public func startListening() { // A list of device types can be passed to discover. The default devices types used when nothing is specified: // urn:schemas-upnp-org:device:MediaServer:1 // urn:linn-co-uk:device:Source:1 // urn:av-openhome-org:device:Source:1 try? openHomeRegistry.startDiscovery() } public func stopListening() { openHomeRegistry.stopDiscovery() } ``` -------------------------------- ### Control Volume with OpenHomeVolume1Service Source: https://context7.com/katoemba/swiftupnp/llms.txt Use this service to get device capabilities, read current volume, set absolute volume, increment/decrement volume, mute/unmute, set balance, set fade, and check volume limits. Ensure the device supports the OpenHomeVolume1Service. ```swift import SwiftUPnP func controlVolume(device: UPnPDevice) async { guard let vol = await device.openHomeVolume1Service else { return } do { // Get device capabilities let caps = try await vol.characteristics() print("Max volume: \(caps.volumeMax), Unity: \(caps.volumeUnity), Steps: \(caps.volumeSteps)") // e.g. Max volume: 100, Unity: 80, Steps: 100 // Read current volume let current = try await vol.volume() print("Current volume: \(current.value)") // e.g. 45 // Set absolute volume try await vol.setVolume(value: 50) // Increment / decrement by one step try await vol.volumeInc() try await vol.volumeDec() // Mute try await vol.setMute(value: true) let muteState = try await vol.mute() print("Muted: \(muteState.value)") // true // Balance (-balanceMax ... +balanceMax) try await vol.setBalance(value: 0) // Fade try await vol.setFade(value: 0) // Check enforced limit let limit = try await vol.volumeLimit() print("Volume limit: \(limit.value)") // e.g. 100 } catch { print("Volume action failed: \(error)") } } ``` -------------------------------- ### OpenHomeProduct1Service Management Source: https://context7.com/katoemba/swiftupnp/llms.txt This function demonstrates how to query device identity, manage standby mode, list audio sources, switch between sources, and subscribe to product state changes using the OpenHomeProduct1Service. ```APIDOC ## OpenHomeProduct1Service ### Description Queries device identity (manufacturer, model, room/name) and manages audio source selection. Standby control is also exposed here. ### Methods - `manufacturer()`: Retrieves the device manufacturer information. - `model()`: Retrieves the device model information. - `product()`: Retrieves the device product name and room assignment. - `standby()`: Gets the current standby status of the device. - `setStandby(value: Bool)`: Sets the standby status of the device. - `sourceCount()`: Gets the total number of available audio sources. - `source(index: Int)`: Retrieves information about an audio source at a specific index. - `setSourceIndex(value: Int)`: Sets the active audio source by its index. - `setSourceIndexByName(value: String)`: Sets the active audio source by its name. - `sourceIndex()`: Gets the index of the currently active audio source. - `subscribeToEvents()`: Subscribes to state change events for the product service. ### State Changes - `stateChangeStream`: A stream that emits updates on product state changes, including standby status, source index, product room, and product name. ``` -------------------------------- ### Manage OpenHome Playlist with SwiftUPnP Source: https://context7.com/katoemba/swiftupnp/llms.txt Control playback, insert/delete tracks, seek, and subscribe to playlist state changes. Requires importing SwiftUPnP and Combine. ```swift import SwiftUPnP import Combine func managePlaylist(device: UPnPDevice) async { guard let playlist = await device.openHomePlaylist1Service else { return } do { // Transport control try await playlist.play() try await playlist.pause() try await playlist.stop() try await playlist.next() try await playlist.previous() // Playback mode try await playlist.setRepeat(value: true) try await playlist.setShuffle(value: false) let shuffleState = try await playlist.shuffle() print("Shuffle: \(shuffleState.value)") // false // Get current track id let idResp = try await playlist.id() print("Current track id: \(idResp.value)") // e.g. 7 // Get all ids in playlist order (binary array) let idArray = try await playlist.idArray() print("Token: \(idArray.token), Track count: \(idArray.array?.count ?? 0)") // Read a specific track by id let track = try await playlist.read(id: 7) print("URI: \(track.uri)") print("Metadata: \(track.metadata)") // Insert a track after position 0 (beginning) let inserted = try await playlist.insert( afterId: 0, uri: "http://192.168.1.10:8200/MediaItems/99.flac", metadata: "..." ) print("New track id: \(inserted.newId)") // Delete a track try await playlist.deleteId(value: inserted.newId) // Seek to a specific track id try await playlist.seekId(value: 7) // Seek by index (0-based position in playlist) try await playlist.seekIndex(value: 0) // Seek to absolute second within current track try await playlist.seekSecondAbsolute(value: 90) // Subscribe to playlist state changes var cancellables = Set() playlist.stateSubject .sink { state in print("State: \(state.transportState?.rawValue ?? \"-\")") print("Repeat: \(state.repeat ?? false), Shuffle: \(state.shuffle ?? false)") print("Track count: \(state.idArray?.count ?? 0)") } .store(in: &cancellables) await playlist.subscribeToEvents() } catch { print("Playlist action failed: \(error)") } } ``` -------------------------------- ### Manage UPnP Service Subscriptions in Swift Source: https://context7.com/katoemba/swiftupnp/llms.txt Demonstrates how to adjust the default subscription timeout, subscribe to events, check subscription status, and unsubscribe from UPnP services. Includes optional SOAP message logging for debugging. ```swift import SwiftUPnP // Increase subscription lifetime globally UPnPService.defaultSubscriptionTimeout = 300 // 5 minutes // Enable SOAP message logging for debugging Task { guard let vol = await someDevice.openHomeVolume1Service else { return } do { // Log request body and response XML let volumeResp = try await vol.volume(log: .bodyAndResponse) print("Current volume: \(volumeResp.value)") await vol.subscribeToEvents() let status = await vol.subscriptionStatus switch status { case .subscribed: print("Subscribed successfully") case .failed: print("Subscription failed — retrying automatically") default: print("Status: \(status)") } // Explicit unsubscribe when done await vol.unsubscribeFromEvents() } catch { print("Error: \(error)") } } ``` -------------------------------- ### Manage OpenHome Product Information Source: https://context7.com/katoemba/swiftupnp/llms.txt Accesses and modifies device identity, standby status, and audio source selection. Ensure the `OpenHomeProduct1Service` is available on the device. ```swift import SwiftUPnP func manageProduct(device: UPnPDevice) async { guard let product = await device.openHomeProduct1Service else { return } do { // Device identity let mfr = try await product.manufacturer() print("Manufacturer: \(mfr.name), URL: \(mfr.url)") let model = try await product.model() print("Model: \(model.name), info: \(model.info)") let prod = try await product.product() print("Room: \(prod.room), Name: \(prod.name)") // Standby let standby = try await product.standby() print("In standby: \(standby.value)") try await product.setStandby(value: false) // wake up // List sources let count = try await product.sourceCount() print("Sources: \(count.value)") for i in 0.. BrowseResult func browseDIDL( objectID: String, browseFlag: BrowseFlag, filter: String, startingIndex: UInt, requestedCount: UInt, sortCriteria: String ) async throws -> DIDLObjects func searchDIDL( containerID: String, searchCriteria: String, filter: String, startingIndex: UInt, requestedCount: UInt, sortCriteria: String ) async throws -> DIDLObjects ``` ### Parameters * **`browse` method:** * `objectID` (String) - The ID of the object to browse. * `browseFlag` (BrowseFlag) - Specifies the type of browse operation (e.g., `.browseDirectChildren`). * `filter` (String) - A filter string to specify which properties to return. * `startingIndex` (UInt) - The starting index for the results. * `requestedCount` (UInt) - The maximum number of results to return. * `sortCriteria` (String) - Criteria for sorting the results. * **`searchDIDL` method:** * `containerID` (String) - The ID of the container to search within. * `searchCriteria` (String) - The criteria for the search. * `filter` (String) - A filter string to specify which properties to return. * `startingIndex` (UInt) - The starting index for the results. * `requestedCount` (UInt) - The maximum number of results to return. * `sortCriteria` (String) - Criteria for sorting the results. ### Response * **`browse` method:** Returns a `BrowseResult` containing raw DIDL-Lite XML string and metadata. * **`browseDIDL` and `searchDIDL` methods:** Returns a `DIDLObjects` containing decoded `DIDLItem` and `DIDLContainer` objects. ``` -------------------------------- ### Generate Swift UPnP Service from SCPD XML Source: https://context7.com/katoemba/swiftupnp/llms.txt Commands to build the UPnPCodeGenerator executable and use it to generate a Swift UPnP service implementation from a device's SCPD XML file. ```bash # Build the generator swift build -c release --product UPnPCodeGenerator # Generate a service implementation from a device's SCPD XML .build/release/UPnPCodeGenerator OpenHomeVolume1Service.xml > OpenHomeVolume1Service.swift # The output includes: # - public class MyService: UPnPService # - enums for every AllowedValueList # - async action methods with typed parameters and response structs # - extension with State struct and stateSubject / stateChangeStream ``` -------------------------------- ### Control OpenHome Transport with SwiftUPnP Source: https://context7.com/katoemba/swiftupnp/llms.txt Manage playback, seek, and subscribe to transport state changes using the OpenHome Transport service. Supports multi-modal playback via `playAs`. Requires importing SwiftUPnP and Combine. ```swift import SwiftUPnP import Combine func controlOpenHomeTransport(device: UPnPDevice) async { guard let transport = await device.openHomeTransport1Service else { return } do { // Basic playback try await transport.play() try await transport.pause() try await transport.stop() try await transport.skipNext() try await transport.skipPrevious() // Play in a specific mode (e.g. switch to Radio mode) try await transport.playAs(mode: "Radio", command: "") // Get transport state let state = try await transport.transportState() print("State: \(state.state.rawValue)") // e.g. Playing // Shuffle / repeat try await transport.setShuffle(shuffle: true) try await transport.setRepeat(repeat: false) // Seek within stream let streamInfo = try await transport.streamInfo() print("StreamId: \(streamInfo.streamId), canSeek: \(streamInfo.canSeek)") if streamInfo.canSeek { try await transport.seekSecondAbsolute(streamId: streamInfo.streamId, secondAbsolute: 120) } // Check capabilities let modeInfo = try await transport.modeInfo() print("Can skip next: \(modeInfo.canSkipNext), Can repeat: \(modeInfo.canRepeat)") // Subscribe to real-time state via AsyncStream var cancellables = Set() transport.stateSubject .sink { s in print("Transport changed — state: \(s.transportState?.rawValue ?? \"-\"), streamId: \(s.streamId ?? 0)") } .store(in: &cancellables) await transport.subscribeToEvents() } catch { print("Transport action failed: \(error)") } } ``` -------------------------------- ### Control UPnP Rendering Settings Source: https://context7.com/katoemba/swiftupnp/llms.txt Manages volume, mute, loudness, and EQ settings for UPnP devices. Requires a device with the `RenderingControl:1` service. Ensure the correct `instanceID` and `channel` are used. ```swift import SwiftUPnP func controlRendering(device: UPnPDevice) async { let services = await device.services guard let rc = services.first(where: { $0.serviceType == "urn:schemas-upnp-org:service:RenderingControl:1" }) as? RenderingControl1Service else { return } let instanceID: UInt32 = 0 do { // Volume on Master channel let vol = try await rc.getVolume(instanceID: instanceID, channel: .master) print("Volume: \(vol.currentVolume)") try await rc.setVolume(instanceID: instanceID, channel: .master, desiredVolume: 50) // Volume in dB let volDB = try await rc.getVolumeDB(instanceID: instanceID, channel: .master) print("Volume dB: \(volDB.currentVolume)") let range = try await rc.getVolumeDBRange(instanceID: instanceID, channel: .master) print("dB range: \(range.minValue) to \(range.maxValue)") // Mute let mute = try await rc.getMute(instanceID: instanceID, channel: .master) print("Muted: \(mute.currentMute)") try await rc.setMute(instanceID: instanceID, channel: .master, desiredMute: false) // Loudness let loudness = try await rc.getLoudness(instanceID: instanceID, channel: .master) try await rc.setLoudness(instanceID: instanceID, channel: .master, desiredLoudness: !loudness.currentLoudness) // Presets let presets = try await rc.listPresets(instanceID: instanceID) print("Presets: \(presets.currentPresetNameList)") try await rc.selectPreset(instanceID: instanceID, presetName: .factoryDefaults) // Subscribe to LastChange events Task { await rc.subscribeToEvents() } Task { for await state in rc.stateChangeStream { print("RenderingControl LastChange XML: \(state.lastChange ?? "-")") } } } catch { print("RenderingControl action failed: \(error)") } } ``` -------------------------------- ### OpenHomeVolume1Service Control Source: https://context7.com/katoemba/swiftupnp/llms.txt Provides async methods to get/set volume, mute, balance, and fade on OpenHome devices. State changes are delivered through the service's stateSubject. ```APIDOC ## `OpenHomeVolume1Service` — Volume, mute, balance, and fade control Provides async methods to get/set volume, mute, balance, and fade on OpenHome devices. State changes (Volume, Mute, Balance, Fade, VolumeLimit, etc.) are delivered through the service's `stateSubject`. ```swift import SwiftUPnP func controlVolume(device: UPnPDevice) async { guard let vol = await device.openHomeVolume1Service else { return } do { // Get device capabilities let caps = try await vol.characteristics() print("Max volume: \(caps.volumeMax), Unity: \(caps.volumeUnity), Steps: \(caps.volumeSteps)") // e.g. Max volume: 100, Unity: 80, Steps: 100 // Read current volume let current = try await vol.volume() print("Current volume: \(current.value)") // e.g. 45 // Set absolute volume try await vol.setVolume(value: 50) // Increment / decrement by one step try await vol.volumeInc() try await vol.volumeDec() // Mute try await vol.setMute(value: true) let muteState = try await vol.mute() print("Muted: \(muteState.value)") // true // Balance (-balanceMax ... +balanceMax) try await vol.setBalance(value: 0) // Fade try await vol.setFade(value: 0) // Check enforced limit let limit = try await vol.volumeLimit() print("Volume limit: \(limit.value)") // e.g. 100 } catch { print("Volume action failed: \(error)") } } ``` ``` -------------------------------- ### UPnPDevice.reanimate / UPnPDevice.reanimateDeep Source: https://context7.com/katoemba/swiftupnp/llms.txt Restore a UPnP device from persisted data. `reanimate` provides a shallow object, while `reanimateDeep` re-fetches the device description XML and loads all services. ```APIDOC ## `UPnPDevice.reanimate(from:)` / `UPnPDevice.reanimateDeep(from:registry:)` — Restore a device from persisted data `UPnPDevice` can be serialized to `Data` (JSON-encoded `UPnPDeviceDescription`) and later rehydrated without relying on live discovery. `reanimate` gives a shallow object; `reanimateDeep` re-fetches the device description XML from the network and loads all services. ### Usage Examples: ```swift import SwiftUPnP // Persist a discovered device func persist(device: UPnPDevice) { if let data = device.data { UserDefaults.standard.set(data, forKey: "lastDevice:\(device.uuid)") } } // Restore (shallow — no services loaded yet) func restoreShallow() -> UPnPDevice? { guard let data = UserDefaults.standard.data(forKey: "lastDevice:uuid-here") else { return nil } return UPnPDevice.reanimate(from: data) } // Restore fully loaded (async, re-fetches device XML) func restoreDeep() async -> UPnPDevice? { guard let data = UserDefaults.standard.data(forKey: "lastDevice:uuid-here") else { return nil } // Pass registry so event subscriptions work on restored services return await UPnPDevice.reanimateDeep(from: data, registry: UPnPRegistry.shared) } // Example usage within a Task Task { if let device = await restoreDeep() { print("Restored: \(device.deviceDefinition?.device.friendlyName ?? "?")") print("Services loaded: \(device.servicesLoaded)") // true } } ``` ``` -------------------------------- ### Restore UPnPDevice from persisted data Source: https://context7.com/katoemba/swiftupnp/llms.txt Use `reanimate` for a shallow restore or `reanimateDeep` for a fully loaded device that re-fetches its description XML. `reanimateDeep` requires a `UPnPRegistry` for event subscriptions. ```swift import SwiftUPnP // Persist a discovered device func persist(device: UPnPDevice) { if let data = device.data { UserDefaults.standard.set(data, forKey: "lastDevice:\(device.uuid)") } } // Restore (shallow — no services loaded yet) func restoreShallow() -> UPnPDevice? { guard let data = UserDefaults.standard.data(forKey: "lastDevice:uuid-here") else { return nil } return UPnPDevice.reanimate(from: data) } // Restore fully loaded (async, re-fetches device XML) func restoreDeep() async -> UPnPDevice? { guard let data = UserDefaults.standard.data(forKey: "lastDevice:uuid-here") else { return nil } // Pass registry so event subscriptions work on restored services return await UPnPDevice.reanimateDeep(from: data, registry: UPnPRegistry.shared) } // Usage Task { if let device = await restoreDeep() { print("Restored: \(device.deviceDefinition?.device.friendlyName ?? "?")") print("Services loaded: \(device.servicesLoaded)") // true } } ``` -------------------------------- ### Browse and Search UPnP Media Server Content Source: https://context7.com/katoemba/swiftupnp/llms.txt Use the ContentDirectory1Service to browse media server contents. The browseDIDL and searchDIDL convenience extensions decode DIDL-Lite XML into typed objects. ```swift import SwiftUPnP func browseMediaServer(device: UPnPDevice) async { guard let cd = await device.contentDirectory1Service else { return } do { // Raw browse — result is DIDL-Lite XML string let raw = try await cd.browse( objectID: "0", // root container browseFlag: .browseDirectChildren, filter: "*", startingIndex: 0, requestedCount: 30, sortCriteria: "" ) print("Total items in root: \(raw.totalMatches)") // Convenience: decoded DIDL objects let result = try await cd.browseDIDL( objectID: "0", browseFlag: .browseDirectChildren, filter: "*", startingIndex: 0, requestedCount: 30, sortCriteria: "+dc:title" ) print("Returned \(result.numberReturned) of \(result.totalMatches)") for container in result.container { print(" Container: \(container.title) (id=\(container.id))") } for item in result.item { print(" Track: \(item.title) by \(item.artist.first?.value ?? "unknown")") if let res = item.res.first { print(" URI: \(res.value), duration: \(res.duration ?? "-")") } } // Search example let searchResult = try await cd.searchDIDL( containerID: "0", searchCriteria: "upnp:class derivedfrom \"object.item.audioItem\" and dc:title contains \"Bach\"", filter: "*", startingIndex: 0, requestedCount: 50, sortCriteria: "+dc:title" ) print("Found \(searchResult.totalMatches) tracks matching Bach") } catch { print("Browse failed: \(error)") } } ``` -------------------------------- ### OpenHomePlaylist1Service Source: https://context7.com/katoemba/swiftupnp/llms.txt Manages the active playlist on an OpenHome renderer: play/pause/stop/next/previous, insert/delete tracks, seek by id or time, and read back the current id array. ```APIDOC ## OpenHomePlaylist1Service ### Description Manages the active playlist on an OpenHome renderer, including transport control, playback mode settings, track management, seeking, and event subscription. ### Methods - `play()`: Starts playback. - `pause()`: Pauses playback. - `stop()`: Stops playback. - `next()`: Skips to the next track. - `previous()`: Skips to the previous track. - `setRepeat(value: Bool)`: Sets the repeat mode. - `setShuffle(value: Bool)`: Sets the shuffle mode. - `shuffle()`: Gets the current shuffle state. - `id()`: Gets the current track ID. - `idArray()`: Gets all track IDs in the playlist. - `read(id: Int)`: Reads track information by ID. - `insert(afterId: Int, uri: String, metadata: String)`: Inserts a new track. - `deleteId(value: Int)`: Deletes a track by ID. - `seekId(value: Int)`: Seeks to a specific track ID. - `seekIndex(value: Int)`: Seeks to a track by its index. - `seekSecondAbsolute(value: Int)`: Seeks to a specific second within the current track. - `subscribeToEvents()`: Subscribes to playlist state changes. ### State Subscription - `stateSubject`: A Combine subject that publishes playlist state changes, including transport state, repeat, shuffle, and track count. ``` -------------------------------- ### Subscribe to UPnPService state changes Source: https://context7.com/katoemba/swiftupnp/llms.txt Subscribe to live state changes from a UPnP service using either Combine's `sink` or Swift's `AsyncStream`. The library handles subscription renewal automatically. ```swift import SwiftUPnP import Combine @MainActor func listenToVolume(device: UPnPDevice) { guard let vol = device.openHomeVolume1Service else { return } // Option 1: Combine var cancellables = Set() vol.stateSubject .receive(on: RunLoop.main) .sink { state in print("Volume: \(state.volume ?? 0), Muted: \(state.mute ?? false)") } .store(in: &cancellables) // Option 2: AsyncStream Task { for await state in vol.stateChangeStream { print("Volume: \(state.volume ?? 0), Muted: \(state.mute ?? false)") print("Balance: \(state.balance ?? 0), Fade: \(state.fade ?? 0)") } } Task { await vol.subscribeToEvents() // Subscription status accessible on main actor: // vol.subscriptionStatus == .subscribed } } ``` -------------------------------- ### AsyncStream Device Events with Swift Concurrency Source: https://context7.com/katoemba/swiftupnp/llms.txt Utilizes Swift structured concurrency to observe device discovery events via `AsyncStream`. Events are guaranteed to be delivered on the main thread. This approach is suitable for modern Swift applications leveraging async/await. ```swift import SwiftUPnP class DeviceWatcher { private let registry = UPnPRegistry.shared func watch() { Task { @MainActor in for await device in registry.deviceAddedStream { print("Appeared: \(device.deviceDefinition?.device.friendlyName ?? device.uuid)") // device.services is populated and ready if let vol = device.openHomeVolume1Service { print(" Has Volume service: \(vol.serviceId)") } } } Task { @MainActor in for await device in registry.deviceRemovedStream { print("Disappeared: \(device.uuid)") } } try? registry.startDiscovery() } } ``` -------------------------------- ### Observe UPnP Device Additions and Removals with AsyncStream Source: https://github.com/katoemba/swiftupnp/blob/main/README.md Utilize AsyncStream for a more modern Swift concurrency approach to observing device additions and removals. This method uses Task to handle the asynchronous iteration. ```swift private let openHomeRegistry = UPnPRegistry.shared public init() { Task { for await device in openHomeRegistry.deviceAddedStream { print("Detected device \(device.deviceDefinition.device.friendlyName) of type \(device.deviceType)") } } Task { for await device in openHomeRegistry.deviceRemovedStream { print("Removed device \(device.deviceDefinition.device.friendlyName) of type \(device.deviceType)") } } } public func startListening() { // A list of device types can be passed to discover. The default devices types used when nothing is specified: // urn:schemas-upnp-org:device:MediaServer:1 // urn:linn-co-uk:device:Source:1 // urn:av-openhome-org:device:Source:1 try? openHomeRegistry.startDiscovery() } public func stopListening() { openHomeRegistry.stopDiscovery() } ``` -------------------------------- ### Invoke UPnP Action Source: https://github.com/katoemba/swiftupnp/blob/main/README.md Call UPnP actions using async functions. Ensure the device and service are available before invoking. ```swift let device: UPnPDevice try? await device.openHomeVolume1Service?.setVolume(value: 50) ``` -------------------------------- ### Parse UPnP DIDL-Lite Metadata Strings Source: https://context7.com/katoemba/swiftupnp/llms.txt Use the DIDLLite struct to parse UPnP DIDL-Lite XML metadata. The `from` method parses a complete document, while `firstItem` extracts the first item. ```swift import SwiftUPnP func parseDIDL(metadataString: String) { // Parse a complete DIDL-Lite document guard let didl = DIDLLite.from(metadataString) else { print("Failed to parse DIDL-Lite") return } print("Containers: \(didl.container.count), Items: \(didl.item.count)") // Convenience — extract just the first item if let item = DIDLLite.firstItem(metadataString) { print("Title: \(item.title)") print("Album: \(item.album ?? \"n/a\")") print("Track #: \(item.originalTrackNumber ?? 0)") print("Artists: \(item.artist.map { $0.value }.joined(separator: ", "))") for res in item.res { print("Resource: \(res.value)") print(" ProtocolInfo: \(res.protocolInfo ?? "-")") print(" Duration: \(res.duration ?? "-")") print(" SampleFreq: \(res.sampleFrequency ?? "-")") print(" Channels: \(res.nrAudioChannels.map { String($0) } ?? "-")") } for artUrl in item.albumArtURI { print("Art: \(artUrl)") } } } ``` -------------------------------- ### DIDLLite - Parse DIDL-Lite Metadata Source: https://context7.com/katoemba/swiftupnp/llms.txt Provides functionality to parse DIDL-Lite XML metadata strings into structured Swift objects, allowing easy access to media item properties. ```APIDOC ## `DIDLLite` — Parse DIDL-Lite metadata strings A Codable struct for parsing UPnP DIDL-Lite XML metadata (returned by `ContentDirectory`, stored in playlist track metadata, etc.). ### Class Methods * `static func from(_ metadataString: String) -> DIDLLite?` * Parses a complete DIDL-Lite XML string into a `DIDLLite` object. * `static func firstItem(_ metadataString: String) -> DIDLItem?` * Convenience method to extract and parse only the first `DIDLItem` from a DIDL-Lite string. ### `DIDLLite` Properties * `container`: An array of `DIDLContainer` objects. * `item`: An array of `DIDLItem` objects. ### `DIDLItem` Properties (Examples) * `title`: String - The title of the item. * `album`: String? - The album the item belongs to. * `originalTrackNumber`: UInt? - The track number. * `artist`: [Person]? - An array of artists associated with the item. * `res`: [Resource]? - An array of resource objects representing media locations. * `albumArtURI`: [String] - An array of URIs for album art. ### `Resource` Properties (Examples) * `value`: String - The URI of the resource. * `protocolInfo`: String? - The protocol information for the resource. * `duration`: String? - The duration of the media resource. * `sampleFrequency`: String? - The sample frequency of the audio resource. * `nrAudioChannels`: Int? - The number of audio channels. ``` -------------------------------- ### RenderingControl1Service Control Source: https://context7.com/katoemba/swiftupnp/llms.txt This function shows how to control rendering properties such as volume, mute, loudness, and presets using the RenderingControl1Service. It also covers subscribing to LastChange events. ```APIDOC ## RenderingControl1Service ### Description Provides access to UPnP-standard rendering control including per-channel volume, mute, loudness, and display properties (brightness, contrast, etc.). ### Methods - `getVolume(instanceID: UInt32, channel: String)`: Retrieves the current volume for a specific channel. - `setVolume(instanceID: UInt32, channel: String, desiredVolume: UInt16)`: Sets the desired volume for a specific channel. - `getVolumeDB(instanceID: UInt32, channel: String)`: Retrieves the current volume in decibels (dB) for a specific channel. - `getVolumeDBRange(instanceID: UInt32, channel: String)`: Retrieves the minimum and maximum volume range in dB for a specific channel. - `getMute(instanceID: UInt32, channel: String)`: Retrieves the current mute status for a specific channel. - `setMute(instanceID: UInt32, channel: String, desiredMute: Bool)`: Sets the desired mute status for a specific channel. - `getLoudness(instanceID: UInt32, channel: String)`: Retrieves the current loudness status for a specific channel. - `setLoudness(instanceID: UInt32, channel: String, desiredLoudness: Bool)`: Sets the desired loudness status for a specific channel. - `listPresets(instanceID: UInt32)`: Lists all available preset names. - `selectPreset(instanceID: UInt32, presetName: String)`: Selects a rendering control preset. - `subscribeToEvents()`: Subscribes to LastChange events for the rendering control service. ### Parameters - `instanceID` (UInt32): The instance ID of the rendering control. - `channel` (String): The audio channel to control (e.g., .master). - `desiredVolume` (UInt16): The desired volume level. - `desiredMute` (Bool): The desired mute state (true for muted, false for unmuted). - `desiredLoudness` (Bool): The desired loudness state. - `presetName` (String): The name of the preset to select. ### State Changes - `stateChangeStream`: A stream that emits updates on rendering control state changes, primarily through the LastChange XML. ``` -------------------------------- ### OpenHomeTransport1Service Source: https://context7.com/katoemba/swiftupnp/llms.txt Controls playback on devices that expose the OpenHome Transport service, supporting multiple playback modes including multi-modal playback via `playAs(mode:command:)`. ```APIDOC ## OpenHomeTransport1Service ### Description Controls playback on devices supporting the OpenHome Transport service, offering basic playback controls, mode-specific playback, stream seeking, capability checks, and real-time state subscription. ### Methods - `play()`: Starts playback. - `pause()`: Pauses playback. - `stop()`: Stops playback. - `skipNext()`: Skips to the next track. - `skipPrevious()`: Skips to the previous track. - `playAs(mode: String, command: String)`: Plays content in a specified mode. - `transportState()`: Gets the current transport state. - `setShuffle(shuffle: Bool)`: Sets the shuffle mode. - `setRepeat(repeat: Bool)`: Sets the repeat mode. - `streamInfo()`: Gets information about the current stream, including seekability. - `seekSecondAbsolute(streamId: Int, secondAbsolute: Int)`: Seeks to a specific second within the stream. - `modeInfo()`: Retrieves information about supported playback modes and capabilities. - `subscribeToEvents()`: Subscribes to transport state changes. ### State Subscription - `stateSubject`: A Combine subject that publishes real-time transport state changes, including transport state and stream ID. ``` -------------------------------- ### UPnPService.subscribeToEvents() / unsubscribeFromEvents() Source: https://context7.com/katoemba/swiftupnp/llms.txt Subscribe to live state change events from a UPnP service. The library automatically renews subscriptions and routes incoming NOTIFY payloads. ```APIDOC ## `UPnPService.subscribeToEvents()` / `unsubscribeFromEvents()` — Live state change subscription Sends an HTTP SUBSCRIBE to the device's event URL. The library auto-renews subscriptions before timeout. Incoming NOTIFY payloads are routed internally to the correct service's `stateSubject`. ### Usage Example: ```swift import SwiftUPnP import Combine @MainActor func listenToVolume(device: UPnPDevice) { guard let vol = device.openHomeVolume1Service else { return } // Option 1: Combine var cancellables = Set() vol.stateSubject .receive(on: RunLoop.main) .sink { state in print("Volume: \(state.volume ?? 0), Muted: \(state.mute ?? false)") } .store(in: &cancellables) // Option 2: AsyncStream Task { for await state in vol.stateChangeStream { print("Volume: \(state.volume ?? 0), Muted: \(state.mute ?? false)") print("Balance: \(state.balance ?? 0), Fade: \(state.fade ?? 0)") } } Task { await vol.subscribeToEvents() // Subscription status accessible on main actor: // vol.subscriptionStatus == .subscribed } } ``` ``` -------------------------------- ### Control Playback with AVTransport1Service Source: https://context7.com/katoemba/swiftupnp/llms.txt Use this service to control playback on UPnP AV renderers. It requires an instance ID and supports loading tracks, playing, seeking, skipping tracks, pausing, stopping, and setting play modes. Ensure the device has an AVTransport:1 service. ```swift import SwiftUPnP func controlAVTransport(device: UPnPDevice) async { // Note: avTransport1Service is accessible through the services array, // or add it via UPnPRegistry.typedService(). The registry auto-assigns it. let services = await device.services guard let av = services.first(where: { $0.serviceType == "urn:schemas-upnp-org:service:AVTransport:1" }) as? AVTransport1Service else { return } do { let instanceID: UInt32 = 0 // Load a track try await av.setAVTransportURI( instanceID: instanceID, currentURI: "http://192.168.1.10:8200/MediaItems/42.flac", currentURIMetaData: "" ) // Play at normal speed try await av.play(instanceID: instanceID, speed: .one) // Get playback position let pos = try await av.getPositionInfo(instanceID: instanceID) print("Track \(pos.track), elapsed: \(pos.relTime), duration: \(pos.trackDuration)") // e.g. Track 1, elapsed: 00:01:23, duration: 00:04:55 // Seek to 30 seconds absolute try await av.seek(instanceID: instanceID, unit: .absTime, target: "00:00:30") // Skip forward/back try await av.next(instanceID: instanceID) try await av.previous(instanceID: instanceID) // Pause and stop try await av.pause(instanceID: instanceID) try await av.stop(instanceID: instanceID) // Transport state let info = try await av.getTransportInfo(instanceID: instanceID) print("State: \(info.currentTransportState.rawValue)") // e.g. STOPPED // Set shuffle play mode try await av.setPlayMode(instanceID: instanceID, newPlayMode: .shuffle) } catch { print("AVTransport action failed: \(error)") } } ``` -------------------------------- ### Receive UPnP State Changes with Combine Source: https://github.com/katoemba/swiftupnp/blob/main/README.md Subscribe to state changes from a UPnP service using Combine publishers. State changes are delivered as strongly typed structs. Ensure the service is subscribed to events. ```swift let device: UPnPDevice var cancellables = Set() if let service = device.openHomeVolume1Service { service.stateSubject .sink { print("Received volume change, volume = \($0.volume ?? -1)") } .store(in: &cancellables) Task { await service.subscribeToEvents() } } ``` -------------------------------- ### Receive UPnP State Changes with AsyncStream Source: https://github.com/katoemba/swiftupnp/blob/main/README.md Alternatively, receive state changes from a UPnP service by iterating over an AsyncStream. Ensure the service is subscribed to events. ```swift let device: UPnPDevice if let service = device.openHomeVolume1Service { Task { for await volumeChange in service.stateChangeStream { print("Received volume change, volume = \(volumeChange.volume ?? -1)") } } Task { await service.subscribeToEvents() } } ``` -------------------------------- ### Access typed UPnPService properties Source: https://context7.com/katoemba/swiftupnp/llms.txt Access specific UPnP services like ConnectionManager or ContentDirectory via convenient computed properties on `UPnPDevice`. These properties are available once `servicesLoaded` is true. ```swift import SwiftUPnP @MainActor func inspectDevice(_ device: UPnPDevice) { // UPnP AV Profile if let cm = device.connectionManager1Service { print("ConnectionManager: \(cm.serviceId)") } if let cd = device.contentDirectory1Service { print("ContentDirectory: \(cd.serviceId)") } if let av = device.avTransport1Service { /* ... */ } // via extension not shown explicitly but discoverable if let rc = device.renderingControl1Service { /* ... */ } // OpenHome Profile if let vol = device.openHomeVolume1Service { print("Volume1: \(vol.controlUrl)") } if let vol2 = device.openHomeVolume2Service { print("Volume2: \(vol2.controlUrl)") } if let pl = device.openHomePlaylist1Service { print("Playlist1: \(pl.serviceId)") } if let tp = device.openHomeTransport1Service { print("Transport1: \(tp.serviceId)") } if let prod = device.openHomeProduct1Service { print("Product1: \(prod.serviceId)") } if let info = device.openHomeInfo1Service { print("Info1: \(info.serviceId)") } if let rad = device.openHomeRadio1Service { print("Radio1: \(rad.serviceId)") } } ```