open-standards/wai/wai-sink-swift/README.md
Transaction Science dc7c2de277
All checks were successful
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 15s
WAI operation catalogue / does this change touch it (push) Successful in 16s
WAI examples / WAI examples · does this change touch them (push) Successful in 15s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 50s
wai-sink-swift native sink package / does this change touch it (push) Successful in 2m29s
wai-sink-android native sink library / does this change touch it (push) Successful in 2m39s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 12m29s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 13m50s
WAI examples / WAI examples · portable · linux (push) Successful in 14m26s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 5m48s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 5m29s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 12m1s
WAI examples / WAI examples · android-app · linux-arm64 (push) Successful in 13m39s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Successful in 15m13s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (push) Successful in 19m26s
Swift sink: IntDecodeTests run hosted in TestHost, in the simulators and on devices
The integer decode's tests (native-sink §13) join the TestHost project with their vectors in
the host's test bundle (the staged Resources/int folder), so scripts/test-simulator.sh runs
them in fresh phone and television simulators, as a device runs the host; the script refuses
to run when the vectors are not staged. Run on 2026-11-11 on an arm64 laptop (Xcode 27.2):
the xcframework (desktop and both simulator slices) built, swift test passed (10 tests), and
both simulators passed every test, IntDecodeTests among them; in the simulators the decoder's
GPU is refused at discovery (their Metal adapter offers 15 inter-stage shader variables, the
WebGPU default limits ask 16), so the nine vectors decoded on the CPU there, each the CPU's
bytes. native-sink §13.5 records it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-11 16:37:20 -05:00

8.9 KiB

wai-sink-swift — the native sink for the platform player, in Swift

The native sink of extensions/native-sink.md for phones and tablets, televisions and set-top boxes, and desktops whose native player an application reaches from Swift. The player is not changed and decodes on the platform's media engine; the sink sits on the path of its requests and, for one playback session:

  • verifies a media playlist against its playlist receipt and the chain of its live revisions (http-delivery §8.6), and every object whose name states its content hash against that hash, and withholds from the player whatever does not verify;
  • signs, under a fresh key per session, stream-audit's acknowledgements of every object received, a presentation claim for each marked ad, and the session record, and posts the bundle to the owner's collector (wai-audit serve), with its Event-mode CMCD while it plays;
  • ends a session nobody can see, or whose viewer has gone idle, and stops the player's requests.

Everything that decides is libwai's C interface (native-sink §9), the same on every native platform; this package is the platform's half: the loopback relay, the player, and the signals of who can see it.

Layout

path what
Package.swift the package: library WaiSink, binary target WaiFFI (the C interface): a release's framework by URL and checksum, or the one built here
Sources/WaiSink/Sink.swift WaiSink: one session, every call of the C interface, JSON in and out
Sources/WaiSink/Relay.swift WaiRelay: the loopback server the player is given (native-sink §4.4)
Sources/WaiSink/PlayerSession.swift WaiPlayerSession: the platform player through the relay, observed every 200 ms (§5, §6)
Sources/WaiSink/Attention.swift WaiViewAttention: whether the application is shown, the player view's area in view, the viewer's input (§6.2); WaiFixedAttention for hosts that decide it themselves
Sources/WaiSink/Collector.swift WaiCollector: v1/sessions and v1/cmcd (stream-audit §12)
Sources/WaiSink/IntDecoder.swift WaiIntDecoder: WAI's integer capabilities decoded in the application's process, on the device's GPU where it passes the start-up exactness check, with the report naming the unit (native-sink §13)
scripts/build-xcframework.sh builds build/WaiFFI.xcframework from ../wai-rs
scripts/package-release.sh the framework zipped, and the copy of the package that names the zip by URL and checksum, for a release
scripts/check-release-package.sh that copy resolved, built and run as an application uses it, and refused with a wrong checksum
scripts/test-simulator.sh runs the tests hosted in TestHost/ in a fresh simulator it creates, boots and deletes, every step under a hard limit
Tests/WaiSinkTests the corpus through the wrapper; the relay over the reference package's floor; the player end to end; the integer decode on the GPU and the CPU (IntDecodeTests)
TestHost/ a minimal application the same tests run inside on a phone or tablet, where no tool-hosted test bundle runs: an Xcode project that depends on this package by its path; consumers never see it

Use

import WaiSink

let cfg = WaiSink.Config(channel: "news-east", owner: "<the collector's key, 64 hex>")
let session = try WaiPlayerSession(url: URL(string: "https://media.example/ch/7/v.m3u8")!,
                                   config: cfg,
                                   attention: WaiViewAttention(view: playerView),
                                   collector: WaiCollector(base: URL(string: "https://collector.example/")!))
