MoQT for Apple.
One artifact: a kind byte.
JouleMoQ
is a 1,207-LOC Swift package that subscribes to live MoQT tracks from
iOS, iPadOS, macOS, and visionOS — over a plain
URLSessionWebSocketTask, no WebTransport
required. Each WebSocket message is one byte of kind plus UTF-8 JSON.
That byte is the whole wire format.
1 · Wire format
Five bytes. That's the protocol.
The Swift client mirrors the Rust
joule-moq-ws-bridge
one-to-one: each message is a single binary frame, leading byte names
the kind, the rest is JSON. New kinds get a new byte; old subscribers
ignore unknown bytes — so the wire is additive-only.
Canonical reference: joulesperbit/crates/joule-moq-ws-bridge. The Swift client must round-trip every byte above; that's the conformance test in JouleMoQTests.
2 · Install
Add to your Package.swift
// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "MyApp",
platforms: [.iOS(.v15), .macOS(.v12), .visionOS(.v1)],
dependencies: [
.package(url: "https://github.com/Transaction-Science/swift-moq.git", from: "0.3.0"),
],
targets: [
.target(name: "MyApp", dependencies: [
.product(name: "JouleMoQ", package: "swift-moq"),
]),
]
) Pulls in JouleCompose transitively (BLAKE3 + Ed25519 C ABI for signed-frame verification). Both packages are MIT-licensed, no native binaries shipped — built from source.
3 · Usage
Subscribe; iterate signed frames
import JouleMoQ
let url = URL(string: "wss://tv.transaction.science/moq-live/subscribe")!
let req = SubscribeRequest(
namespace: "tv.transaction.science.demo.live".split(separator: ".").map(String.init),
trackName: "live",
maxMrlLayer: 0
)
let sub = try await JouleMoQSubscriber(url: url, request: req).subscribe()
for try await frame in sub.objects {
let payload = try frame.payload() // Data
print("group=\(frame.header.groupId) bytes=\(payload.count)")
// Per-track decoder:
// /live → live-state JSON
// /video → layered Matryoshka AV1
// /audio → comms.audio.layered-pcm (WAI)
}
The objects async sequence yields one
DeliveredObjectFrame per
0x10 byte. Each frame carries the
MoQT object header (group / object / size) plus an
ObjectReceipt ready for verification
against the publisher's pinned key.
4 · Verify a signed frame
Match the JWP receipt-chain rules
The verifier composes BLAKE3 object hash → Merkle inclusion proof → Ed25519 signature against the group receipt, per the JWP receipts sub-spec. The Rust, browser-WASM, and Swift verifiers all pass the same vectors.
import JouleMoQ
import JouleCompose
let pinned = try VerifyingKey(hex: pinnedPublisherKeyHex)
let verifier = try JouleVerifier(trustAnchor: pinned)
// Cache the signed group receipt once per group.
try verifier.cacheGroup(jsonString: groupReceiptJson)
for try await frame in sub.objects {
let result = try verifier.verifyObject(
payload: try frame.payload(),
receipt: frame.objectReceipt
)
// result.cumulativeJoulesUWh, result.groupId, result.objectId
} 5 · Tests
One live test, three fixture tests
cd crates/joule-compose-mobile/swift-moq
xcrun swift test
Three are pure-Swift unit tests against fixture JSON (subscribe
round-trip, delivered-frame parse, receipt-shape decode). The fourth,
testLivePublisherDelivers, connects
to wss://tv.transaction.science/moq-live/subscribe
and asserts a signed frame arrives within 8 seconds. The live test
auto-skips on connection failure so air-gapped CI still passes.
Point at staging or a local publisher:
JOULE_MOQ_LIVE_URL=ws://localhost:9239/subscribe xcrun swift test 6 · Not yet
v0.4 candidates
UNSUBSCRIBE round-trip
The bridge implements 0x20; today the client closes the socket. Wiring the cooperative flow is a v0.4 line item.
Native MoQT-over-WebTransport
Once WebKit ships WebTransport, a sibling JouleMoQT target will use Network framework's QUIC stack directly. The wire format is the same.
Forward error correction
Group-gap retransmit is handled publisher-side already; surfacing the gap signal as a Swift event is a separate line item.
Kotlin / Android parallel
The wire format keeps the client small enough that a Kotlin port is roughly the same shape as JouleMoQ. Scheduled separately from the Q3 plan.
Build with it
Need a working iOS / visionOS demo, a custom track decoder, or a private staging publisher? Send us the use case and we'll come back with a scoping document.
Request Swift SDK access