open-standards/wai/wai-sink-android
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Transaction Science c6abab6581 wai-sink-android: read a tool's output whole before searching it (#488)
The AAR check failed in CI with "the AAR has no jni/arm64-v8a/libwai.so"
although the libraries were built and packed: under pipefail,
`unzip -l | grep -q` fails whenever grep stops at its first match before
unzip has written the rest (SIGPIPE). On the AAR the Studio built, that
pipeline fails 7 times in 300, and the listing read first 0 in 300. The
workflow's AAR check reads the listing once and prints it on a failure;
build-native.sh reads nm's and readelf's output whole too, where the same
race could report an export or a NEEDED entry missing, or let a LOAD
segment's misalignment through.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 18:43:13 -04:00
..
core wai-sink-android: a missing refusal fails the status test by name 2026-10-26 09:07:30 -04:00
jni wai-sink-android: the native sink's Kotlin library over the C interface 2026-10-26 08:59:13 -04:00
scripts wai-sink-android: read a tool's output whole before searching it (#488) 2026-10-26 18:43:13 -04:00
sink wai-sink-android: the native sink's Kotlin library over the C interface 2026-10-26 08:59:13 -04:00
.gitignore wai-sink-android: the native sink's Kotlin library over the C interface 2026-10-26 08:59:13 -04:00
gradle.properties wai-sink-android: the native sink's Kotlin library over the C interface 2026-10-26 08:59:13 -04:00
README.md wai-sink-android: the native sink's Kotlin library over the C interface 2026-10-26 08:59:13 -04:00
settings.gradle.kts wai-sink-android: the native sink's Kotlin library over the C interface 2026-10-26 08:59:13 -04:00

wai-sink-android — the native sink for the platform player, in Kotlin

The native sink of 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), 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
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

Use

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:

<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); and drive the observation loop to its hidden and idle endings with a clock they advance. The instrumented tests run the same corpus and relay checks on the device, 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.

Distribution

A release builds every ABI and assembles sink-release.aar. Used as a file, an AAR brings no dependency of its own: an application that adds it adds the media player library and its HTTP presentations module at the version sink/build.gradle.kts names. Published to a repository with its POM, it brings them itself; nothing here publishes. The library holds no codec: the platform's player decodes, with the platform's codecs.

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.