NetDesk 1.5.0 — source release

This commit is contained in:
NetDesk committed 2026-10-18 02:02:46 +00:00
commit 40318aafdd
227 files changed
+80798

No files matched your search

+180
View File
@@ -0,0 +1,180 @@
# Building NetDesk for Android
NetDesk's Android app is a Flutter UI (`ui/`) bound, through
[flutter_rust_bridge](https://cjycode.com/flutter_rust_bridge/) (FRB v2), to a
compiled Rust library. The Rust FFI layer is the **`netdesk-frb`** crate
(`frb/`), a `cdylib` that statically links **`netdesk-core`** (signaling, E2E
crypto, transport, H.264 decode). On Android the library ships inside the
APK/AAB as `lib/<abi>/libnetdesk_frb.so` and the Dart side `dlopen`s it (the FRB
loader looks for the stem `netdesk_frb`).
```
netdesk-frb (+ netdesk-core) --cargo-ndk--> ui/android/app/src/main/jniLibs/<abi>/libnetdesk_frb.so
Flutter (ui/) --gradle-----> release .apk / split .apk / .aab (signed)
```
---
## The one command
From the repo root (`netdesk-native/`), in a shell whose `PATH` contains the
MinGW tools, cargo, and Flutter:
```powershell
powershell -ExecutionPolicy Bypass -File packaging\build-android.ps1
```
That single command:
1. verifies / auto-provisions the toolchain (cargo-ndk, the rustup Android
targets),
2. detects the Android NDK at `C:\dev\android-ndk`,
3. compiles `netdesk-frb` for **arm64-v8a, armeabi-v7a, x86_64** with cargo-ndk
and drops `libnetdesk_frb.so` straight into
`ui/android/app/src/main/jniLibs/<abi>/`,
4. builds the release **fat APK**, the **per-ABI split APKs**, and the Play
**App Bundle (.aab)**.
Useful switches:
| Switch | Effect |
| --- | --- |
| `-Target apk\|split\|aab\|rust\|all` | Build only that deliverable (default `all`; `rust` = just the `.so`). |
| `-SkipRust` | Reuse the `.so` already in `jniLibs`, build Flutter only. |
| `-Abis arm64-v8a` | Restrict the ABIs (default all three). |
| `-GenerateKeystore` | Create the release keystore + `key.properties` first (see below). |
| `-MinSdk 21` | Android API the `.so` links against (must be `<=` Flutter `minSdk`). |
| `-NdkHome <path>` | Override the NDK location (default `C:\dev\android-ndk`). |
---
## Toolchain
| Component | Version / location used here |
| --- | --- |
| Rust | stable (cargo 1.99) on `PATH` |
| rustup targets | `aarch64-linux-android`, `armv7-linux-androideabi`, `x86_64-linux-android` |
| cargo-ndk | 4.1.2 (`cargo install cargo-ndk`) |
| Android NDK | r26d (`26.3.11579264`) at `C:\dev\android-ndk` (`ANDROID_NDK_HOME`) |
| Android SDK | `%LOCALAPPDATA%\Android\Sdk` |
| Flutter | `C:\dev\flutter\bin\flutter.bat` |
| JDK (keytool + Gradle) | Android Studio JBR (auto-detected) |
| nasm | on `PATH` (needed by `openh264` for the x86_64 ABI) |
One-time setup (the build script also does the first three automatically):
```powershell
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android
cargo install cargo-ndk
# NDK: unzip the Android NDK r26+ to C:\dev\android-ndk (set ANDROID_NDK_HOME)
```
Recommended `PATH` (Git Bash form):
```bash
export PATH="/c/mingw-netdesk/mingw64/bin:$HOME/.cargo/bin:/c/dev/flutter/bin:$PATH"
```
### What cargo-ndk runs
The Rust step is exactly:
```
cargo ndk -t arm64-v8a -t armeabi-v7a -t x86_64 \
-o ui/android/app/src/main/jniLibs --platform 21 \
build --release --package netdesk-frb --lib
```
cargo-ndk sets the per-ABI clang/linker from the NDK, and the `-o` flag copies
each produced `libnetdesk_frb.so` into `jniLibs/<abi>/`. The `.so` are build
artifacts (git-ignored) and are regenerated on every run.
---
## Release signing
Signing credentials live in `ui/android/key.properties`, which is **git-ignored**
(so are `*.keystore` / `*.jks`). A committable template is
`ui/android/key.properties.example`.
`app/build.gradle.kts` loads `key.properties` when present and signs the release
build with it; when it is absent the release build falls back to **debug**
signing so `flutter run --release` still works during development.
Create the upload keystore once (the password is read from the environment and
is **never** written into a committed file):
```powershell
$env:NETDESK_KEYSTORE_PASSWORD = '<your-strong-password>'
powershell -ExecutionPolicy Bypass -File packaging\build-android.ps1 -GenerateKeystore
```
This writes:
* `ui/android/app/netdesk-release.keystore` - RSA 4096, alias `netdesk`, 10000-day validity,
* `ui/android/key.properties` - with the alias, store file, and the password you
supplied (local only; keep it out of version control).
> Back up the keystore and remember the password. If you lose either you can no
> longer push updates to an app already published under that key (unless it uses
> Play App Signing with a separate upload key you can reset).
---
## Outputs
After a full build:
| Deliverable | Path (under `ui/`) |
| --- | --- |
| Fat APK (all ABIs) | `build/app/outputs/flutter-apk/app-release.apk` |
| Split APKs (per ABI) | `build/app/outputs/flutter-apk/app-arm64-v8a-release.apk`, `-armeabi-v7a-`, `-x86_64-` |
| Play App Bundle | `build/app/outputs/bundle/release/app-release.aab` |
The fat APK is convenient for side-loading / direct download; the split APKs are
smaller per device; the `.aab` is what Google Play requires.
---
## Publishing to Google Play (AAB)
1. Produce the signed bundle:
```powershell
powershell -ExecutionPolicy Bypass -File packaging\build-android.ps1 -Target aab
```
(or run the default `all` target). The bundle is
`ui/build/app/outputs/bundle/release/app-release.aab`.
2. In the [Play Console](https://play.google.com/console), create the app, then
upload the `.aab` under a release track (internal testing first).
3. Enrol in **Play App Signing** (recommended). Google then holds the *app
signing key*; the keystore above becomes your *upload key*. If the upload key
is ever compromised you can ask Google to reset it - which is why App Signing
is preferred over shipping the raw signing key.
4. `versionCode` / `versionName` come from `ui/pubspec.yaml`
(`version: x.y.z+build`). Bump the `+build` number for every Play upload - the
Console rejects a duplicate `versionCode`.
---
## Troubleshooting
* **"Android NDK not found"** - the script could not find a complete NDK at
`C:\dev\android-ndk` (it checks for `source.properties` +
`toolchains\llvm\prebuilt`). Finish extracting the NDK, set `ANDROID_NDK_HOME`,
or pass `-NdkHome <path>`. Use `-SkipRust` to build only the Flutter layer
against an already-built `.so`.
* **x86_64 ABI fails in `openh264`** - that ABI needs `nasm` on `PATH`. arm64-v8a
and armeabi-v7a do not. Drop x86_64 with `-Abis arm64-v8a,armeabi-v7a` if you
only ship to real devices.
* **Gradle wants to download an NDK** - `app/build.gradle.kts` points AGP at
`ANDROID_NDK_HOME` when it is set, so the prebuilt (already stripped) `.so` are
packaged without a second NDK download. Make sure the env var is set (the build
script sets it).
* **`UnsatisfiedLinkError: libnetdesk_frb.so`** at runtime - the `.so` for the
device's ABI is missing from `jniLibs`. Re-run the Rust step (`-Target rust`)
for that ABI, or confirm the FRB loader stem is still `netdesk_frb`.
+288
View File
@@ -0,0 +1,288 @@
# NetDesk — iOS build (`.ipa`)
How to build the signed NetDesk iOS app — the Flutter UI on top of the compiled
Rust `netdesk-core` engine — and ship it to TestFlight / the App Store.
> **This only builds on a Mac.** iOS toolchains (Xcode, the iOS SDKs,
> `xcodebuild`, code signing) exist on macOS only. Nothing here can run on the
> Windows dev box — Windows is used to *author* these scripts, the Mac runs
> them. Everything below assumes macOS + Xcode.
---
## 1. What gets built
```
core crate (netdesk-core, lib netdesk_core)
│ cargo build --release --target aarch64-apple-ios → libnetdesk_core.a (device)
│ cargo build --release --target aarch64-apple-ios-sim → libnetdesk_core.a (simulator)
▼
NetDeskCore.xcframework (xcodebuild -create-xcframework, both slices)
│ vendored via ui/ios/netdesk_core.podspec + pod 'netdesk_core' in the Podfile
▼
Flutter iOS Runner ── flutter build ipa ──► ui/build/ios/ipa/netdesk.ipa
```
The Rust static library is the FFI surface the Flutter UI binds to. Today that
is the workspace **`core`** crate (`crate-type = ["lib","staticlib","cdylib"]`,
lib name `netdesk_core` → `libnetdesk_core.a`). When the Phase-3
`flutter_rust_bridge` wrapper crate lands under a different package name (e.g.
`netdesk-frb`), build it with the same script by overriding two variables:
```bash
CORE_CRATE=netdesk-frb LIB_NAME=netdesk_frb packaging/build-ios.sh --team <TEAM_ID>
```
---
## 2. Prerequisites
### 2.1 Apple Developer Program — **$99/year**
A paid **Apple Developer Program** membership (<https://developer.apple.com/programs/>,
US$99/yr) is required to:
- create a **distribution signing certificate**,
- register an **App ID** (bundle identifier) and a **provisioning profile**,
- upload to **App Store Connect / TestFlight**.
The free personal team can only sideload to your own device for 7 days — not
enough to ship. Enroll first; enrollment can take a day or two to clear.
### 2.2 Signing certificate + provisioning profile
You need, inside your developer account:
| Thing | Where | Notes |
|-------|-------|-------|
| **Team ID** | developer.apple.com → Membership | 10 chars, e.g. `ABCDE12345`. You pass this to the script. |
| **App ID / bundle id** | Certificates, IDs & Profiles → Identifiers | Must match the app's `PRODUCT_BUNDLE_IDENTIFIER` (default `space.netdesk.netdesk`, set via `--org`). |
| **Distribution certificate** | Certificates, IDs & Profiles → Certificates → *Apple Distribution* | One per team; Xcode can create it for you. |
| **Provisioning profile** | Certificates, IDs & Profiles → Profiles → *App Store* (or Ad Hoc) | Ties the App ID + certificate + devices. |
**Easiest path — automatic signing.** Open the project once in Xcode, sign in
with your Apple ID, tick *Automatically manage signing*, pick your Team. Xcode
creates the certificate + profile for you and the command-line build reuses
them. `build-ios.sh` writes `DEVELOPMENT_TEAM` + `CODE_SIGN_STYLE = Automatic`
into `ui/ios/Flutter/{Release,Debug}.xcconfig`, so after that one-time Xcode
sign-in the whole build is non-interactive.
**Manual signing** (CI, locked-down teams): download the distribution
certificate (`.p12`) and the provisioning profile (`.mobileprovision`), install
both into the login keychain / `~/Library/MobileDevice/Provisioning Profiles/`,
then set `signingStyle` to `manual` and add a `provisioningProfiles` dict in
`ui/ios/ExportOptions.plist`.
### 2.3 Toolchains on the Mac
```bash
# Xcode + command line tools
xcode-select --install # or install full Xcode from the App Store
sudo xcode-select -s /Applications/Xcode.app
# Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
# Flutter SDK (>= 3.27, per ui/pubspec.yaml) — add its bin/ to PATH
# https://docs.flutter.dev/get-started/install/macos
flutter doctor
# CocoaPods (Flutter will also install pods during the build)
sudo gem install cocoapods
```
> **PATH note.** On Windows the project builds with
> `export PATH="/c/mingw-netdesk/mingw64/bin:$HOME/.cargo/bin:/c/dev/flutter/bin:$PATH"`.
> On the **Mac** that line does not apply — just make sure `cargo`, `rustup`,
> `flutter` and `xcrun` are on PATH (`source $HOME/.cargo/env`, Flutter's
> `bin/` in your shell profile).
---
## 3. Generate the Flutter iOS project (one time)
`ui/` currently has `lib/` + `pubspec.yaml` but **no `ios/` folder** — it was
never generated (it's platform scaffolding, not source). Create it on the Mac:
```bash
cd ui
flutter create --org space.netdesk --project-name netdesk --platforms=ios .
```
This adds `ui/ios/` (the `Runner` Xcode project, `Runner.xcworkspace`,
`Info.plist`, launch assets, the Flutter xcconfig layer) without touching your
`lib/` Dart code. The bundle identifier becomes `<org>.netdesk`, i.e.
`space.netdesk.netdesk` — change `--org` to use your own (e.g.
`--org com.akronlogic` → `com.akronlogic.netdesk`), and register that exact id
as the App ID in your developer account.
> On the Windows box the equivalent invocation (for reference only — it does not
> produce a buildable iOS app on Windows) is:
> `C:\dev\flutter\bin\flutter.bat create --platforms=ios .`
**`build-ios.sh` runs this step for you** if `ui/ios/` does not exist, so you
can skip straight to §5. Run it by hand only if you want to pick the `--org`
yourself before the first build.
---
## 4. The VideoToolbox decode path (why the frameworks matter)
NetDesk's iOS H.264 decode is **hardware** via **VideoToolbox**, implemented in
[`core/src/decode_vt.rs`](../core/src/decode_vt.rs) and gated
`#[cfg(any(target_os = "macos", target_os = "ios"))]`. It feeds Annex-B →
AVCC-repacked access units into a `VTDecompressionSession`, builds the
`CMVideoFormatDescription` from the first keyframe's SPS/PPS, and asks for
32-bit BGRA output (VideoToolbox does the YUV→RGB on the GPU); openh264 is the
software fallback if the session can't start.
That module links three Apple system frameworks directly with
`#[link(kind = "framework")]`:
| Framework | Used for |
|-----------|----------|
| **VideoToolbox** | `VTDecompressionSession*` (the decoder itself) |
| **CoreMedia** | `CMVideoFormatDescription` / `CMSampleBuffer` / `CMBlockBuffer` |
| **CoreVideo** | `CVPixelBuffer*` (the decoded BGRA surface) |
| **CoreFoundation** | `CFDictionary`/`CFString`/`CFRelease` (via `core-foundation-sys`, for the pixel-buffer attributes) |
A **static** Rust library records those `#[link]` directives in its metadata but
does **not** pull the frameworks into the final app link. So the podspec
(`ui/ios/netdesk_core.podspec`) names them explicitly:
```ruby
s.frameworks = 'VideoToolbox', 'CoreMedia', 'CoreVideo', 'CoreFoundation'
```
If you ever see link errors like `Undefined symbol: _VTDecompressionSessionCreate`
or `_CMVideoFormatDescriptionCreateFromH264ParameterSets`, a framework is
missing from that line. At runtime, the log line
`decode: VideoToolbox H.264 (hardware)` confirms the hardware path was selected.
---
## 5. Build the `.ipa` — the exact commands
### One command (recommended)
```bash
# from the repo root, on the Mac:
packaging/build-ios.sh --team ABCDE12345
```
Replace `ABCDE12345` with your 10-character Apple **Team ID**. That script does
every step in §6 and prints the path to the finished `.ipa`. Useful variants:
```bash
packaging/build-ios.sh --team ABCDE12345 --method app-store # default: App Store / TestFlight
packaging/build-ios.sh --team ABCDE12345 --method ad-hoc # ad-hoc distribution
packaging/build-ios.sh --team ABCDE12345 --method development # dev devices
packaging/build-ios.sh --team ABCDE12345 --org com.akronlogic # bundle id com.akronlogic.netdesk (first run only)
packaging/build-ios.sh --skip-ipa # build NetDeskCore.xcframework only, no signing needed
```
Output:
- `ui/build/ios/ipa/netdesk.ipa` — the signed app
- `packaging/dist/ios/NetDeskCore.xcframework` — the Rust core (also staged into `ui/ios/`)
### The same steps by hand
If you'd rather run it step by step (this is exactly what the script does):
```bash
# 0. Rust iOS targets
rustup target add aarch64-apple-ios aarch64-apple-ios-sim
# 1. Build the Rust core static lib for device + simulator
export IPHONEOS_DEPLOYMENT_TARGET=13.0
cargo build --release -p netdesk-core --target aarch64-apple-ios
cargo build --release -p netdesk-core --target aarch64-apple-ios-sim
# 2. Assemble the xcframework (delete any stale one first)
rm -rf packaging/dist/ios/NetDeskCore.xcframework
xcodebuild -create-xcframework \
-library target/aarch64-apple-ios/release/libnetdesk_core.a \
-library target/aarch64-apple-ios-sim/release/libnetdesk_core.a \
-output packaging/dist/ios/NetDeskCore.xcframework
# 3. Generate ui/ios if needed, then wire the xcframework in
cd ui
flutter create --org space.netdesk --project-name netdesk --platforms=ios . # only if ui/ios is missing
flutter pub get
cp -R ../packaging/dist/ios/NetDeskCore.xcframework ios/
cp ../packaging/ios/netdesk_core.podspec ios/netdesk_core.podspec
# ... add `pod 'netdesk_core', :path => '.'` to ios/Podfile (Runner target),
# ... add DEVELOPMENT_TEAM + CODE_SIGN_STYLE=Automatic to ios/Flutter/*.xcconfig,
# ... write ios/ExportOptions.plist with your teamID + method.
cd ios && pod install && cd ..
# 4. Build + sign the ipa
flutter build ipa --release --export-options-plist=ios/ExportOptions.plist
# → ui/build/ios/ipa/netdesk.ipa
```
### Ship it
```bash
# TestFlight / App Store (App Store Connect API key):
xcrun altool --upload-app -f ui/build/ios/ipa/netdesk.ipa -t ios \
--apiKey <KEY_ID> --apiIssuer <ISSUER_ID>
```
Or open `ui/build/ios/archive/Runner.xcarchive` in **Xcode → Window →
Organizer → Distribute App** and follow the GUI.
---
## 6. What `build-ios.sh` automates (summary)
1. **Preflight** — macOS, Xcode, `rustup`/`cargo`, `flutter`, and a Team ID (unless `--skip-ipa`).
2. **`rustup target add`** `aarch64-apple-ios aarch64-apple-ios-sim`.
3. **`cargo build --release`** the core staticlib for both targets (`IPHONEOS_DEPLOYMENT_TARGET=13.0`).
4. **`xcodebuild -create-xcframework`** → `NetDeskCore.xcframework`.
5. **`flutter create`** `ui/ios` if missing, copy the xcframework + podspec in, inject the pod into the Podfile, set the signing team in the Flutter xcconfigs, write `ExportOptions.plist`, `pod install`.
6. **`flutter build ipa`** with your team and export method.
---
## 7. Gotchas
- **Apple-silicon vs Intel Mac.** The simulator slice defaults to
`aarch64-apple-ios-sim` (Apple silicon). On an Intel Mac build the Intel
simulator slice instead: `SIM_TARGET=x86_64-apple-ios packaging/build-ios.sh …`
(and `rustup target add x86_64-apple-ios`). The device slice is always
`aarch64-apple-ios`.
- **First build compiles openh264 from source** (the software-decode fallback).
That needs the Xcode clang/C++ toolchain (present with Xcode) — the first
`cargo build` is slow; later ones are cached.
- **Deployment target.** Keep `IPHONEOS_DEPLOYMENT_TARGET`, the podspec
`platform :ios`, and the Runner's min iOS in sync (all `13.0` here). Mismatches
cause CocoaPods warnings or link errors.
- **Bundle id must match the App ID.** The provisioning profile is tied to a
specific bundle id; if `--org` doesn't match a registered App ID, signing
fails. Register the id in your developer account first.
- **Phase-3 FFI.** This build links the Rust core but the Flutter UI does not
*call* it yet (the Dart bindings are Phase 3: uncomment `flutter_rust_bridge`
+ `ffi` in `ui/pubspec.yaml`, add `core/src/ffi.rs`, generate the header with
`cbindgen`, add `-headers` to the `xcodebuild -create-xcframework` call, and
run `flutter_rust_bridge_codegen generate`). Until then the archive may be
dead-stripped out of the app — harmless; the signed `.ipa` still builds. See
the commented `-force_load` block in `netdesk_core.podspec` to force a link
test before Phase 3.
- **`xcodebuild -create-xcframework` refuses to overwrite** an existing output —
the script `rm -rf`s the old xcframework first; do the same by hand.
---
## 8. Files
| Path | Role |
|------|------|
| `packaging/build-ios.sh` | the end-to-end Mac build script |
| `packaging/ios/netdesk_core.podspec` | CocoaPods spec vendoring the xcframework + system frameworks (copied to `ui/ios/`) |
| `packaging/ios/Podfile` | Flutter iOS Podfile template with the `netdesk_core` pod (used if `ui/ios/Podfile` is absent) |
| `packaging/dist/ios/NetDeskCore.xcframework` | build output — the compiled Rust core |
| `core/src/decode_vt.rs` | the VideoToolbox hardware-decode implementation |
| `docs/cross-platform-decode.md` | the per-platform decode design (VideoToolbox section) |
+189
View File
@@ -0,0 +1,189 @@
# NetDesk — Linux build & packaging
Build the native `netdesk-viewer` for Linux and bundle the two distributions
**NetDesk by Akronlogic** ships on Linux:
| Edition | Artifact | Install? |
|--------------|-----------------------------------|----------------------------------------|
| **AppImage** | `NetDesk-<ver>-x86_64.AppImage` | No — `chmod +x` and run anywhere |
| **.deb** | `netdesk_<ver>_amd64.deb` | Yes — `apt install ./…` (launcher + icon) |
Both are produced by [`packaging/build-linux.sh`](../packaging/build-linux.sh).
Everything it writes lands in **`packaging/dist/linux/`** (git-ignored). This is
the Linux counterpart of the Windows [`packaging/README.md`](../packaging/README.md).
> This target must be **built on Linux** (x86_64). The viewer links the system
> FFmpeg/VAAPI libraries for the Linux hardware-decode path
> (`core/src/decode_vaapi.rs`), which are not present when cross-building from
> Windows or macOS.
---
## 1. Build dependencies
The Linux decode backend pulls in FFmpeg through the `ffmpeg-next` crate, whose
build script (`ffmpeg-sys-next`) locates `libav*` with **pkg-config** and
generates bindings with **libclang**. The GUI (eframe/egui on winit + glow)
needs OpenGL at runtime. Install the toolchain + dev libraries first.
### Debian / Ubuntu
```bash
sudo apt update
sudo apt install -y build-essential clang pkg-config nasm \
libavcodec-dev libavformat-dev libavutil-dev libswscale-dev \
libavfilter-dev libavdevice-dev libswresample-dev \
libva-dev \
libgl1-mesa-dev
```
- `build-essential` → the C/C++ linker (`cc`/`g++`) Rust and `openh264-sys2` need.
- `clang` → libclang for `ffmpeg-sys-next`'s bindgen.
- `pkg-config` → locates the FFmpeg and VAAPI libraries.
- `nasm` → assembler for the bundled `openh264` software-decode fallback.
- `libav*-dev` → FFmpeg ≥ 4 development libraries (the full set matches the
default features of `ffmpeg-next`).
- `libva-dev` → VAAPI headers for the hardware-decode path.
- `libgl1-mesa-dev` → OpenGL for the glow renderer.
### Fedora (FFmpeg dev libs come from RPM Fusion)
```bash
sudo dnf install -y gcc gcc-c++ clang pkgconf-pkg-config nasm \
ffmpeg-devel libva-devel mesa-libGL-devel
```
### Optional — native file-picker
The viewer's **File transfer → Upload…** button uses `rfd`, which is compiled
with **no Linux backend** in `viewer/Cargo.toml` (`default-features = false`).
The build and the app run fine without it, but the picker is a no-op on Linux
until `rfd` is given a backend feature:
- **GTK 3** backend → add `features = ["gtk3"]` to `rfd`, then install
`libgtk-3-dev` (build) / `libgtk-3-0` (runtime).
- **XDG portal** backend (no GTK) → add `features = ["xdg-portal", "tokio"]`;
needs a running `xdg-desktop-portal` at runtime, nothing extra at build.
This is the only reason `libgtk` is ever involved on Linux — it is **not**
required to build or run NetDesk as shipped.
---
## 2. The one command
```bash
export PATH="$HOME/.cargo/bin:$PATH" # ensure cargo/rustup are reachable
packaging/build-linux.sh
```
That runs `cargo build --release -p netdesk-viewer`
(target `x86_64-unknown-linux-gnu`) and then produces **both** the `.deb` and
the AppImage in `packaging/dist/linux/`:
```
packaging/dist/linux/
├── netdesk_0.1.0_amd64.deb ← dpkg package (launcher + hicolor icons)
├── NetDesk-0.1.0-x86_64.AppImage ← portable single file
├── com.akronlogic.netdesk.desktop (generated launcher, shared by both)
├── AppDir/ (AppImage staging)
└── deb/ (.deb staging tree)
```
> The underlying build command on its own:
> ```bash
> cargo build --release -p netdesk-viewer --target x86_64-unknown-linux-gnu
> ```
### Flags
| Flag | Effect |
|-----------------------|----------------------------------------------------|
| `--skip-build` | reuse an existing `target/.../release/netdesk-viewer` |
| `--no-appimage` | build the `.deb` only |
| `--no-deb` | build the AppImage only |
| `--target <triple>` | override the Rust target (default `x86_64-unknown-linux-gnu`) |
| `LINUXDEPLOY=/path` | env: use a local `linuxdeploy` instead of downloading it |
The AppImage step downloads **linuxdeploy** once (cached in
`packaging/dist/.tools/`) and runs it with `APPIMAGE_EXTRACT_AND_RUN=1`, so it
works in containers/CI without FUSE. linuxdeploy `ldd`-scans the binary and
bundles its shared libraries (`libavcodec`, `libavutil`, `libswscale`, …) into
the AppImage; `libGL`, `libX11`/`xcb`, Wayland and `libva` are intentionally
**not** bundled — they are `dlopen()`ed and must come from the host graphics
stack. The `.deb` instead declares those as package `Depends:`.
---
## 3. VAAPI hardware-decode note
NetDesk hardware-decodes the host's H.264 stream on Linux through
**VAAPI via FFmpeg/libavcodec** (`core/src/decode_vaapi.rs`): it opens the
libavcodec H.264 decoder with a VAAPI `hw_device_ctx` on the default DRI render
node (`/dev/dri/renderD128`), decodes on the GPU, pulls the surface to system
memory (NV12), crops to the true display size, and swscales to the packed RGB8
`Frame` the viewer paints — see [`cross-platform-decode.md`](cross-platform-decode.md)
for the full backend matrix.
**Hardware decode is optional at runtime.** `best_decoder()` tries VAAPI first
and **falls back to the openh264 software decoder** if no usable VAAPI device is
present, so headless / GPU-less boxes still work. VAAPI is a build dependency
(the libraries must link) but not a runtime requirement.
To get the hardware path at runtime on the target machine:
1. **Install a VAAPI driver** (the `.deb` lists these as `Recommends`):
- Intel (Broadwell+): `intel-media-va-driver` (iHD)
- Intel (older): `i965-va-driver`
- AMD / others (Mesa): `mesa-va-drivers` (radeonsi)
2. **Verify it:** `vainfo` should list an `H264` decode profile (e.g.
`VAProfileH264High : VAEntrypointVLD`). Install `vainfo` from `libva-utils`.
3. **Render-node access:** the user must be able to read
`/dev/dri/renderD128` — typically by membership of the `render` (or `video`)
group.
4. **Driver override (if needed):** `export LIBVA_DRIVER_NAME=iHD` (or `i965`,
`radeonsi`) when autodetection picks the wrong driver.
Confirm which path is live from the viewer's log:
```
decode: VAAPI H.264 (FFmpeg hardware) ← hardware path selected
VAAPI init failed (...); using openh264 ... ← fell back to software
```
If it fell back, check `vainfo`, the `/dev/dri/renderD128` permissions, and
`LIBVA_DRIVER_NAME`.
---
## 4. Troubleshooting
- **`libavcodec` not found via pkg-config / bindgen errors** — the FFmpeg dev
libraries or `clang` are missing. Re-run the install command in §1. For FFmpeg
in a non-standard prefix, set `PKG_CONFIG_PATH`.
- **`openh264` build fails for want of an assembler** — install `nasm`.
- **AppImage not produced** (linuxdeploy download/FUSE error) — the script warns
and still writes the `.deb`. Supply a local tool with `LINUXDEPLOY=/path`, or
run with `--no-appimage`. `APPIMAGE_EXTRACT_AND_RUN=1` is already exported for
FUSE-less hosts.
- **`.deb` won't install — unmet FFmpeg dependency** — soname package names
differ per release; the control file lists `libavcodec60 | 59 | 58 | ffmpeg`
(and likewise for `libavutil`/`libswscale`). On an unusual release, installing
the distro `ffmpeg` package satisfies the catch-all.
- **Wrong window/taskbar icon on GNOME/Wayland** — the launcher file name and
its `Icon=` key both use the app-id `com.akronlogic.netdesk`, matching the
viewer's `with_app_id(...)`; for the `.deb`, `gtk-update-icon-cache` runs in
`postinst`. Log out/in if the shell cached the old icon.
---
## 5. Test
```bash
vainfo # confirm a VAAPI driver + H264 profile (optional)
cargo test -p netdesk-core # core decode/transport tests
./packaging/dist/linux/NetDesk-*.AppImage # smoke-test the viewer
```
Smoke-test end to end against a live host; the log line
`decode: VAAPI H.264 (FFmpeg hardware)` confirms the hardware path was selected.
+293
View File
@@ -0,0 +1,293 @@
# Building the NetDesk desktop app for macOS
This builds the **same** egui viewer/host-panel binary the Windows editions
ship (crate `netdesk-viewer`) as a signed, notarized **universal** macOS app
(`NetDesk.app`) and a distributable `.dmg`.
- One binary, two roles: the viewer window **and** the host panel, identical to
the Windows build — only the packaging here is Mac-specific.
- **Decode path:** on macOS the host H.264 (Annex-B) stream is decoded on the
Apple video engine through **VideoToolbox** — `core/src/decode_vt.rs`
(`VTDecompressionSession`, 32-bit BGRA output repacked to RGB8), gated
`#[cfg(any(target_os = "macos", target_os = "ios"))]`. It falls back to the
portable openh264 software decoder if the hardware path can't initialize. See
[`cross-platform-decode.md`](./cross-platform-decode.md). No VideoToolbox-
specific flags are needed at build time: the CoreMedia / CoreVideo /
VideoToolbox framework `#[link]`s resolve against the active SDK.
> **Where this runs:** on the Mac (the M4), **not** the Windows dev box. Cross-
> compiling a macOS `.app` from Windows is not supported — codesign, notarytool,
> iconutil, lipo and hdiutil are all macOS-only tools.
The script is [`packaging/build-macos.sh`](../packaging/build-macos.sh). All
outputs land in `packaging/dist/`.
---
## TL;DR — the Mac build command
Once the one-time Apple Developer setup below is done (Developer ID cert in the
keychain + a stored `notarytool` profile named `netdesk`), the whole signed +
notarized build is:
```bash
# on the Mac, from the repo root:
export PATH="$HOME/.cargo/bin:$PATH"
bash packaging/build-macos.sh --notary-profile netdesk
```
That produces, in `packaging/dist/`:
| Artifact | What it is |
|----------------------------------------|---------------------------------------------|
| `NetDesk.app` | signed + notarized + **stapled** universal app |
| `NetDesk-macOS-universal.dmg` | signed + notarized + stapled disk image (ship this) |
Useful flags: `--skip-build` (reuse the per-target binaries), `--no-sign`
(local unsigned test build), `--no-notarize` (sign but skip the Apple round-
trip), `--identity "…"`, `--icon <png>`, `--version <x.y.z>`, `--entitlements
<plist>`. Run `bash packaging/build-macos.sh --help` for the full list.
---
## Prerequisites (on the Mac)
1. **Xcode command-line tools** (gives `clang`, the SDKs, `lipo`, `iconutil`,
`sips`, `hdiutil`, `codesign`, `xcrun`):
```bash
xcode-select --install
```
A full Xcode install from the App Store also works.
2. **Rust** via rustup. The script adds the two Apple-silicon/Intel std targets
for you, but you can pre-add them:
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup target add aarch64-apple-darwin x86_64-apple-darwin
```
3. An **Apple Developer Program** membership (needed only for signing +
notarization — a `--no-sign` build needs none). Set that up once, below.
---
## One-time Apple Developer setup
You need a **Developer ID Application** certificate (this is the identity that
lets an app run on *any* Mac after notarization — distinct from the "Apple
Development" / "Apple Distribution" / Mac App Store certs) and a way for
`notarytool` to authenticate.
### 1. Enrol in the Apple Developer Program
<https://developer.apple.com/programs/> — \$99/yr. You must be the **Account
Holder** (or have the Admin role) to create a Developer ID certificate.
### 2. Create the "Developer ID Application" certificate
Easiest, via Xcode:
1. Xcode → **Settings… → Accounts**, sign in with the Apple ID on the team.
2. Select the team → **Manage Certificates…**
3. Click **+** → **Developer ID Application**.
4. Xcode creates the private key + cert and installs both into your **login
keychain**. (Back up the private key — it cannot be re-downloaded.)
Or manually at <https://developer.apple.com/account/resources/certificates> →
**+** → *Developer ID → Developer ID Application*, upload a CSR made with
**Keychain Access → Certificate Assistant → Request a Certificate from a
Certificate Authority**, then double-click the downloaded `.cer` to install it.
Verify it is present and usable for signing:
```bash
security find-identity -v -p codesigning
# look for a line like:
# 1) ABCD… "Developer ID Application: Akronlogic LLC (TEAMID1234)"
```
The script auto-detects this identity; override with `--identity "Developer ID
Application: Akronlogic LLC (TEAMID1234)"` if you have more than one.
### 3. Find your Team ID
<https://developer.apple.com/account> → **Membership details** → *Team ID* (a
10-character string, also shown in the parentheses of the identity name above).
### 4. Create credentials for notarytool
Two supported options — pick one:
**A. App-specific password (simplest).** At <https://appleid.apple.com> →
*Sign-In and Security → App-Specific Passwords* → **+**, label it e.g.
`netdesk-notarytool`. Then store it once in the keychain as a named profile:
```bash
xcrun notarytool store-credentials netdesk \
--apple-id "you@akronlogic.com" \
--team-id "TEAMID1234" \
--password "abcd-efgh-ijkl-mnop" # the app-specific password
```
`netdesk` is the profile name you pass to `--notary-profile netdesk`.
**B. App Store Connect API key.** Create a key at *App Store Connect → Users and
Access → Integrations → App Store Connect API* (role: *Developer*), download the
`AuthKey_XXXX.p8`, and store it:
```bash
xcrun notarytool store-credentials netdesk \
--key "/path/AuthKey_XXXXXXXX.p8" \
--key-id "XXXXXXXXXX" \
--issuer "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
```
> Instead of a stored profile you can pass credentials inline each run:
> `--apple-id you@akronlogic.com --team-id TEAMID1234` **plus** the app-specific
> password in the environment: `export NETDESK_NOTARY_PASSWORD='abcd-efgh-…'`.
---
## What the script does (step by step)
`packaging/build-macos.sh`:
1. **rustup target add** `aarch64-apple-darwin` `x86_64-apple-darwin`.
2. **cargo build --release -p netdesk-viewer** once per target — producing
`target/aarch64-apple-darwin/release/netdesk-viewer` and the `x86_64` one.
(Cross-building the Intel slice on an Apple-silicon Mac uses the same SDK and
needs no extra tooling.)
3. **lipo -create** the two into one universal Mach-O at
`NetDesk.app/Contents/MacOS/NetDesk` (`lipo -info` prints `arm64 x86_64`).
4. **iconutil** builds `NetDesk.icns` from the largest PNG master it finds
(`assets/icon-1024.png` → `icon-512.png` → `icon.png`, or `--icon <png>`):
`sips` resizes the master into a complete `.iconset` (16→1024 px, @1x/@2x),
then `iconutil -c icns` packs it into `Contents/Resources/NetDesk.icns`.
5. **Assembles `NetDesk.app`** — writes `Contents/Info.plist` with bundle id
**`com.akronlogic.netdesk`**, `CFBundleVersion` / `CFBundleShortVersionString`
= the workspace version (read from `Cargo.toml`, currently **0.1.0**),
`CFBundleExecutable`/`CFBundleIconFile` = `NetDesk`, `LSMinimumSystemVersion`
11.0, `NSHighResolutionCapable`, plus a `PkgInfo` (`APPL????`).
6. **codesign** the executable and the bundle with the Developer ID Application
identity, **`--options runtime`** (hardened runtime, required to notarize) and
**`--timestamp`** (secure timestamp), then `codesign --verify --deep --strict`.
7. **Notarize + staple the `.app`**: `ditto -c -k --keepParent` it into a zip,
`xcrun notarytool submit … --wait`, then `xcrun stapler staple NetDesk.app`
so the ticket travels with the app offline. (Skip with `--no-staple-app`.)
8. **Build the `.dmg`** (`hdiutil create`, UDZO, with an `/Applications`
symlink for drag-install), **sign** it, **notarize** it
(`notarytool submit … --wait`), and **staple** it.
If no Developer ID identity is found (or `--no-sign`), it still produces an
**unsigned** `.app` + `.dmg` and tells you so — handy for a local smoke test.
If signing works but no notarization credentials are present, it signs and
prints exactly how to add them, like the Windows installer script does when
NSIS is missing.
---
## Exact commands (equivalent to what the script runs)
If you want to drive it by hand, this is the full sequence (`VERSION=0.1.0`,
`ID="Developer ID Application: Akronlogic LLC (TEAMID1234)"`):
```bash
export PATH="$HOME/.cargo/bin:$PATH"
# 1-2. targets + release builds
rustup target add aarch64-apple-darwin x86_64-apple-darwin
cargo build --release -p netdesk-viewer --target aarch64-apple-darwin
cargo build --release -p netdesk-viewer --target x86_64-apple-darwin
# 3. universal binary
mkdir -p packaging/dist/NetDesk.app/Contents/MacOS packaging/dist/NetDesk.app/Contents/Resources
lipo -create -output packaging/dist/NetDesk.app/Contents/MacOS/NetDesk \
target/aarch64-apple-darwin/release/netdesk-viewer \
target/x86_64-apple-darwin/release/netdesk-viewer
# 4. icon (ICONSET fully populated 16..1024 as in the script)
iconutil -c icns NetDesk.iconset -o packaging/dist/NetDesk.app/Contents/Resources/NetDesk.icns
# 5. Info.plist (the script writes com.akronlogic.netdesk + version 0.1.0)
# 6. sign (hardened runtime + timestamp)
codesign --force --timestamp --options runtime --sign "$ID" packaging/dist/NetDesk.app/Contents/MacOS/NetDesk
codesign --force --timestamp --options runtime --sign "$ID" packaging/dist/NetDesk.app
codesign --verify --deep --strict --verbose=2 packaging/dist/NetDesk.app
# 7. notarize + staple the app
ditto -c -k --keepParent packaging/dist/NetDesk.app packaging/dist/NetDesk.zip
xcrun notarytool submit packaging/dist/NetDesk.zip --keychain-profile netdesk --wait
xcrun stapler staple packaging/dist/NetDesk.app
# 8. dmg -> sign -> notarize -> staple
hdiutil create -volname NetDesk -srcfolder <stage-with-app+Applications-symlink> \
-fs HFS+ -format UDZO -ov packaging/dist/NetDesk-macOS-universal.dmg
codesign --force --timestamp --sign "$ID" packaging/dist/NetDesk-macOS-universal.dmg
xcrun notarytool submit packaging/dist/NetDesk-macOS-universal.dmg --keychain-profile netdesk --wait
xcrun stapler staple packaging/dist/NetDesk-macOS-universal.dmg
```
The script wraps all of this (with the full `.iconset` generation, the plist,
auto-detection, and graceful fallbacks) — prefer it over the manual steps.
---
## Verify a finished build
```bash
# signature valid + hardened runtime + Developer-ID authority
codesign --verify --deep --strict --verbose=2 packaging/dist/NetDesk.app
codesign -dv --verbose=4 packaging/dist/NetDesk.app 2>&1 | grep -E 'Authority|Timestamp|flags|Identifier'
# Gatekeeper will accept it (Developer ID + notarized)
spctl -a -vvv --type exec packaging/dist/NetDesk.app
# notarization ticket is stapled (works offline)
xcrun stapler validate packaging/dist/NetDesk.app
xcrun stapler validate packaging/dist/NetDesk-macOS-universal.dmg
```
`spctl` should print `source=Notarized Developer ID` and `accepted`.
---
## Runtime permissions (host mode)
Signing/notarization are **not** the macOS capture permissions. When the binary
runs as the **host** and first tries to capture the screen or inject input,
macOS prompts the user (TCC) to grant, under **System Settings → Privacy &
Security**:
- **Screen Recording** — to capture the remote screen.
- **Accessibility** — to inject mouse/keyboard input (CGEvent).
These are user-granted at runtime and are **not** code-signing entitlements, so
the default build needs no entitlements file. (If a future host feature drives
*other* apps via Apple Events, add an entitlements plist with the matching keys
and pass it via `--entitlements`.)
---
## Troubleshooting
- **`no identity found` / unsigned output** — the Developer ID Application cert
(and its private key) must be in the login keychain of the user running the
build; confirm with `security find-identity -v -p codesigning`.
- **Notarization `Invalid` / rejected** — fetch the log:
`xcrun notarytool log <submission-id> --keychain-profile netdesk`. Almost
always a missing `--options runtime` (hardened runtime) or a missing secure
`--timestamp`; the script sets both.
- **`The binary is not signed with a valid Developer ID certificate`** — an
*Apple Development* cert was used instead of *Developer ID Application*; only
the latter notarizes for distribution outside the App Store.
- **openh264 fails to cross-compile the x86_64 slice** — it builds C++ via the
`cc` crate; the Xcode CLT clang handles `-arch x86_64` on an arm Mac, so a
full `xcode-select --install` (or Xcode) is enough. If it still fails, build
only the native slice with `--target aarch64-apple-darwin` and drop the lipo
step (the app is then arm64-only).
- **Icon looks soft** — add a 1024×1024 `assets/icon-1024.png` master (the repo
currently ships 256 and 512); the script prefers the largest it finds.
+223
View File
@@ -0,0 +1,223 @@
# Cross-platform hardware H.264 decode
NetDesk's core decodes the host's H.264 (Annex-B) stream into packed-RGB8
[`Frame`]s for the UI to paint. Every platform shares one trait and one output
type, so the viewer never knows which backend is running:
```rust
pub struct Frame { pub w: usize, pub h: usize, pub rgb: Vec<u8> } // w*h*3 bytes
pub trait VideoDecoder { fn decode(&mut self, au: &[u8]) -> Result<Option<Frame>>; }
pub fn best_decoder() -> Box<dyn VideoDecoder + Send>;
```
`best_decoder()` selects the fastest decoder for the current OS at runtime and
**always** falls back to the portable openh264 software decoder if the native
hardware path cannot initialize (no GPU, missing runtime, headless box, etc.).
| Target | Backend | Module | API | Crate(s) |
|---------------|----------------|---------------------------|-----------------------------|---------------------------------|
| Windows | Media Foundation (DXVA2) | `core/src/decode_mf.rs` | `IMFTransform` (MSH264 MFT) | `windows` |
| macOS / iOS | VideoToolbox | `core/src/decode_vt.rs` | `VTDecompressionSession` | `core-foundation(-sys)` + frameworks |
| Android | MediaCodec | `core/src/decode_mediacodec.rs` | `AMediaCodec` (NDK) | `ndk-sys` / `libmediandk` |
| Linux | VAAPI | `core/src/decode_vaapi.rs` | FFmpeg/libavcodec + VAAPI | `ffmpeg-next` |
| All (fallback)| openh264 | `core/src/decode.rs` | software | `openh264` |
Each native module is `#[cfg]`-gated to its own OS, so building for one target
never compiles (or even links) another target's module or its dependencies. The
per-platform crates are declared under `[target.'cfg(...)'.dependencies]` in
`core/Cargo.toml`; on a non-matching host they are only added to `Cargo.lock`
(resolved), never built, and their build scripts never run.
All backends emit the **true display dimensions** (e.g. 1920×1080), cropping out
any codec padding / stride / alignment, so frames are never stretched.
---
## Windows — Media Foundation (reference, builds here)
Already shipping. Build with the portable MinGW toolchain:
```bash
export PATH="/c/mingw-netdesk/mingw64/bin:$HOME/.cargo/bin:$PATH"
cargo build -p netdesk-core # or: --target x86_64-pc-windows-gnu
```
### Dimension fix (2026-10)
The MS H.264 decoder MFT reports the **coded** size in `MF_MT_FRAME_SIZE`, which
is padded up to the codec's macroblock alignment (1080p → a 1088+ coded height;
other resolutions pad larger — this is what produced the wrong `1920x18xx` in the
stats). The real picture rectangle is carried separately in a video-aperture
blob. `decode_mf.rs` now:
1. Reads `MF_MT_FRAME_SIZE` into `coded_w`/`coded_h` — used **only** for the NV12
plane layout (stride × plane height, and the UV plane offset).
2. Reads the display rectangle from, in priority order,
`MF_MT_MINIMUM_DISPLAY_APERTURE` → `MF_MT_GEOMETRIC_APERTURE` →
`MF_MT_PAN_SCAN_APERTURE` (the last only when `MF_MT_PAN_SCAN_ENABLED`), as an
`MFVideoArea` blob giving offset `(x, y)` and visible `width`×`height`. The
aperture is clamped inside the coded frame so a malformed blob can't over-read.
3. `nv12_to_rgb(..)` copies only the visible `width`×`height` region at
`(crop_x, crop_y)` out of the padded NV12 (system-memory and DXGI/D3D11
staging paths both), so `Frame.w`/`Frame.h` are the true `1920×1080`.
When no aperture is present the whole coded frame is treated as the picture
(unchanged behaviour).
---
## macOS / iOS — VideoToolbox
`decode_vt.rs`, gated `#[cfg(any(target_os = "macos", target_os = "ios"))]`.
Feeds AUs into a `VTDecompressionSession` on the platform video engine and
requests 32-bit BGRA output (VideoToolbox does YUV→RGB on the GPU), which is
repacked to RGB8. The session is built lazily from the first keyframe's SPS/PPS
(`CMVideoFormatDescriptionCreateFromH264ParameterSets`); Annex-B is repacked to
AVCC for the `CMSampleBuffer`. `CVPixelBufferGetWidth/Height` already return the
cropped display size, so no manual crop is needed.
Links the system **CoreMedia**, **CoreVideo** and **VideoToolbox** frameworks
directly; `core-foundation` is used for the pixel-buffer attribute dictionary.
### Build (requires macOS + Xcode)
```bash
xcode-select --install # or a full Xcode install
rustup target add aarch64-apple-darwin x86_64-apple-darwin # macOS
rustup target add aarch64-apple-ios aarch64-apple-ios-sim # iOS
cargo build -p netdesk-core --target aarch64-apple-darwin # Apple-silicon mac
cargo build -p netdesk-core --target aarch64-apple-ios # device
cargo build -p netdesk-core --target aarch64-apple-ios-sim # simulator
```
The framework `#[link]`s resolve against the SDK the active toolchain selects
(`xcrun --sdk macosx|iphoneos|iphonesimulator --show-sdk-path`). No extra flags.
### Test
```bash
cargo test -p netdesk-core --target aarch64-apple-darwin
```
Smoke-test end to end by running the viewer against a live host; the log line
`decode: VideoToolbox H.264 (hardware)` confirms the path was selected.
---
## Android — MediaCodec
`decode_mediacodec.rs`, gated `#[cfg(target_os = "android")]`.
Drives a byte-buffer-mode `AMediaCodec` AVC decoder (the SoC hardware decoder via
OMX/Codec2). The codec is configured lazily from the first keyframe's SPS/PPS as
`csd-0`/`csd-1`; each `decode` queues the Annex-B AU as one input buffer and
dequeues one output buffer. Output is YUV 4:2:0 — NV12 semi-planar or I420
planar depending on the device — converted to RGB8 honouring the format's
`stride`, `slice-height`, `color-format`, and `crop-left/top/right/bottom` keys,
so the emitted frame is the true display size.
Talks to the stable NDK Media C ABI in **`libmediandk`** directly (the same ABI
the `ndk-sys` crate binds), so it is version-independent.
### Build (requires the Android NDK)
Install the NDK (via Android Studio's SDK Manager or `sdkmanager "ndk;27.0.x"`),
then use [`cargo-ndk`]:
```bash
rustup target add aarch64-linux-android armv7-linux-androideabi \
x86_64-linux-android i686-linux-android
cargo install cargo-ndk
export ANDROID_NDK_HOME=$HOME/Android/Sdk/ndk/27.0.12077973
# API 24+; links libmediandk / liblog automatically via the NDK sysroot.
cargo ndk -t arm64-v8a -t armeabi-v7a -p 24 build -p netdesk-core --release
```
Or without cargo-ndk, point Cargo at the NDK clang linker for each target, e.g.:
```bash
export CC_aarch64_linux_android=$ANDROID_NDK_HOME/.../aarch64-linux-android24-clang
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=$CC_aarch64_linux_android
cargo build -p netdesk-core --target aarch64-linux-android --release
```
### Test
Run on a device/emulator (the `mediandk` APIs are not present on the host):
```bash
cargo ndk -t arm64-v8a build -p netdesk-core --tests
# push the test binary with `adb push` and run it on-device, or exercise it
# through the app's instrumented tests.
```
The log line `decode: MediaCodec H.264 (hardware)` confirms selection.
---
## Linux — VAAPI (via FFmpeg)
`decode_vaapi.rs`, gated `#[cfg(target_os = "linux")]`.
Opens the libavcodec H.264 decoder with a VAAPI `hw_device_ctx` (default DRI
render node). libavcodec does all bitstream parsing; decode runs on the GPU, the
surface is pulled to system memory with `av_hwframe_transfer_data` (NV12),
cropped to the display rectangle with `av_frame_apply_cropping`, and converted to
RGB8 with swscale. If no VAAPI device is usable, `new()` errors and the core
falls back to openh264, so GPU-less / headless boxes still work.
Pulls libav* in through the **`ffmpeg-next`** crate and needs the system FFmpeg
(≥ 4) development libraries and a VAAPI driver at build/run time.
### Build (requires system FFmpeg + VAAPI dev packages)
Debian/Ubuntu:
```bash
sudo apt install -y clang pkg-config \
libavcodec-dev libavutil-dev libswscale-dev libavformat-dev \
libva-dev vainfo intel-media-va-driver # or mesa-va-drivers for AMD
cargo build -p netdesk-core # host target
```
Fedora:
```bash
sudo dnf install clang pkgconf-pkg-config \
ffmpeg-devel libva-devel libva-utils mesa-va-drivers
cargo build -p netdesk-core
```
`ffmpeg-sys-next` locates the libraries with `pkg-config`; set
`PKG_CONFIG_PATH` if FFmpeg is in a non-standard prefix. `bindgen` needs
`clang`/`libclang` (the `clang` package above).
### Test
```bash
vainfo # confirm a VAAPI driver + H264 profile
cargo test -p netdesk-core
```
At runtime the log line `decode: VAAPI H.264 (FFmpeg hardware)` confirms the
hardware path; `VAAPI init failed (...); using openh264 software decode` means it
fell back (check `vainfo`, `/dev/dri/renderD128` permissions, and the
`LIBVA_DRIVER_NAME` driver).
---
## Adding another backend
1. Add `core/src/decode_<name>.rs` implementing `VideoDecoder` + `unsafe impl
Send`, with a `new() -> Result<Self>` that errors when the platform can't
support it.
2. Declare the module in `decode.rs` behind the right `#[cfg(...)]`.
3. Add a `#[cfg(...)]` branch in `best_decoder()` before the openh264 fallback.
4. Add the deps under a matching `[target.'cfg(...)'.dependencies]` in
`core/Cargo.toml`.
5. Emit the **true display size** (crop any codec padding) and BT.601 limited-
range RGB to match the other backends.