open-standards/wai/wai-sink-android/README.md
Transaction Science d61d8691a3
All checks were successful
WAI operation catalogue / does this change touch it (push) Successful in 12s
wai-sink-android native sink library / does this change touch it (push) Successful in 13s
wai-sink-swift native sink package / does this change touch it (push) Successful in 27s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 14s
WAI examples / WAI examples · does this change touch them (push) Successful in 16s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 33s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 13m46s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 9m59s
WAI examples / WAI examples · portable · linux (push) Successful in 12m29s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 7m25s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 7m30s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 11m13s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Successful in 16m30s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (push) Successful in 34m15s
WAI examples / WAI examples · android-app · linux-arm64 (push) Successful in 21m46s
wai-sink-android: WaiIntDecoder, native-sink §13's integer decode through JNI, libwai built with sink_decode
The Kotlin library carries the decoder the Swift package carries. jni/wai_sink_jni.c gains the
thirteen wai_int_* calls (decoder open, free and info; a parameter set's new, add, listing and
free; a payload's decode and an envelope's, answering { bytes, answer }; a frame stream's open,
reset and free and its next object's decode); WaiIntDecoder and WaiIntFrameStream wrap them as
the Swift package does, and WaiSinkException.INT_INPUT carries WAI_ERR_INT_INPUT.
scripts/build-native.sh builds libwai with sink_decode for every ABI (wgpu's Vulkan backend,
loaded at run time; nothing new beyond the crates wai-rs's Cargo.lock holds) and checks the new
exports; its macOS host build now reads a Mach-O library's exports with nm -g (-D read none).

core/src/testkit/int-cases.json names a committed vector of each of the eight integer
capabilities with its set and the output its corpus commits (a SHA-256 of the whole output or of
each frame, or photo's ref_rgb.bin), and a stream of each of the three frame forms from the
corpus's own index. IntDecodeTest (host JVM) and IntDecodeDeviceTest (instrumented;
test-emulator.sh stages the files into the test APK) decode every case and every frame with the
GPU offered and on the CPU alone, requiring the same bytes and the committed output; a float
capability is refused on both integer paths; an unreadable configuration is INT_INPUT.

Run on 2026-11-11: on an arm64 laptop (Metal), the host JVM tests, 20 of 20: 8 of 9 cases on the
GPU (wai.video.int_motion on the CPU) and 7 of 16 frames, every output the CPU's and the committed
one; the three ABIs built and linked (16 KiB pages, exports checked); in the arm64 emulator (API
36), 16 of 16 instrumented tests. The emulator's own Vulkan driver (gfxstream over the host's
software renderer) crashed the test process with a SIGSEGV inside its vkQueueSubmit at the
decoder's first submission, so test-emulator.sh runs it with the guest's Vulkan off
(WAI_SINK_EMULATOR_VULKAN=on keeps it); the decoder there states the GPU absent and decodes every
case on the CPU. No run on a device's Vulkan GPU was possible here: the README and native-sink
§13 say so.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-11 17:31:26 -05:00

216 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# wai-sink-android — the native sink for the platform player, in Kotlin
The native sink of [`extensions/native-sink.md`](../extensions/native-sink.md) for phones, tablets
and televisions whose applications are written in Kotlin, as a library (an AAR). The platform's media
player library plays the stream and decodes on the platform's decoders, unchanged; 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 library is the platform's half: the JNI layer over that interface, the loopback
relay, the player session, and the signals of who can see it. The records it signs are the ones the
page's integration and the Swift package sign, byte for byte (the corpus's `page-parity` case).
## Layout
| path | what |
|---|---|
| `jni/wai_sink_jni.c` | the JNI layer: one function per call of `wai.h`, statuses carried as `WaiSinkException` |
| `core/src/main/kotlin` | the platform-free half: `WaiSink` (one session, every call, JSON in and out), `WaiRelay` (the loopback server the player is given, native-sink §4.4), `WaiSessionDriver` (the observation loop, §5, §6), `WaiCollector` (`v1/sessions`, `v1/cmcd`), `WaiIntDecoder` (WAI's integer capabilities decoded in the application's process, §13), `Json` |
| `sink/src/main/kotlin` | the platform's half: `WaiPlayerSession` (the media player library through the relay, observed every 200 ms) and `WaiViewAttention` (the activity's lifecycle, the window's and the display's state, the view's place in its window, the window's input, §6.2) |
| `core/src/test`, `core/src/testkit` | the unit tests on the host JVM, through the JNI layer over a host build of libwai; the kit they share with the device tests, and the integer decode's cases (`int-cases.json`) |
| `sink/src/androidTest` | the instrumented tests: the corpus and the relay on the device, the player end to end, the attention signals in a real activity |
| `scripts/build-native.sh` | libwai and the JNI layer: `android` for the AAR's ABIs, `host` for the JVM tests |
| `scripts/gradle.sh` | the pinned Gradle, fetched once and checked against its SHA-256 |
| `scripts/test-emulator.sh` | the instrumented tests in an emulator, headless |
| `tv/` | a television host for the sink: one activity, a launcher entry for the television launcher, a banner (*Television*) |
| `scripts/check-tv-apk.sh`, `scripts/tv-artwork.py` | the television host's compiled manifest held to the platform's television requirements; its banner and icon, drawn |
| `scripts/check-publication.sh` | the publication maven-publish writes, held to its coordinates and contents before a release publishes it |
## Use
```kotlin
val config = WaiSink.Config(channel = "news-east", owner = "<the collector's key, 64 hex>")
val session = WaiPlayerSession(context, "https://media.example/ch/7/v.m3u8", config,
attention = WaiViewAttention(playerView),
collector = WaiCollector("https://collector.example/"))
playerView.player = session.player // the host shows the player as it always has
session.onEnd = { bundle, ended -> /* posted already; kept here if the host wants it */ }
session.start()
```
Create, start and stop the session on the main thread. The player reads the relay over HTTP on the
loopback interface, so the application's network security configuration permits cleartext to
`127.0.0.1` and nowhere else:
```xml
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">127.0.0.1</domain>
</domain-config>
</network-security-config>
```
The relay listens on `127.0.0.1` alone, on a port the platform assigns, and answers only paths under
its session's random 128-bit token; anything the sink refuses is answered 502, and the bytes go
nowhere. 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.
`WaiViewAttention` reads shown as: the view's activity started (a split screen or a
picture-in-picture window counts as shown), its window visible and the display interactive; in view
as the share of the view inside its window's visible frame. The platform reports no occlusion of one
application's window by another's, so where windows overlap this states the window's place on the
screen, not what covers it (native-sink §6.2). A host with its own notion of visibility gives a
`WaiAttention` of its own.
## Build and test
Versions, pinned: NDK 29.0.14206865, Android Gradle Plugin 8.13.0, Gradle 8.13 (checked by
SHA-256), Kotlin 2.3.21, JDK 17; `compileSdk` 36, `minSdk` 24; the media player library 1.11.1;
the Rust toolchain of `rust-toolchain.toml` with the targets `aarch64-linux-android`,
`armv7-linux-androideabi` and `x86_64-linux-android`. ABIs: `arm64-v8a`, `armeabi-v7a`, `x86_64`,
each library linked for 16 KiB pages.
```
cd wai/wai-sink-android
scripts/build-native.sh host # libwai and the JNI layer for this machine
scripts/gradle.sh -Pwai.sink.jvmOnly=true :core:test # the unit tests (no Android SDK needed)
scripts/build-native.sh android # every ABI, with the NDK
scripts/gradle.sh :sink:assembleRelease # sink/build/outputs/aar/sink-release.aar
scripts/test-emulator.sh # the instrumented tests, headless
```
The unit tests run the corpus `live-conformance/native-sink/` through the JNI layer, every case and
every refused configuration, and require the `page-parity` bundle byte for byte; run the relay over
the reference package's floor (presented byte for byte; a fragment changed at the origin answered
502 with no bytes on every request; ranges served from what verified; content codings removed; an
unsigned playlist refused where signing is required; nothing answered outside the session's token);
drive the observation loop to its `hidden` and `idle` endings with a clock they advance; and decode
the integer decode's cases (`core/src/testkit/int-cases.json`: a committed vector of each of the
eight integer capabilities, with its set, and a stream of each of the three frame forms, frame by
frame) through `WaiIntDecoder` with the GPU offered and on the
CPU alone, requiring the same bytes, each the output its corpus commits (its SHA-256, each frame's,
or the bytes themselves), and an answer that names
the unit that ran it. The instrumented tests run the same corpus, relay and integer decode checks on
the device (`scripts/test-emulator.sh` stages the cases' files into the test APK), play the floor (AV1 in CMAF)
through the platform's media player library and the relay to its end with a video surface attached,
refuse the changed fragment on every retry the player makes, end a session nobody can see as
`hidden` with its player stopped, and read the attention signals from a real activity.
The emulator runs the host's ABI: on an arm64 host the instrumented tests exercise the `arm64-v8a`
libraries; the `armeabi-v7a` and `x86_64` libraries are built, linked and checked, not run.
## Television
The library runs on a television as on a phone: nothing in it needs a touchscreen, and its
manifest declares `android.hardware.touchscreen` and `android.software.leanback` not required,
so an application that merges it is not made to need either (a `required="true"` elsewhere still
wins the merge). The attention signals count a key from a remote control as the viewer's input,
as they count a touch.
`tv/` is a television host for the sink, written from the platform's public documentation for
television applications (read 2026-11-11): one activity with a launcher entry for the television
launcher (`CATEGORY_LEANBACK_LAUNCHER`, beside the ordinary one), a 320×180 banner with the
application's name in it and a 160×160 icon (`scripts/tv-artwork.py` draws both, byte for byte
the same on every host; an owner replaces them), touchscreen and leanback declared not required,
and every control a focusable view with its focus drawn as a thick outline. With the programme
in focus the centre key pauses and resumes and left and right seek ten seconds; the media keys
play, pause, stop, rewind and fast-forward; Back leaves for the home screen. What it plays comes
in the launching intent:
```
adb shell am start -n science.transaction.wai.tv/wai.sink.tv.TvActivity \
--es url https://media.example/ch/7/v.m3u8 --es channel news-east --es owner <64 hex>
```
```
scripts/gradle.sh :tv:assembleDebug # tv/build/outputs/apk/debug/tv-debug.apk
scripts/check-tv-apk.sh tv/build/outputs/apk/debug/tv-debug.apk # its manifest, read with aapt2
```
CI builds it and reads its manifest. **It has not run on a television.** The emulator here has a
phone image only; a television image is a further SDK component with its own licence, which was
not accepted. With one, an owner makes a television virtual device, installs the debug APK with
`adb install`, and launches it as above; for distribution, the APK is signed with the owner's
key and published through the owner's store account.
## Distribution
A release builds every ABI and publishes the library with maven-publish as
`science.transaction.wai:wai-sink:<version>`, the version of `wai/wai-rs`: attached to the release
as `wai-sink-<version>.aar`, and to the forge's Maven registry once the repository holds the token
the release publishes with (`.github/workflows/wai-release.yml`). The library's manifest is of the
package `science.transaction.wai.sink`; its classes keep the package `wai.sink`, which the JNI
layer's symbols name. From the registry, with its POM, it brings the media player library and its
HTTP presentations module at the version `sink/build.gradle.kts` names:
```kotlin
repositories { maven("https://git.transaction.science/api/packages/Transaction-Science/maven") }
dependencies { implementation("science.transaction.wai:wai-sink:<version>") }
```
Used as a file, the AAR brings no dependency of its own, and an application adds those two itself.
Before either, the release writes the publication to a repository on disk and holds it to
`scripts/check-publication.sh`: its coordinates, its manifest's package, both libraries for each
ABI, and its classes.
```
scripts/gradle.sh :sink:publishReleasePublicationToStagingRepository # sink/build/staging-repo
scripts/check-publication.sh sink/build/staging-repo <version>
```
The library holds no codec: the platform's player decodes, with the platform's codecs.
## WAI's integer capabilities, decoded in the application's process
`WaiIntDecoder` is native-sink §13's decoder (`libwai`'s `sink_decode`, through `wai_int_*`), the
one the Swift package carries: one per process, it finds the device's GPU and runs the start-up
exactness check on it when it opens, then decodes whole payloads and `WAI1` envelopes of the eight
integer capabilities, each answer naming the unit that ran it.
```kotlin
val decoder = WaiIntDecoder.open() // WaiIntDecoder.Gpu.OFF: the CPU alone
val gpu = decoder.gpu() // state, adapter, translator, why
val out = decoder.decode("wai.neural.int_hyper", payload,
mapOf("entropy.json" to entropy, "g_s.whm" to gS, "h_s.whm" to hS))
out.bytes; out.media; out.unit; out.dispatch // RGB8 rows; what they are; gpu or cpu
decoder.close()
```
A stream of an integer frame form (`wai.video.int_motion.frame`, `wai.video.int_hyper.frame`,
`wai.video.int_cond.frame`; native-sink §13.6) is a `WaiIntFrameStream`, opened with its set (and
the set's listing, for a pin in the form `wai.prior.set`); each object received goes to
`decoder.decodeFrame(envelope, stream)` in order, and the answer's `frame` names the frame admitted.
`libwai` is built with `sink_decode` for every ABI. Its GPU path is wgpu's Vulkan backend, which
loads the platform's Vulkan library at run time: a device with no Vulkan, or whose GPU does not
pass the check, decodes on the CPU, and every answer says so (`dispatch_unit_absent`,
`dispatch_unit_inexact`). The same bytes come out either way.
**It has not run on a device's Vulkan GPU.** The emulator here has no GPU of its own: its Vulkan
driver forwards to the host's software renderer, and on 2026-11-11 (emulator 36.3.10, image
android-36.1, arm64) that driver crashed the test process with a SIGSEGV inside its own
`vkQueueSubmit` at the decoder's first submission, during discovery, which no process survives. So
`scripts/test-emulator.sh` runs the emulator with the guest's Vulkan off
(`WAI_SINK_EMULATOR_VULKAN=on` keeps it, and reproduces the crash); the decoder then finds no
adapter, states the GPU `absent`, and decoded every case on the CPU to the CPU's bytes and the
committed outputs. The start-up check and the decodes on a phone's or a television's Vulkan GPU
wait for a device. Until a run on one, nothing is claimed for that path on this platform; and a
device whose Vulkan driver fails the same way would end the application's process when the decoder
opens with the GPU offered, which `WaiIntDecoder.open(WaiIntDecoder.Gpu.OFF)` avoids.
## Energy
The platform's player decodes in a process of the platform's, 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. An emulator's figures are the host
machine's, not a device's, and none is stated.