playerLayer.player = session.player     // the host shows the player as it always has
session.onEnd = { bundle, ended in /* posted already; kept here if the host wants it */ }
session.start()

For a stream the owner's packager seals (playlist receipts, segments named by their SHA-256, packaging §7), set requireSigned: true (or contentAddressed: true), and nothing the publisher did not sign reaches the decoder. For a stream another service delivers, leave both off: it is acknowledged and evidenced, never refused for what its names look like.

Decoding WAI's integer capabilities on the GPU

A host that plays a WAI capability itself, not through the platform's player, decodes it with WaiIntDecoder (native-sink §13): wai.neural.int_hyper, wai.neural.int_synth, wai.neural.int_mlicpp, wai.video.int_hyper, wai.video.int_cond, wai.video.int_motion, wai.audio.int_codec and wai.splat.int_codec, as whole payloads or WAI1 envelopes.

let decoder = try WaiIntDecoder()                 // finds the GPU and checks it, once per process
print(try decoder.gpu())                          // state "ready", the adapter, the translator
let out = try decoder.decode(capability: "wai.neural.int_hyper", payload: wih1,
                             set: ["entropy.json": e, "g_s.whm": gs, "h_s.whm": hs])
out.bytes        // RGB8 rows, the same bytes on every device and on the CPU
out.unit         // "gpu", or "cpu" where the GPU is not used, with the reason in out.dispatch

The GPU runs the integer operator set's WGSL kernels through libwai's Metal backend, integer arithmetic only, every value in a width the graph's bounds certificate proves it fits. It runs nothing until it has passed the start-up exactness check: WGSL's integer semantics, the arithmetic probe, graphs at every width and at the edge of the narrow kernels' bound, and a whole decode, each giving the CPU's integers. A GPU that differs anywhere is not used, and every decode's report says so (dispatch_unit_inexact); the decode then runs on the CPU and gives the same bytes. WaiIntDecoder(gpu: .off) decodes on the CPU alone. The library links Metal, QuartzCore and CoreGraphics for it; Package.swift declares them.

Build and test

On the build host (Xcode with the phone, television and desktop SDKs; Rust with the targets scripts/build-xcframework.sh names):

cd wai/wai-sink-swift
scripts/build-xcframework.sh                 # every slice; WAI_SINK_SLICES="macos ios-sim" for some
swift test                                   # the desktop: corpus, relay, player end to end, integer decode
scripts/test-simulator.sh ios                # the same tests, hosted in TestHost, in a fresh phone simulator
scripts/test-simulator.sh tvos               # and in a fresh television simulator

On a phone or tablet, the tests run hosted in TestHost/ (signed with the team configured in Xcode):

xcodebuild build-for-testing -project TestHost/WaiSinkTestHost.xcodeproj -scheme WaiSinkTestHost \
  -destination 'generic/platform=iOS' -allowProvisioningUpdates DEVELOPMENT_TEAM=<team>
xcodebuild test-without-building -xctestrun <the .xctestrun it wrote> -destination 'platform=iOS,id=<device>'

The player tests make their presentation at run time with the platform's own encoder (AVC, ISO/IEC 14496-10, in fragmented MP4, a segment a second), serve it from an origin on loopback, and play it through the relay with a video output attached, since no layer is on screen in a test.

Distribution

A release attaches WaiFFI-<version>.xcframework.zip, the framework with every slice, and wai-sink-swift-<version>.zip, this package with its Package.swift naming that zip by its download URL and its SwiftPM checksum (.binaryTarget(name: "WaiFFI", url: …, checksum: …), the archive's SHA-256, as swift package compute-checksum gives it). scripts/package-release.sh writes both. An application adds the package from that folder, as a local package or a copy in a repository of its own; SwiftPM fetches the framework from the URL and refuses it unless its checksum is the one named.

Before the release attaches them, scripts/check-release-package.sh resolves, builds and runs an application that depends on the package, the framework served from the release's own files through a mirror of its URL, and requires the package with one digit of its checksum changed to be refused; once they are attached, it does both again from the URL itself. In this repository, Package.swift names build/WaiFFI.xcframework while scripts/build-xcframework.sh has built it there, so the tests run against the libwai of their checkout.

The library holds no codec: the platform's player decodes, and its codecs are the platform's, licensed with it. The integer decoder is WAI's own (wai-rs), with wgpu (MIT or Apache-2.0) for the GPU.

Energy

The platform's player decodes in a process of the platform's, on its fixed-function decoder, which no counter an application can read covers. The session therefore states the sink decode and the display as the unmetered marker (native-sink §7), never the application's own counter as either. A decode through WaiIntDecoder runs in the application's process: on the CPU a counter of the process covers it; on the GPU a CPU counter does not, and the sink decode stays the marker unless the host holds a figure that covers the GPU's work (native-sink §13.4).