The first wai-v* tag after 0833173ed would have failed the Android leg's publication and the
images job without the secret WAI_PACKAGES_TOKEN, and left the images job unclaimed with no
runner labelled oci, which keeps the run open. Both now run only once the setup exists, and fail
as before once they run:
- A job setup (gate pool, no checkout) reports whether WAI_PACKAGES_TOKEN is set as an output,
never its value, since a job's `if:` cannot read a secret; each missing piece of the setup is
one notice line, and it succeeds.
- The Maven publication step tests the mapped secret: without it, one notice and success. The
AAR is still built, checked and attached.
- The images job needs version and setup, with
`if: needs.version.result == 'success' && needs.setup.outputs.packages == 'true' &&
vars.WAI_OCI_RUNNER == 'true'`. Forgejo 16.0.6 evaluates a blocked job's `if:` on the server
once its needs are done (services/actions/job_emitter.go, prepareJobForEmitting), with the
`vars` context and the needs' results and outputs (runner v12.13.2's jobparser,
newJobIfInterpreter); a false one marks the job skipped, so it is never queued for a runner.
Only `env` and `secrets` there fall back to the runner, and neither is used.
The workflow's header gives the owner's steps in order: the secret, then an oci runner and the
variable WAI_OCI_RUNNER=true.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
8.6 KiB
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 |
scripts/check-publication.sh |
the publication maven-publish writes, held to its coordinates and contents before a release publishes it |
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 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:
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.
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.