A modern swift framework for UPnP

In this article I want to present details on SwiftUPnP, the library that powers the UPnP / OpenHome implementation in Rigelian.

OpenHome is a network protocol derived from UPnP, specifically meant to control music players. Here I’m highlighting 3 alternatives to implement this protocol in a swift based iOS / macOS app.

  • ohNet – a cross platform library provided by OpenHome itself.
  • UPnAtom – a swift based implementation of the standard UPnP protocol.
  • SwiftUPnP – a modern swift based implementation of the full UPnP AV and OpenHome protocol stack.

ohNet

This is a cross platform library provided by OpenHome itself. It’s built in C++ and includes bindings for C++, C#, Java, JavaScript & C. It’s actively maintained (by Linn I believe).
I never managed to properly compile this using Xcode. While I could compile from the command line, I could not successfully integrate it into an app. I’m sure it must be possible, but I gave up after spending too much time on it. Also, bridging from C/C++ to swift is possible but not straight forward, and results in a call site that is not optimized for swift.

UPnAtom

This library is built in swift, and includes a fairly complete implementation of the AV UPnP protocol. Support for DIDL (the schema used when browsing metadata) is missing, I added partial support in a fork from the original library. Also I added support for swift package manager.
I used this library for experimental browsing music files on a UPnP media server. Because there is no support for the various OpenHome services, that would have to be added which would be a lot of work with cumbersome xml parsing. I didn’t pursue this further.

SwiftUPnP

This library is built in swift and includes a full implementation of the AV UPnP protocol as well as the complete OpenHome protocol. It uses standard networking facilities, async/await for asynchronous processing of results, and codable structs that can easily be encoded/decoded through XMLCoder.
I created this based on my learnings on UPnAtom, in an attempt to make a simpler yet more complete library. It distinguishes between devices and services, and will detect the available services after a player is discovered on the network. These services are made available via an easy to use swift interface.
I implemented key functions of a few initial services by hand, and found this to be error-prone with a lot of repetitive effort. There had to be an easier way, and there is: the formal xml-based service descriptions that are available for both UPnP and OpenHome can be parsed and used to generate swift source code that implements 100% of the service, including the processing of results as well as processing UPnP events.

The service definition is captured in a set of codable structs. The root struct is then extended with swift code generation functions, to generate code like this, to insert a track into the play queue:

public struct InsertResponse: Codable {
	enum CodingKeys: String, CodingKey {
		case newId = "NewId"
	}

	public var newId: UInt32

	public func log(deep: Bool = false, indent: Int = 0) {
		Logger.swiftUPnP.debug("\(Logger.indent(indent))InsertResponse {")
		Logger.swiftUPnP.debug("\(Logger.indent(indent+1))newId: \(newId)")
		Logger.swiftUPnP.debug("\(Logger.indent(indent))}")
	}
}
public func insert(afterId: UInt32, uri: String, metadata: String, log: UPnPService.MessageLog = .none) async throws -> InsertResponse {
	struct SoapAction: Codable {
		enum CodingKeys: String, CodingKey {
			case urn = "xmlns:u"
			case afterId = "AfterId"
			case uri = "Uri"
			case metadata = "Metadata"
		}

		@Attribute var urn: String
		public var afterId: UInt32
		public var uri: String
		public var metadata: String
	}
	struct Body: Codable {
		enum CodingKeys: String, CodingKey {
			case action = "u:Insert"
			case response = "u:InsertResponse"
		}

		var action: SoapAction?
		var response: InsertResponse?
	}
	let result: Envelope = try await postWithResult(action: "Insert", envelope: Envelope(body: Body(action: SoapAction(urn: Attribute(serviceType), afterId: afterId, uri: uri, metadata: metadata))), log: log)

	guard let response = result.body.response else { throw ServiceParseError.noValidResponse }
	return response
}

 

Which can then be called like this, where song is a structure that is previously retrieved from a UPnP media server.

let result = try? await self.playlistService.insert(afterId: afterId,
                                                    uri: song.location,
                                                    metadata: song.metadata)

 

A command line tool is created which can create service implementations of all OpenHome and UPnP services based on the service definitions. If new services are added, they can be generated in the same way.

If you want to find out more or use the framework, check out the GitHub repository.