Skip to main content
SDK reference · JouleMoQ for Swift

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.

byte
direction
body
when
0x01
client → server
SubscribeRequest
First message after the WebSocket opens.
0x02
server → client
SubscribeAck
Bridge confirms the subscription; carries track aliases.
0x10
server → client
DeliveredObjectFrame
One signed MoQT object — header + payload + JWP receipt.
0x20
client → server
UNSUBSCRIBE
Reserved. v0 closes the socket instead.
0xff
either
GOODBYE
Graceful shutdown with optional reason payload.

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