feat: Android TV app embedding Python server via Chaquopy
Lint & Test / test (push) Successful in 2m10s
Lint & Test / test (push) Successful in 2m10s
Adds a native Android TV application that runs the full LedGrab Python server in-process via Chaquopy. Captures the TV box screen using the MediaProjection API and exposes the existing web UI on the device's local network — users configure via phone/tablet browser. Android (new /android/ module): - Kotlin shell: MainActivity, CaptureService (foreground service), ScreenCapture (MediaProjection + ImageReader), PythonBridge (Chaquopy). - Polished Leanback-themed UI with QR code for easy web UI access. - AGP 8.9 + Chaquopy 17 + Gradle 8.11 (avoids the AGP 8.7 thread-lock bug). - Pre-built pydantic-core wheels for arm64-v8a, x86_64, x86 cross-compiled with maturin + Android NDK, linked against Chaquopy's libpython3.11.so. Python server platform guards: - New utils/platform.py with is_android()/is_windows()/is_linux() helpers. - Guard every top-level import of desktop-only packages (mss, psutil, sounddevice, pyserial, PyAudioWPatch, etc.) with try/except ImportError. - Android-incompatible calls gated with None-checks so the server runs on reduced capabilities on Android (no CPU/RAM metrics, no mss displays). - utils/image_codec.py gains a Pillow fallback for resize + JPEG encode when cv2 is unavailable; all internal cv2.resize callers migrated. - New android_entry.py start_server/stop_server invoked from Kotlin. - get_displays API falls back to best available engine when mss fails. New capture engines: - MediaProjectionEngine: receives RGBA frames pushed from Kotlin through a thread-safe queue; caches last frame for static-screen previews. - ScrcpyClientEngine: optional H.264 streaming via scrcpy-client library (priority 10, overrides the ADB-screencap engine when installed). Frontend: - Tab loaders previously required an apiKey; now correctly treat "auth disabled" as authenticated (Android has no auth by default). - Re-trigger the active tab's loader after loadServerInfo resolves authRequired, since initTabs runs earlier. - Add i18n keys for the demo / mediaprojection / scrcpy_client engines. Docs: - TODO.md: follow-ups for multi-ABI wheel rebuilds, CI pipeline, USB serial LED controllers, root-only capture, perf metrics abstraction. - CLAUDE.md: Android dependency sync policy (pip --exclude doesn't exist). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Cross-compile pydantic-core for Android ARM64.
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Rust toolchain (rustup)
|
||||
# - Android NDK (set ANDROID_NDK_HOME or let this script find it)
|
||||
# - maturin (pip install maturin)
|
||||
# - Python 3.11 (matching Chaquopy's embedded version)
|
||||
#
|
||||
# Output: ../wheels/pydantic_core-*.whl
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
WHEELS_DIR="$SCRIPT_DIR/../wheels"
|
||||
BUILD_DIR="$SCRIPT_DIR/../.build-cache"
|
||||
|
||||
# ── Pydantic-core version (must match pydantic>=2.9.2 requirement) ──
|
||||
PYDANTIC_CORE_VERSION="2.27.2"
|
||||
|
||||
# ── Find Android NDK ────────────────────────────────────────────────
|
||||
if [ -z "${ANDROID_NDK_HOME:-}" ]; then
|
||||
# Try common locations
|
||||
for candidate in \
|
||||
"$HOME/Library/Android/sdk/ndk"/* \
|
||||
"$HOME/Android/Sdk/ndk"/* \
|
||||
"$LOCALAPPDATA/Android/Sdk/ndk"/* \
|
||||
"/usr/local/lib/android/sdk/ndk"/*; do
|
||||
if [ -d "$candidate" ]; then
|
||||
ANDROID_NDK_HOME="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -z "${ANDROID_NDK_HOME:-}" ]; then
|
||||
echo "ERROR: Android NDK not found. Set ANDROID_NDK_HOME or install via Android Studio."
|
||||
exit 1
|
||||
fi
|
||||
echo "Using Android NDK: $ANDROID_NDK_HOME"
|
||||
|
||||
# ── Determine NDK API level and toolchain ───────────────────────────
|
||||
API_LEVEL=24
|
||||
HOST_TAG=""
|
||||
case "$(uname -s)" in
|
||||
Linux*) HOST_TAG="linux-x86_64" ;;
|
||||
Darwin*) HOST_TAG="darwin-x86_64" ;;
|
||||
MINGW*|MSYS*|CYGWIN*) HOST_TAG="windows-x86_64" ;;
|
||||
*) echo "Unsupported host OS"; exit 1 ;;
|
||||
esac
|
||||
|
||||
TOOLCHAIN="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/$HOST_TAG"
|
||||
CC="$TOOLCHAIN/bin/aarch64-linux-android${API_LEVEL}-clang"
|
||||
AR="$TOOLCHAIN/bin/llvm-ar"
|
||||
|
||||
if [ ! -f "$CC" ] && [ ! -f "${CC}.cmd" ]; then
|
||||
echo "ERROR: NDK compiler not found at $CC"
|
||||
echo "Check your NDK installation."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Install Rust Android target ─────────────────────────────────────
|
||||
echo "Adding Rust target aarch64-linux-android..."
|
||||
rustup target add aarch64-linux-android
|
||||
|
||||
# ── Configure Cargo for cross-compilation ───────────────────────────
|
||||
mkdir -p "$BUILD_DIR"
|
||||
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$CC"
|
||||
export CC_aarch64_linux_android="$CC"
|
||||
export AR_aarch64_linux_android="$AR"
|
||||
|
||||
# ── Clone pydantic-core ─────────────────────────────────────────────
|
||||
REPO_DIR="$BUILD_DIR/pydantic-core"
|
||||
if [ -d "$REPO_DIR" ]; then
|
||||
echo "Updating existing pydantic-core checkout..."
|
||||
cd "$REPO_DIR"
|
||||
git fetch --tags
|
||||
git checkout "v$PYDANTIC_CORE_VERSION"
|
||||
else
|
||||
echo "Cloning pydantic-core v$PYDANTIC_CORE_VERSION..."
|
||||
git clone --depth 1 --branch "v$PYDANTIC_CORE_VERSION" \
|
||||
https://github.com/pydantic/pydantic-core.git "$REPO_DIR"
|
||||
cd "$REPO_DIR"
|
||||
fi
|
||||
|
||||
# ── Build with maturin ──────────────────────────────────────────────
|
||||
echo "Building pydantic-core for aarch64-linux-android..."
|
||||
maturin build \
|
||||
--release \
|
||||
--target aarch64-linux-android \
|
||||
--interpreter python3.11 \
|
||||
--out "$WHEELS_DIR"
|
||||
|
||||
echo ""
|
||||
echo "=== Build complete ==="
|
||||
echo "Wheels:"
|
||||
ls -la "$WHEELS_DIR"/pydantic_core-*.whl 2>/dev/null || echo "WARNING: No wheel found!"
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Set up the cross-compilation environment for building Python native
|
||||
# extensions targeting Android ARM64.
|
||||
#
|
||||
# This script:
|
||||
# 1. Verifies Android NDK is installed
|
||||
# 2. Installs the Rust aarch64-linux-android target
|
||||
# 3. Installs maturin (Python wheel builder for Rust extensions)
|
||||
# 4. Runs a quick test compile to verify the toolchain works
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
echo "=== LedGrab Android NDK Setup ==="
|
||||
|
||||
# ── Check prerequisites ─────────────────────────────────────────────
|
||||
|
||||
if ! command -v rustc &>/dev/null; then
|
||||
echo "ERROR: Rust is not installed."
|
||||
echo "Install from: https://rustup.rs/"
|
||||
exit 1
|
||||
fi
|
||||
echo "Rust: $(rustc --version)"
|
||||
|
||||
if ! command -v cargo &>/dev/null; then
|
||||
echo "ERROR: Cargo is not installed."
|
||||
exit 1
|
||||
fi
|
||||
echo "Cargo: $(cargo --version)"
|
||||
|
||||
if ! command -v python3.11 &>/dev/null && ! command -v python3 &>/dev/null; then
|
||||
echo "WARNING: Python 3.11 not found. Needed for maturin builds."
|
||||
fi
|
||||
|
||||
# ── Install Rust target ─────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo "Installing Rust Android target..."
|
||||
rustup target add aarch64-linux-android
|
||||
echo "Installed targets:"
|
||||
rustup target list --installed | grep android
|
||||
|
||||
# ── Install maturin ─────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo "Installing maturin..."
|
||||
pip install maturin 2>/dev/null || pip3 install maturin 2>/dev/null || {
|
||||
echo "WARNING: Could not install maturin. Install manually: pip install maturin"
|
||||
}
|
||||
|
||||
if command -v maturin &>/dev/null; then
|
||||
echo "maturin: $(maturin --version)"
|
||||
fi
|
||||
|
||||
# ── Verify NDK ──────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
if [ -n "${ANDROID_NDK_HOME:-}" ]; then
|
||||
echo "ANDROID_NDK_HOME: $ANDROID_NDK_HOME"
|
||||
else
|
||||
echo "ANDROID_NDK_HOME is not set."
|
||||
echo "Set it to your NDK installation path, e.g.:"
|
||||
echo " export ANDROID_NDK_HOME=\$HOME/Android/Sdk/ndk/26.1.10909125"
|
||||
echo ""
|
||||
echo "Or install NDK via Android Studio:"
|
||||
echo " SDK Manager → SDK Tools → NDK (Side by side)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Setup complete ==="
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Ensure ANDROID_NDK_HOME is set"
|
||||
echo " 2. Run: ./build-pydantic-core.sh"
|
||||
Reference in New Issue
Block a user