Moq
The Swift Package Manager target for Media over QUIC.
This is an ergonomic wrapper around the UniFFI-generated MoqFFI types, providing AsyncSequence adapters and Swift-friendly errors.
Install
.package(url: "https://github.com/moq-dev/moq-swift", from: "0.2.0"),Add Moq to your target's dependencies:
.target(
name: "MyApp",
dependencies: [
.product(name: "Moq", package: "moq-swift"),
],
),Supported platforms: iOS 15+, iPadOS 15+, macOS 12+. The package ships an XCFramework with iOS device (arm64), iOS Simulator (arm64 + x86_64), and macOS universal slices.
Connect
import Moq
// Wire an origin as both publish source and consume sink for the
// typical full-duplex client. Set just one side for a subscribe-only
// or publish-only client.
let origin = MoqOriginProducer()
let client = MoqClient()
client.setPublish(origin: origin)
client.setConsume(origin: origin)
let session = try await client.connect(url: "https://relay.example.com")For development against a relay with a self-signed certificate, configure the client before connecting:
let client = MoqClient()
client.setTlsDisableVerify(disable: true)
try client.setBind(addr: "127.0.0.1:0")
client.setPublish(origin: origin)
client.setConsume(origin: origin)
let session = try await client.connect(url: "https://localhost:4443")When you're done, signal graceful shutdown to the peer:
session.shutdown() // alias for cancel(code: 0)A server can reject the connection on auth grounds: MoqError.Unauthorized (HTTP 401) or MoqError.Forbidden (HTTP 403). These are terminal: retrying without new credentials won't help, so handle them separately from a transient transport failure. Use the isAuth helper to catch both:
do {
let session = try await client.connect(url: "https://relay.example.com")
} catch let error as MoqError where error.isAuth {
// Prompt for credentials; don't reconnect.
}Subscribe
let consumer = origin.consume()
let announced = try consumer.announced(prefix: "demos/")
for try await announcement in announced.announcements {
let catalog = try announcement.broadcast().subscribeCatalog()
for try await update in catalog.updates {
print("catalog: \(update)")
}
}Publish
let broadcast = try MoqBroadcastProducer()
let audio = try broadcast.publishMedia(format: "opus", init: opusInitBytes)
try origin.publish(path: "my-stream", broadcast: broadcast)
try audio.writeFrame(payload: payload, timestampUs: 0)
try audio.writeFrame(payload: payload, timestampUs: 20_000)
try audio.finish()
try broadcast.finish()On-demand raw tracks
Use a dynamic broadcast when subscribers should be able to request raw tracks that are not published yet:
let broadcast = try MoqBroadcastProducer()
let dynamic = try broadcast.dynamic()
try origin.publish(path: "events", broadcast: broadcast)
for try await track in dynamic.requestedTracks {
if try track.name() == "alerts" {
try track.writeFrame(payload: Data("ready".utf8))
try track.finish()
} else {
try track.abort(errorCode: 404)
}
}JSON tracks
For JSON payloads, publish and subscribe with the framing handled for you, in one of two modes. Snapshot (lossy) carries one value updated over time; a subscriber only sees the latest. Stream (lossless) is an ordered append-log where every record is preserved. Values cross as JSON strings; encode and decode with JSONEncoder/JSONDecoder.
// Snapshot: each update supersedes the last.
let config = MoqJsonConfig(deltaRatio: 8, compression: true)
let status = try broadcast.publishJson(name: "status", config: config)
try status.update(value: #"{"state":"live"}"#)
let broadcastConsumer = try broadcast.consume()
let consumer = try broadcastConsumer.subscribeJson(name: "status", config: config)
for try await value in consumer.values {
print(value)
}
// Stream: every record is delivered in order.
let streamConfig = MoqJsonStreamConfig(compression: false)
let events = try broadcast.publishJsonStream(name: "events", config: streamConfig)
try events.append(value: #"{"event":"started"}"#)compression must match on the producer and subscriber. In snapshot mode, deltaRatio of 0 disables merge-patch deltas (every change is a fresh snapshot).
Cancellation
All async sequences cooperate with structured concurrency. Cancelling the surrounding Task propagates to the underlying cancel() call on the consumer:
let task = Task {
for try await frame in mediaConsumer {
process(frame)
}
}
// Later:
task.cancel() // releases native resourcesLocal development
To run the test suite, build a host-only XCFramework first:
just swift checkThis runs swift/scripts/check.sh, which builds moq-ffi for the host arch, regenerates the UniFFI Swift bindings, drops a single-slice MoqFFI.xcframework into swift/, and then runs swift test. Requires macOS with xcodebuild.
See also
- Source: swift/Sources/Moq
- Mirror repo: moq-dev/moq-swift
- The Rust crate this wraps: moq-net