Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions .github/workflows/ci-unienc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,26 @@ on:
paths:
- '.github/workflows/ci-unienc.yml'
- 'InstantReplay.Externals/unienc/**'
# Only the trunk, because a branch with an open pull request is already
# covered by the `pull_request` trigger and an unrestricted `push` runs the
# whole matrix a second time for the same commit. That cost nothing when this
# workflow was one `cargo fmt --check`; it is a duplicate of every job below.
push:
branches:
- main
paths:
- '.github/workflows/ci-unienc.yml'
- 'InstantReplay.Externals/unienc/**'

# A new push to a branch makes the run already in flight for it pointless, and
# these jobs are long enough — a simulator boot, an emulator boot, two macOS
# runners — that leaving it to finish holds runners for a result nobody will
# read. Runs on the trunk are left alone: each of those is the record for one
# merged commit, so cancelling one loses the only result that commit ever gets.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

jobs:
fmt:
name: Format
Expand Down Expand Up @@ -81,3 +96,114 @@ jobs:
path: InstantReplay.Externals/unienc/target/tmp/*.mp4
if-no-files-found: warn
retention-days: 7

test-ios-simulator:
name: Test (iOS simulator)
runs-on: macos-15
timeout-minutes: 45
env:
RUST_BACKTRACE: 1
defaults:
run:
working-directory: InstantReplay.Externals/unienc
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: 'recursive'
- run: rustup default stable
# A Rust test binary is a plain executable, so simctl runs it directly and
# no signing identity is needed. VideoToolbox does encode H.264 in the
# simulator, so this covers the encoder logic and not just the build.
- name: Run tests in the simulator
run: scripts/ios-simulator-test.sh
- name: Upload muxed output
if: always()
uses: actions/upload-artifact@v4
with:
name: e2e-output-ios-simulator
path: InstantReplay.Externals/unienc/target/aarch64-apple-ios-sim/tmp/*.mp4
if-no-files-found: warn
retention-days: 7

test-android-emulator:
name: Test (Android emulator)
runs-on: ubuntu-latest
timeout-minutes: 45
env:
RUST_BACKTRACE: 1
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: 'recursive'
- run: rustup default stable
- run: rustup target add aarch64-linux-android x86_64-linux-android
- run: cargo install cargo-ndk
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- run: echo "ANDROID_NDK_HOME=$ANDROID_NDK_LATEST_HOME" >> $GITHUB_ENV
# KVM has to be reachable or the emulator falls back to an unusably slow
# software CPU.
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
# The emulator's encoder is software (c2.android.avc.encoder), so this
# covers the MediaCodec and MediaMuxer plumbing rather than any vendor's
# hardware encoder. Those still need a real device.
- name: Run the harness on an emulator
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
arch: x86_64
target: google_apis
script: InstantReplay.Externals/unienc/scripts/android-device-test.sh
- name: Upload muxed output
if: always()
uses: actions/upload-artifact@v4
with:
name: e2e-output-android-emulator
path: InstantReplay.Externals/unienc/target/android-harness/
if-no-files-found: warn
retention-days: 7

test-web:
name: Test (WebAssembly, browser)
# macOS rather than Linux because of the audio. Chrome reaches WebCodecs'
# AAC encoder through `MojoAudioEncoder`, which is gated on the
# `media::kPlatformAudioEncoder` feature; `media/base/media_switches.cc`
# enables that by default only on Windows, macOS and Android, because those
# are the platforms with an OS-level AAC encoder behind it. On Linux the
# harness's audio configuration is refused outright, so the backend's audio
# path — the one that had never worked before #178 — could not be covered
# there at all.
runs-on: macos-15
timeout-minutes: 45
env:
RUST_BACKTRACE: 1
defaults:
run:
working-directory: InstantReplay.Externals/unienc
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: 'recursive'
# build-std needs a nightly toolchain, because the Emscripten target has no
# prebuilt std. This matches build-unienc.yml.
- run: rustup toolchain install nightly --component rust-src
- run: rustup target add wasm32-unknown-emscripten
# `unienc_webcodecs`'s build script shells out to `tsc`. The Linux runner
# image happens to ship TypeScript globally and the macOS one does not, so
# install it rather than depend on what an image includes.
- run: npm install -g typescript
- uses: mymindstorm/setup-emsdk@v14
# The WebCodecs backend drives the browser's own encoders, so a real
# browser is the only thing that can run this, and it has to be Google
# Chrome rather than Chromium: Chromium ships without the proprietary
# codecs and refuses every H.264 profile the harness offers. The runner
# image already carries Chrome at the path `run-in-browser.sh` defaults
# to, so nothing needs installing or pointing at.
- name: Run the harness in a browser
run: scripts/web-browser-test.sh
16 changes: 16 additions & 0 deletions InstantReplay.Externals/unienc/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,18 @@
[target.'cfg(windows)']
rustflags = ["-C", "link-args=/Brepro"]

# A Rust test binary is a plain executable, so the iOS simulator can run one
# directly through simctl. No app bundle, no provisioning profile and no code
# signing are involved, which is what keeps `cargo test --target
# aarch64-apple-ios-sim` usable as an everyday command. A simulator has to be
# booted first; scripts/ios-simulator-test.sh does that.
#
# This covers the encoders' own logic on the Apple platform. It does not cover
# the static library link, where the iOS build differs from macOS most (see the
# mimalloc symbol localization in build-unienc.yml), nor the hardware encoder of
# a real device.
[target.aarch64-apple-ios-sim]
runner = ["xcrun", "simctl", "spawn", "--standalone", "booted"]

[target.x86_64-apple-ios]
runner = ["xcrun", "simctl", "spawn", "--standalone", "booted"]
18 changes: 18 additions & 0 deletions InstantReplay.Externals/unienc/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "unienc_harness_android"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
publish = false

[lib]
crate-type = ["cdylib"]

[dependencies]
jni = "0.21.1"
unienc = { workspace = true }
unienc_testkit = { workspace = true }
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package jp.co.cyberagent.unienc.harness;

/**
* Shim that gives the native harness a JavaVM.
*
* <p>Loading the library is the whole point: {@code System.load} calls
* {@code JNI_OnLoad}, which is where the MediaCodec backend picks up the
* JavaVM it cannot work without. Everything else happens in native code.
*
* <p>Run through {@code app_process} so that no APK is needed; see
* {@code scripts/android-device-test.sh}.
*/
public final class Harness {
private Harness() {}

private static native int run(String outputPath);

public static void main(String[] args) {
if (args.length != 2) {
System.out.println("usage: Harness <library path> <output path>");
System.exit(2);
}

// These two lines separate the ways this can produce no output at all:
// app_process never reaching main, System.load hanging or failing, and
// the native harness itself hanging.
System.out.println("harness: loading " + args[0]);
System.load(args[0]);
System.out.println("harness: loaded, starting");

System.exit(run(args[1]));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//! Android device harness for the shared end-to-end test.
//!
//! The MediaCodec backend needs a `JavaVM`, and the only way to obtain one is to
//! be loaded by a JVM. A bare executable pushed with `adb` therefore cannot run
//! the encoders, however convenient that would be.
//!
//! MediaCodec and MediaMuxer do not need an `Activity` or a `Context` though, so
//! a JVM is the *only* thing missing, and `app_process` provides one from a
//! shell. That is why this is a library loaded by a small Java shim rather than
//! an instrumented test inside an APK: no Gradle project, no packaging, and the
//! same command works against an emulator and a real device.
//!
//! See `scripts/android-device-test.sh`.

use std::ffi::{c_int, c_void};
use std::path::PathBuf;

use jni::JNIEnv;
use jni::objects::{JClass, JString};

use unienc_testkit::E2eConfig;

/// Called by the JVM when the Java shim loads this library.
///
/// Handing the `JavaVM` to `unienc_android_mc` here is what makes the encoders
/// usable at all; without it every call fails with `JavaVM not initialized`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn JNI_OnLoad(vm: *mut c_void, reserved: *mut c_void) -> c_int {
unsafe { unienc::android::set_java_vm(vm as *mut _, reserved) }
}

/// Runs the harness and returns a process exit status: zero when everything the
/// harness checks holds, one when it does not.
///
/// Both the description of a successful output and the reason for a failure go
/// to stdout, which under `app_process` is the shell that invoked it.
#[unsafe(no_mangle)]
pub extern "system" fn Java_jp_co_cyberagent_unienc_harness_Harness_run(
mut env: JNIEnv,
_class: JClass,
output_path: JString,
) -> c_int {
let output_path: String = match env.get_string(&output_path) {
Ok(path) => path.into(),
Err(error) => {
println!("harness: cannot read the output path argument: {error}");
return 1;
}
};

match unienc_testkit::run_and_verify(&E2eConfig::default(), &PathBuf::from(output_path)) {
Ok(description) => {
println!("harness: ok\n{description}");
0
}
Err(message) => {
println!("harness: FAILED\n{message}");
1
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "unienc_harness_web"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
publish = false

[dependencies]
futures = "0.3.31"
unienc_common = { workspace = true }
unienc_testkit = { workspace = true }
Loading
Loading