mirror of
git://f0xx.org/ac/ac-mobile-android
synced 2026-07-29 02:58:06 +03:00
initial
This commit is contained in:
118
ndk/CMakeLists.txt
Normal file
118
ndk/CMakeLists.txt
Normal file
@@ -0,0 +1,118 @@
|
||||
cmake_minimum_required(VERSION 3.22.1)
|
||||
project(androidcast_codecs C)
|
||||
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
|
||||
set(THIRD_PARTY_ROOT ${CMAKE_SOURCE_DIR}/../third-party)
|
||||
set(NATIVE_LIBS_ROOT ${CMAKE_SOURCE_DIR}/../build/native)
|
||||
set(NDK_BRIDGE ${CMAKE_SOURCE_DIR})
|
||||
|
||||
add_library(androidcast_codecs SHARED
|
||||
${NDK_BRIDGE}/jni/androidcast_codecs.c
|
||||
${NDK_BRIDGE}/jni/libvpx_bridge.c
|
||||
${NDK_BRIDGE}/jni/opus_bridge.c
|
||||
${NDK_BRIDGE}/jni/speex_bridge.c
|
||||
${NDK_BRIDGE}/jni/crash_hook.c
|
||||
)
|
||||
|
||||
target_include_directories(androidcast_codecs PRIVATE
|
||||
${NDK_BRIDGE}/jni
|
||||
)
|
||||
|
||||
# Per-ABI libvpx.a from scripts/build-native-codecs.sh (do not link arm64 into armeabi-v7a).
|
||||
set(LIBVPX_LIB "")
|
||||
if(ANDROID_ABI)
|
||||
set(LIBVPX_PER_ABI "${NATIVE_LIBS_ROOT}/${ANDROID_ABI}/libvpx.a")
|
||||
if(EXISTS "${LIBVPX_PER_ABI}")
|
||||
set(LIBVPX_LIB "${LIBVPX_PER_ABI}")
|
||||
endif()
|
||||
endif()
|
||||
if(NOT LIBVPX_LIB AND DEFINED ENV{ANDROIDCAST_LIBVPX_LIB})
|
||||
string(FIND "$ENV{ANDROIDCAST_LIBVPX_LIB}" "${ANDROID_ABI}" _abi_match)
|
||||
if(_abi_match GREATER -1 AND EXISTS "$ENV{ANDROIDCAST_LIBVPX_LIB}")
|
||||
set(LIBVPX_LIB "$ENV{ANDROIDCAST_LIBVPX_LIB}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(LIBVPX_LIB)
|
||||
message(STATUS "Linking libvpx for ${ANDROID_ABI}: ${LIBVPX_LIB}")
|
||||
target_compile_definitions(androidcast_codecs PRIVATE ANDROIDCAST_HAVE_LIBVPX=1)
|
||||
target_link_libraries(androidcast_codecs "${LIBVPX_LIB}")
|
||||
if(EXISTS "${THIRD_PARTY_ROOT}/libvpx")
|
||||
target_include_directories(androidcast_codecs PRIVATE "${THIRD_PARTY_ROOT}/libvpx")
|
||||
target_include_directories(androidcast_codecs PRIVATE "${THIRD_PARTY_ROOT}/libvpx/vpx")
|
||||
endif()
|
||||
else()
|
||||
message(STATUS "No libvpx.a for ${ANDROID_ABI} — VPx JNI probe off (build with scripts/build-native-codecs.sh ${ANDROID_ABI})")
|
||||
endif()
|
||||
|
||||
# libopus.a — same autodetect pattern (scripts/build-native-codecs.sh).
|
||||
set(LIBOPUS_LIB "")
|
||||
if(ANDROID_ABI)
|
||||
set(LIBOPUS_PER_ABI "${NATIVE_LIBS_ROOT}/${ANDROID_ABI}/libopus.a")
|
||||
if(EXISTS "${LIBOPUS_PER_ABI}")
|
||||
set(LIBOPUS_LIB "${LIBOPUS_PER_ABI}")
|
||||
endif()
|
||||
endif()
|
||||
if(NOT LIBOPUS_LIB AND DEFINED ENV{ANDROIDCAST_LIBOPUS_LIB})
|
||||
string(FIND "$ENV{ANDROIDCAST_LIBOPUS_LIB}" "${ANDROID_ABI}" _opus_abi_match)
|
||||
if(_opus_abi_match GREATER -1 AND EXISTS "$ENV{ANDROIDCAST_LIBOPUS_LIB}")
|
||||
set(LIBOPUS_LIB "$ENV{ANDROIDCAST_LIBOPUS_LIB}")
|
||||
endif()
|
||||
endif()
|
||||
if(LIBOPUS_LIB)
|
||||
message(STATUS "Linking libopus for ${ANDROID_ABI}: ${LIBOPUS_LIB}")
|
||||
target_compile_definitions(androidcast_codecs PRIVATE ANDROIDCAST_HAVE_OPUS=1)
|
||||
target_link_libraries(androidcast_codecs "${LIBOPUS_LIB}")
|
||||
if(EXISTS "${THIRD_PARTY_ROOT}/opus/include")
|
||||
target_include_directories(androidcast_codecs PRIVATE "${THIRD_PARTY_ROOT}/opus/include")
|
||||
endif()
|
||||
else()
|
||||
message(STATUS "No libopus.a for ${ANDROID_ABI} — Opus JNI probe off (build with scripts/build-native-codecs.sh ${ANDROID_ABI})")
|
||||
endif()
|
||||
|
||||
# libspeex.a
|
||||
set(LIBSPEEX_LIB "")
|
||||
if(ANDROID_ABI)
|
||||
set(LIBSPEEX_PER_ABI "${NATIVE_LIBS_ROOT}/${ANDROID_ABI}/libspeex.a")
|
||||
if(EXISTS "${LIBSPEEX_PER_ABI}")
|
||||
set(LIBSPEEX_LIB "${LIBSPEEX_PER_ABI}")
|
||||
endif()
|
||||
endif()
|
||||
if(NOT LIBSPEEX_LIB AND DEFINED ENV{ANDROIDCAST_LIBSPEEX_LIB})
|
||||
string(FIND "$ENV{ANDROIDCAST_LIBSPEEX_LIB}" "${ANDROID_ABI}" _speex_abi_match)
|
||||
if(_speex_abi_match GREATER -1 AND EXISTS "$ENV{ANDROIDCAST_LIBSPEEX_LIB}")
|
||||
set(LIBSPEEX_LIB "$ENV{ANDROIDCAST_LIBSPEEX_LIB}")
|
||||
endif()
|
||||
endif()
|
||||
if(LIBSPEEX_LIB)
|
||||
message(STATUS "Linking libspeex for ${ANDROID_ABI}: ${LIBSPEEX_LIB}")
|
||||
target_compile_definitions(androidcast_codecs PRIVATE ANDROIDCAST_HAVE_SPEEX=1)
|
||||
target_link_libraries(androidcast_codecs "${LIBSPEEX_LIB}")
|
||||
if(EXISTS "${THIRD_PARTY_ROOT}/speex/include")
|
||||
target_include_directories(androidcast_codecs PRIVATE "${THIRD_PARTY_ROOT}/speex/include")
|
||||
endif()
|
||||
else()
|
||||
message(STATUS "No libspeex.a for ${ANDROID_ABI} — Speex JNI probe off (build with scripts/build-native-codecs.sh ${ANDROID_ABI})")
|
||||
endif()
|
||||
|
||||
find_library(log-lib log)
|
||||
find_library(android-lib android)
|
||||
find_library(dl-lib dl)
|
||||
target_link_libraries(androidcast_codecs ${log-lib} ${android-lib} ${dl-lib})
|
||||
|
||||
# Per-account obfuscated entitlement flags (separate .so; safe to omit on load failure).
|
||||
set(OBFUSCATE_ROOT ${CMAKE_SOURCE_DIR}/obfuscate)
|
||||
|
||||
add_library(androidcast_entitlements SHARED
|
||||
${NDK_BRIDGE}/jni/entitlements_jni.c
|
||||
${OBFUSCATE_ROOT}/obfuscate.c
|
||||
${OBFUSCATE_ROOT}/obfuscate_file.c
|
||||
)
|
||||
|
||||
target_include_directories(androidcast_entitlements PRIVATE
|
||||
${OBFUSCATE_ROOT}
|
||||
)
|
||||
|
||||
target_compile_options(androidcast_entitlements PRIVATE -std=c11 -Wall -Wextra)
|
||||
target_link_libraries(androidcast_entitlements ${log-lib})
|
||||
46
ndk/README.md
Normal file
46
ndk/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Native codecs (libvpx, Opus, Speex)
|
||||
|
||||
The app loads `libandroidcast_codecs.so` for availability probes and future software encode/decode paths.
|
||||
|
||||
## Current status
|
||||
|
||||
| Component | Java | Native | Notes |
|
||||
|-----------|------|--------|-------|
|
||||
| FEC 3/4 + RS | Yes | — | v2 **per-shard** on wire (`FecShardWire`); v1 monolithic decode still accepted |
|
||||
| NACK + retransmit cache | Yes | — | UDP `MSG_NACK`, combined with FEC when enabled |
|
||||
| libvpx VP8/VP9 | Native | Encode + decode | Screen cast via ImageReader; receiver libvpx (decoder uses `VPX_CODEC_USE_ERROR_CONCEALMENT` when supported) or MediaCodec fallback |
|
||||
| Opus / Speex | Probe + static link when `lib*.a` built | Encode/decode JNI sinks | `scripts/build-native-codecs.sh` → CMake autolink |
|
||||
|
||||
## Enable native codecs (developer)
|
||||
|
||||
1. Initialize all third-party submodules:
|
||||
|
||||
```bash
|
||||
bash scripts/init-third-party-submodules.sh
|
||||
```
|
||||
|
||||
2. Build static libs per ABI (NDK auto-detected via `scripts/android-ndk.sh`):
|
||||
|
||||
```bash
|
||||
./scripts/build-native-codecs.sh arm64-v8a
|
||||
./scripts/build-native-codecs.sh armeabi-v7a # if shipped in APK
|
||||
```
|
||||
|
||||
If the repo path has **spaces**, the script stages under `/tmp/androidcast-libvpx-$USER`.
|
||||
|
||||
3. Gradle / CMake link `build/native/<abi>/lib*.a` when present and set `ANDROIDCAST_HAVE_LIBVPX`, `ANDROIDCAST_HAVE_OPUS`, `ANDROIDCAST_HAVE_SPEEX` accordingly.
|
||||
|
||||
4. `./gradlew assembleDebug`
|
||||
|
||||
Optional override: `ANDROIDCAST_LIBVPX_LIB` only applies when its path contains the current ABI name (e.g. `.../arm64-v8a/libvpx.a`).
|
||||
|
||||
ABIs without matching `.a` files still build; missing codecs are disabled on those architectures.
|
||||
|
||||
## Stream protection defaults
|
||||
|
||||
- Default: **None** (no FEC/NACK)
|
||||
- Enable in **Global settings → Stream protection (UDP)**
|
||||
- After handshake, sender and receiver negotiate the effective mode (no protocol version bump)
|
||||
- Until negotiation completes, UDP protection stays **off**; peers without FEC support **ignore** protected frames
|
||||
|
||||
Full checklist: [docs/ROADMAP.md](../docs/ROADMAP.md)
|
||||
23
ndk/jni/androidcast_codecs.c
Normal file
23
ndk/jni/androidcast_codecs.c
Normal file
@@ -0,0 +1,23 @@
|
||||
/* package ndk/jni/androidcast_codecs.c */
|
||||
|
||||
/*********************************************************************
|
||||
* androidcast_codecs.c
|
||||
* Created at: Sun 17 May 2026 22:49:49 +0200
|
||||
* Updated at: Wed 20 May 2026 15:17:13 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
|
||||
* Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8
|
||||
* Contributors:
|
||||
* - Anton Afanasyeu <a.afanasieff@gmail.com> (2 commits, 19 lines)
|
||||
* - Cursor Agent (project assistant)
|
||||
* Digest: SHA256 a6e0a5cdb3c06c23f932e9927494a53786a587533964767fab09fce1da5028a8
|
||||
**********************************************************************/
|
||||
#include <jni.h>
|
||||
#include <android/log.h>
|
||||
|
||||
#define LOG_TAG "androidcast_codecs"
|
||||
|
||||
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
|
||||
(void)vm;
|
||||
(void)reserved;
|
||||
__android_log_print(ANDROID_LOG_INFO, LOG_TAG, "androidcast codecs native library loaded");
|
||||
return JNI_VERSION_1_6;
|
||||
}
|
||||
134
ndk/jni/crash_hook.c
Normal file
134
ndk/jni/crash_hook.c
Normal file
@@ -0,0 +1,134 @@
|
||||
/* package ndk/jni/crash_hook.c */
|
||||
|
||||
/*********************************************************************
|
||||
* crash_hook.c
|
||||
* Created at: Wed 20 May 2026 14:31:55 +0200
|
||||
* Updated at: Wed 20 May 2026 15:17:13 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
|
||||
* Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8
|
||||
* Contributors:
|
||||
* - Anton Afanasyeu <a.afanasieff@gmail.com> (2 commits, 130 lines)
|
||||
* - Cursor Agent (project assistant)
|
||||
* Digest: SHA256 3ae2b4ff82863a890f6b5a592d9dd3d93bd28afbf93817a4ecae9e5c2cf03811
|
||||
**********************************************************************/
|
||||
#include <jni.h>
|
||||
#include <android/log.h>
|
||||
#include <dlfcn.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <unwind.h>
|
||||
|
||||
#define LOG_TAG "androidcast_crash"
|
||||
#define REPORT_DIR_MAX 512
|
||||
#define MAX_FRAMES 64
|
||||
|
||||
static char g_report_dir[REPORT_DIR_MAX];
|
||||
static volatile sig_atomic_t g_handling;
|
||||
|
||||
typedef struct {
|
||||
void **frames;
|
||||
int max;
|
||||
int count;
|
||||
} backtrace_state_t;
|
||||
|
||||
static _Unwind_Reason_Code unwind_callback(struct _Unwind_Context *context, void *arg) {
|
||||
backtrace_state_t *state = (backtrace_state_t *) arg;
|
||||
uintptr_t pc = _Unwind_GetIP(context);
|
||||
if (pc == 0) {
|
||||
return _URC_NO_REASON;
|
||||
}
|
||||
if (state->count < state->max) {
|
||||
state->frames[state->count++] = (void *) pc;
|
||||
}
|
||||
return _URC_NO_REASON;
|
||||
}
|
||||
|
||||
static int capture_backtrace(void **frames, int max) {
|
||||
backtrace_state_t state = {frames, max, 0};
|
||||
_Unwind_Backtrace(unwind_callback, &state);
|
||||
return state.count;
|
||||
}
|
||||
|
||||
static void write_native_report(const char *signal_name) {
|
||||
if (g_report_dir[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
char path[REPORT_DIR_MAX + 64];
|
||||
snprintf(path, sizeof(path), "%s/native_%d_%ld.txt",
|
||||
g_report_dir, (int) getpid(), (long) time(NULL));
|
||||
FILE *f = fopen(path, "w");
|
||||
if (!f) {
|
||||
return;
|
||||
}
|
||||
fprintf(f, "signal=%s\n", signal_name);
|
||||
fprintf(f, "pid=%d\n", (int) getpid());
|
||||
void *frames[MAX_FRAMES];
|
||||
int n = capture_backtrace(frames, MAX_FRAMES);
|
||||
for (int i = 0; i < n; i++) {
|
||||
Dl_info info;
|
||||
memset(&info, 0, sizeof(info));
|
||||
if (dladdr(frames[i], &info) && info.dli_fname) {
|
||||
uintptr_t offset = (uintptr_t) frames[i] - (uintptr_t) info.dli_fbase;
|
||||
fprintf(f, "#%02d %p %s", i, frames[i], info.dli_fname);
|
||||
if (info.dli_sname) {
|
||||
fprintf(f, " %s+0x%lx", info.dli_sname,
|
||||
(unsigned long) ((uintptr_t) frames[i] - (uintptr_t) info.dli_saddr));
|
||||
} else {
|
||||
fprintf(f, "+0x%lx", (unsigned long) offset);
|
||||
}
|
||||
fprintf(f, "\n");
|
||||
} else {
|
||||
fprintf(f, "#%02d %p\n", i, frames[i]);
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
static void crash_signal_handler(int sig) {
|
||||
if (g_handling) {
|
||||
_exit(128 + sig);
|
||||
}
|
||||
g_handling = 1;
|
||||
const char *name = "UNKNOWN";
|
||||
switch (sig) {
|
||||
case SIGSEGV: name = "SIGSEGV"; break;
|
||||
case SIGABRT: name = "SIGABRT"; break;
|
||||
case SIGBUS: name = "SIGBUS"; break;
|
||||
case SIGFPE: name = "SIGFPE"; break;
|
||||
case SIGILL: name = "SIGILL"; break;
|
||||
}
|
||||
write_native_report(name);
|
||||
signal(sig, SIG_DFL);
|
||||
raise(sig);
|
||||
}
|
||||
|
||||
static void install_handlers(void) {
|
||||
struct sigaction sa;
|
||||
memset(&sa, 0, sizeof(sa));
|
||||
sa.sa_handler = crash_signal_handler;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sa.sa_flags = SA_RESETHAND;
|
||||
sigaction(SIGSEGV, &sa, NULL);
|
||||
sigaction(SIGABRT, &sa, NULL);
|
||||
sigaction(SIGBUS, &sa, NULL);
|
||||
sigaction(SIGFPE, &sa, NULL);
|
||||
sigaction(SIGILL, &sa, NULL);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_foxx_androidcast_crash_CrashNativeBridge_nativeInstallCrashHook(
|
||||
JNIEnv *env, jclass clazz, jstring reportDir) {
|
||||
(void) clazz;
|
||||
const char *dir = (*env)->GetStringUTFChars(env, reportDir, NULL);
|
||||
if (!dir) {
|
||||
return;
|
||||
}
|
||||
strncpy(g_report_dir, dir, REPORT_DIR_MAX - 1);
|
||||
g_report_dir[REPORT_DIR_MAX - 1] = '\0';
|
||||
(*env)->ReleaseStringUTFChars(env, reportDir, dir);
|
||||
install_handlers();
|
||||
__android_log_print(ANDROID_LOG_INFO, LOG_TAG, "native crash hook installed: %s", g_report_dir);
|
||||
}
|
||||
103
ndk/jni/entitlements_jni.c
Normal file
103
ndk/jni/entitlements_jni.c
Normal file
@@ -0,0 +1,103 @@
|
||||
#include <jni.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "obfuscate.h"
|
||||
#include "obfuscate_file.h"
|
||||
|
||||
static jint throw_illegal_state(JNIEnv *env, const char *msg)
|
||||
{
|
||||
jclass cls = (*env)->FindClass(env, "java/lang/IllegalStateException");
|
||||
if (cls != NULL)
|
||||
(*env)->ThrowNew(env, cls, msg);
|
||||
return OBF_ERR_NULL;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_foxx_androidcast_entitlements_EntitlementNativeBridge_nativeReadFrom(
|
||||
JNIEnv *env, jclass clazz, jstring path, jlong key, jlongArray out_val)
|
||||
{
|
||||
const char *cpath;
|
||||
uint64_t decoded = 0;
|
||||
int32_t rv;
|
||||
jlong slot;
|
||||
|
||||
(void)clazz;
|
||||
if (path == NULL || out_val == NULL)
|
||||
return throw_illegal_state(env, "path or out_val is null");
|
||||
|
||||
cpath = (*env)->GetStringUTFChars(env, path, NULL);
|
||||
if (cpath == NULL)
|
||||
return OBF_ERR_IO;
|
||||
|
||||
rv = read_from(cpath, (uint64_t)key, &decoded);
|
||||
(*env)->ReleaseStringUTFChars(env, path, cpath);
|
||||
if (rv != OBF_OK)
|
||||
return rv;
|
||||
|
||||
slot = (jlong)decoded;
|
||||
(*env)->SetLongArrayRegion(env, out_val, 0, 1, &slot);
|
||||
return OBF_OK;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_foxx_androidcast_entitlements_EntitlementNativeBridge_nativeWriteTo(
|
||||
JNIEnv *env, jclass clazz, jstring path, jlong val, jlong key)
|
||||
{
|
||||
const char *cpath;
|
||||
int32_t rv;
|
||||
|
||||
(void)clazz;
|
||||
if (path == NULL)
|
||||
return throw_illegal_state(env, "path is null");
|
||||
|
||||
cpath = (*env)->GetStringUTFChars(env, path, NULL);
|
||||
if (cpath == NULL)
|
||||
return OBF_ERR_IO;
|
||||
|
||||
rv = write_to(cpath, (uint64_t)val, (uint64_t)key);
|
||||
(*env)->ReleaseStringUTFChars(env, path, cpath);
|
||||
return rv;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_foxx_androidcast_entitlements_EntitlementNativeBridge_nativeDecode(
|
||||
JNIEnv *env, jclass clazz, jlong encoded_val, jlong key, jlongArray out_val)
|
||||
{
|
||||
uint64_t decoded = 0;
|
||||
int32_t rv;
|
||||
jlong slot;
|
||||
|
||||
(void)clazz;
|
||||
if (out_val == NULL)
|
||||
return throw_illegal_state(env, "out_val is null");
|
||||
|
||||
rv = decode((uint64_t)encoded_val, (uint64_t)key, &decoded);
|
||||
if (rv != OBF_OK)
|
||||
return rv;
|
||||
|
||||
slot = (jlong)decoded;
|
||||
(*env)->SetLongArrayRegion(env, out_val, 0, 1, &slot);
|
||||
return OBF_OK;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_foxx_androidcast_entitlements_EntitlementNativeBridge_nativeEncode(
|
||||
JNIEnv *env, jclass clazz, jlong val, jlong key, jlongArray out_encoded)
|
||||
{
|
||||
uint64_t encoded = 0;
|
||||
int32_t rv;
|
||||
jlong slot;
|
||||
|
||||
(void)clazz;
|
||||
if (out_encoded == NULL)
|
||||
return throw_illegal_state(env, "out_encoded is null");
|
||||
|
||||
rv = encode((uint64_t)val, (uint64_t)key, &encoded);
|
||||
if (rv != OBF_OK)
|
||||
return rv;
|
||||
|
||||
slot = (jlong)encoded;
|
||||
(*env)->SetLongArrayRegion(env, out_encoded, 0, 1, &slot);
|
||||
return OBF_OK;
|
||||
}
|
||||
436
ndk/jni/libvpx_bridge.c
Normal file
436
ndk/jni/libvpx_bridge.c
Normal file
@@ -0,0 +1,436 @@
|
||||
/* package ndk/jni/libvpx_bridge.c */
|
||||
|
||||
/*********************************************************************
|
||||
* libvpx_bridge.c
|
||||
* Created at: Sun 17 May 2026 22:49:49 +0200
|
||||
* Updated at: Wed 20 May 2026 15:17:13 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
|
||||
* Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8
|
||||
* Contributors:
|
||||
* - Anton Afanasyeu <a.afanasieff@gmail.com> (6 commits, 432 lines)
|
||||
* - Cursor Agent (project assistant)
|
||||
* Digest: SHA256 3bdffcead822ea7b00582f416ffe18d3f885f8b91ddab3e87752c23dcf326a02
|
||||
**********************************************************************/
|
||||
#include <jni.h>
|
||||
#include <android/native_window.h>
|
||||
#include <android/native_window_jni.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifndef ANDROIDCAST_HAVE_LIBVPX
|
||||
#define ANDROIDCAST_HAVE_LIBVPX 0
|
||||
#endif
|
||||
|
||||
#if ANDROIDCAST_HAVE_LIBVPX
|
||||
#include <vpx/vpx_encoder.h>
|
||||
#include <vpx/vp8cx.h>
|
||||
#include <vpx/vpx_decoder.h>
|
||||
#include <vpx/vp8dx.h>
|
||||
#endif
|
||||
|
||||
#define VPX_ENC_MAGIC 0x56505845L /* VPXE */
|
||||
#define VPX_DEC_MAGIC 0x56505844L /* VPXD */
|
||||
|
||||
#if ANDROIDCAST_HAVE_LIBVPX
|
||||
|
||||
typedef struct {
|
||||
unsigned long magic;
|
||||
vpx_codec_ctx_t codec;
|
||||
vpx_image_t img;
|
||||
int width;
|
||||
int height;
|
||||
int frame_count;
|
||||
int force_keyframe;
|
||||
} VpxEncoderCtx;
|
||||
|
||||
static vpx_codec_iface_t *pick_encoder_iface(const char *mime) {
|
||||
if (mime != NULL && strstr(mime, "VP9") != NULL) {
|
||||
return vpx_codec_vp9_cx();
|
||||
}
|
||||
return vpx_codec_vp8_cx();
|
||||
}
|
||||
|
||||
static vpx_codec_iface_t *pick_decoder_iface(const char *mime) {
|
||||
if (mime != NULL && strstr(mime, "VP9") != NULL) {
|
||||
return vpx_codec_vp9_dx();
|
||||
}
|
||||
return vpx_codec_vp8_dx();
|
||||
}
|
||||
|
||||
static vpx_img_fmt_t img_fmt(void) {
|
||||
return VPX_IMG_FMT_I420;
|
||||
}
|
||||
|
||||
static void copy_i420_to_image(const uint8_t *yuv, vpx_image_t *img, int width, int height) {
|
||||
int y_stride = img->stride[VPX_PLANE_Y];
|
||||
int uv_stride = img->stride[VPX_PLANE_U];
|
||||
const uint8_t *src_y = yuv;
|
||||
const uint8_t *src_u = yuv + width * height;
|
||||
const uint8_t *src_v = src_u + (width / 2) * (height / 2);
|
||||
for (int row = 0; row < height; row++) {
|
||||
memcpy(img->planes[VPX_PLANE_Y] + row * y_stride, src_y + row * width, (size_t) width);
|
||||
}
|
||||
int chroma_h = height / 2;
|
||||
int chroma_w = width / 2;
|
||||
for (int row = 0; row < chroma_h; row++) {
|
||||
memcpy(img->planes[VPX_PLANE_U] + row * uv_stride, src_u + row * chroma_w, (size_t) chroma_w);
|
||||
memcpy(img->planes[VPX_PLANE_V] + row * uv_stride, src_v + row * chroma_w, (size_t) chroma_w);
|
||||
}
|
||||
}
|
||||
|
||||
static VpxEncoderCtx *enc_from_handle(jlong handle) {
|
||||
VpxEncoderCtx *ctx = (VpxEncoderCtx *) (intptr_t) handle;
|
||||
if (ctx == NULL || ctx->magic != VPX_ENC_MAGIC) {
|
||||
return NULL;
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
unsigned long magic;
|
||||
vpx_codec_ctx_t codec;
|
||||
} VpxDecoderCtx;
|
||||
|
||||
static VpxDecoderCtx *dec_from_handle(jlong handle) {
|
||||
VpxDecoderCtx *ctx = (VpxDecoderCtx *) (intptr_t) handle;
|
||||
if (ctx == NULL || ctx->magic != VPX_DEC_MAGIC) {
|
||||
return NULL;
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
static void render_i420_to_window(ANativeWindow *window, const vpx_image_t *img) {
|
||||
if (window == NULL || img == NULL) {
|
||||
return;
|
||||
}
|
||||
const int width = (int) img->d_w;
|
||||
const int height = (int) img->d_h;
|
||||
if (width <= 0 || height <= 0) {
|
||||
return;
|
||||
}
|
||||
ANativeWindow_setBuffersGeometry(window, width, height, WINDOW_FORMAT_RGBA_8888);
|
||||
ANativeWindow_Buffer buffer;
|
||||
if (ANativeWindow_lock(window, &buffer, NULL) != 0) {
|
||||
return;
|
||||
}
|
||||
uint8_t *dst = (uint8_t *) buffer.bits;
|
||||
const int dst_stride = buffer.stride * 4;
|
||||
const uint8_t *y_plane = img->planes[VPX_PLANE_Y];
|
||||
const uint8_t *u_plane = img->planes[VPX_PLANE_U];
|
||||
const uint8_t *v_plane = img->planes[VPX_PLANE_V];
|
||||
const int y_stride = img->stride[VPX_PLANE_Y];
|
||||
const int u_stride = img->stride[VPX_PLANE_U];
|
||||
const int v_stride = img->stride[VPX_PLANE_V];
|
||||
|
||||
for (int row = 0; row < height; row++) {
|
||||
uint8_t *row_dst = dst + row * dst_stride;
|
||||
for (int col = 0; col < width; col++) {
|
||||
int y = y_plane[row * y_stride + col];
|
||||
int u = u_plane[(row / 2) * u_stride + (col / 2)] - 128;
|
||||
int v = v_plane[(row / 2) * v_stride + (col / 2)] - 128;
|
||||
int r = y + ((351 * v) >> 8);
|
||||
int g = y - ((179 * v + 86 * u) >> 8);
|
||||
int b = y + ((443 * u) >> 8);
|
||||
if (r < 0) {
|
||||
r = 0;
|
||||
} else if (r > 255) {
|
||||
r = 255;
|
||||
}
|
||||
if (g < 0) {
|
||||
g = 0;
|
||||
} else if (g > 255) {
|
||||
g = 255;
|
||||
}
|
||||
if (b < 0) {
|
||||
b = 0;
|
||||
} else if (b > 255) {
|
||||
b = 255;
|
||||
}
|
||||
row_dst[col * 4] = (uint8_t) r;
|
||||
row_dst[col * 4 + 1] = (uint8_t) g;
|
||||
row_dst[col * 4 + 2] = (uint8_t) b;
|
||||
row_dst[col * 4 + 3] = 255;
|
||||
}
|
||||
}
|
||||
ANativeWindow_unlockAndPost(window);
|
||||
}
|
||||
|
||||
#endif /* ANDROIDCAST_HAVE_LIBVPX */
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeIsLibvpxAvailable(
|
||||
JNIEnv *env, jclass clazz) {
|
||||
(void) env;
|
||||
(void) clazz;
|
||||
return ANDROIDCAST_HAVE_LIBVPX ? JNI_TRUE : JNI_FALSE;
|
||||
}
|
||||
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeVpxEncoderCreate(
|
||||
JNIEnv *env, jclass clazz, jstring mime, jint width, jint height, jint bitrateKbps) {
|
||||
(void) clazz;
|
||||
#if !ANDROIDCAST_HAVE_LIBVPX
|
||||
(void) env;
|
||||
(void) mime;
|
||||
(void) width;
|
||||
(void) height;
|
||||
(void) bitrateKbps;
|
||||
return 0L;
|
||||
#else
|
||||
if (width <= 0 || height <= 0 || bitrateKbps <= 0) {
|
||||
return 0L;
|
||||
}
|
||||
const char *mime_utf = (*env)->GetStringUTFChars(env, mime, NULL);
|
||||
VpxEncoderCtx *ctx = (VpxEncoderCtx *) calloc(1, sizeof(VpxEncoderCtx));
|
||||
if (ctx == NULL) {
|
||||
(*env)->ReleaseStringUTFChars(env, mime, mime_utf);
|
||||
return 0L;
|
||||
}
|
||||
ctx->magic = VPX_ENC_MAGIC;
|
||||
ctx->width = width;
|
||||
ctx->height = height;
|
||||
|
||||
vpx_codec_enc_cfg_t cfg;
|
||||
vpx_codec_iface_t *iface = pick_encoder_iface(mime_utf);
|
||||
if (vpx_codec_enc_config_default(iface, &cfg, 0) != VPX_CODEC_OK) {
|
||||
free(ctx);
|
||||
(*env)->ReleaseStringUTFChars(env, mime, mime_utf);
|
||||
return 0L;
|
||||
}
|
||||
cfg.g_w = (unsigned int) width;
|
||||
cfg.g_h = (unsigned int) height;
|
||||
cfg.rc_target_bitrate = bitrateKbps;
|
||||
cfg.g_timebase.num = 1;
|
||||
cfg.g_timebase.den = 30;
|
||||
cfg.g_error_resilient = VPX_ERROR_RESILIENT_DEFAULT;
|
||||
cfg.g_lag_in_frames = 0;
|
||||
|
||||
if (vpx_codec_enc_init(&ctx->codec, iface, &cfg, 0) != VPX_CODEC_OK) {
|
||||
free(ctx);
|
||||
(*env)->ReleaseStringUTFChars(env, mime, mime_utf);
|
||||
return 0L;
|
||||
}
|
||||
/* VP8 and VP9 both use VP8E_SET_CPUUSED (see libvpx tests / vp9_cx_iface.c). */
|
||||
vpx_codec_control(&ctx->codec, VP8E_SET_CPUUSED, 8);
|
||||
|
||||
if (!vpx_img_alloc(&ctx->img, img_fmt(), width, height, 1)) {
|
||||
vpx_codec_destroy(&ctx->codec);
|
||||
free(ctx);
|
||||
(*env)->ReleaseStringUTFChars(env, mime, mime_utf);
|
||||
return 0L;
|
||||
}
|
||||
(*env)->ReleaseStringUTFChars(env, mime, mime_utf);
|
||||
return (jlong) (intptr_t) ctx;
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeVpxEncodeYuvFrame(
|
||||
JNIEnv *env, jclass clazz, jlong handle, jbyteArray yuv, jlong ptsUs, jboolean forceKeyframe) {
|
||||
(void) clazz;
|
||||
#if !ANDROIDCAST_HAVE_LIBVPX
|
||||
(void) env;
|
||||
(void) handle;
|
||||
(void) yuv;
|
||||
(void) ptsUs;
|
||||
(void) forceKeyframe;
|
||||
return NULL;
|
||||
#else
|
||||
VpxEncoderCtx *ctx = enc_from_handle(handle);
|
||||
if (ctx == NULL || yuv == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
jsize yuv_len = (*env)->GetArrayLength(env, yuv);
|
||||
int need = ctx->width * ctx->height * 3 / 2;
|
||||
if (yuv_len < need) {
|
||||
return NULL;
|
||||
}
|
||||
jbyte *yuv_bytes = (*env)->GetByteArrayElements(env, yuv, NULL);
|
||||
if (yuv_bytes == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
copy_i420_to_image((const uint8_t *) yuv_bytes, &ctx->img, ctx->width, ctx->height);
|
||||
(*env)->ReleaseByteArrayElements(env, yuv, yuv_bytes, JNI_ABORT);
|
||||
|
||||
vpx_enc_frame_flags_t flags = 0;
|
||||
if (forceKeyframe || ctx->force_keyframe) {
|
||||
flags |= VPX_EFLAG_FORCE_KF;
|
||||
ctx->force_keyframe = 0;
|
||||
}
|
||||
|
||||
if (vpx_codec_encode(&ctx->codec, &ctx->img, (vpx_codec_pts_t) ptsUs, 1, flags,
|
||||
VPX_DL_REALTIME) != VPX_CODEC_OK) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const vpx_codec_cx_pkt_t *pkt;
|
||||
vpx_codec_iter_t iter = NULL;
|
||||
while ((pkt = vpx_codec_get_cx_data(&ctx->codec, &iter)) != NULL) {
|
||||
if (pkt->kind == VPX_CODEC_CX_FRAME_PKT) {
|
||||
jbyteArray out = (*env)->NewByteArray(env, (jsize) pkt->data.frame.sz);
|
||||
if (out == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
(*env)->SetByteArrayRegion(env, out, 0, (jsize) pkt->data.frame.sz,
|
||||
(const jbyte *) pkt->data.frame.buf);
|
||||
jboolean key = (pkt->data.frame.flags & VPX_FRAME_IS_KEY) ? JNI_TRUE : JNI_FALSE;
|
||||
jclass frame_cls = (*env)->FindClass(
|
||||
env, "com/foxx/androidcast/media/codec/jni/VpxEncodedFrame");
|
||||
jmethodID ctor = (*env)->GetMethodID(env, frame_cls, "<init>", "([BZ)V");
|
||||
jobject frame = (*env)->NewObject(env, frame_cls, ctor, out, key);
|
||||
ctx->frame_count++;
|
||||
return frame;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT jbyteArray JNICALL
|
||||
Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeVpxEncodeYuv(
|
||||
JNIEnv *env, jclass clazz, jlong handle, jbyteArray yuv, jlong ptsUs, jboolean forceKeyframe) {
|
||||
#if !ANDROIDCAST_HAVE_LIBVPX
|
||||
(void) env;
|
||||
(void) clazz;
|
||||
(void) handle;
|
||||
(void) yuv;
|
||||
(void) ptsUs;
|
||||
(void) forceKeyframe;
|
||||
return NULL;
|
||||
#else
|
||||
jobject frame = Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeVpxEncodeYuvFrame(
|
||||
env, clazz, handle, yuv, ptsUs, forceKeyframe);
|
||||
if (frame == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
jclass frame_cls = (*env)->GetObjectClass(env, frame);
|
||||
jfieldID data_f = (*env)->GetFieldID(env, frame_cls, "data", "[B");
|
||||
return (jbyteArray) (*env)->GetObjectField(env, frame, data_f);
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeVpxEncoderRelease(
|
||||
JNIEnv *env, jclass clazz, jlong handle) {
|
||||
(void) env;
|
||||
(void) clazz;
|
||||
#if ANDROIDCAST_HAVE_LIBVPX
|
||||
VpxEncoderCtx *ctx = enc_from_handle(handle);
|
||||
if (ctx == NULL) {
|
||||
return;
|
||||
}
|
||||
ctx->magic = 0;
|
||||
vpx_img_free(&ctx->img);
|
||||
vpx_codec_destroy(&ctx->codec);
|
||||
free(ctx);
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeVpxRequestKeyframe(
|
||||
JNIEnv *env, jclass clazz, jlong handle) {
|
||||
(void) env;
|
||||
(void) clazz;
|
||||
#if ANDROIDCAST_HAVE_LIBVPX
|
||||
VpxEncoderCtx *ctx = enc_from_handle(handle);
|
||||
if (ctx != NULL) {
|
||||
ctx->force_keyframe = 1;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeVpxDecoderCreate(
|
||||
JNIEnv *env, jclass clazz, jstring mime) {
|
||||
(void) clazz;
|
||||
#if !ANDROIDCAST_HAVE_LIBVPX
|
||||
(void) env;
|
||||
(void) mime;
|
||||
return 0L;
|
||||
#else
|
||||
const char *mime_utf = (*env)->GetStringUTFChars(env, mime, NULL);
|
||||
VpxDecoderCtx *ctx = (VpxDecoderCtx *) calloc(1, sizeof(VpxDecoderCtx));
|
||||
if (ctx == NULL) {
|
||||
(*env)->ReleaseStringUTFChars(env, mime, mime_utf);
|
||||
return 0L;
|
||||
}
|
||||
ctx->magic = VPX_DEC_MAGIC;
|
||||
vpx_codec_iface_t *iface = pick_decoder_iface(mime_utf);
|
||||
vpx_codec_dec_cfg_t cfg = {0};
|
||||
cfg.threads = 1;
|
||||
/* vpxdec.c: dec_flags |= VPX_CODEC_USE_ERROR_CONCEALMENT when --error-concealment */
|
||||
int dec_flags = 0;
|
||||
if (vpx_codec_get_caps(iface) & VPX_CODEC_CAP_ERROR_CONCEALMENT) {
|
||||
dec_flags |= VPX_CODEC_USE_ERROR_CONCEALMENT;
|
||||
}
|
||||
if (vpx_codec_dec_init(&ctx->codec, iface, &cfg, dec_flags) != VPX_CODEC_OK) {
|
||||
free(ctx);
|
||||
(*env)->ReleaseStringUTFChars(env, mime, mime_utf);
|
||||
return 0L;
|
||||
}
|
||||
(*env)->ReleaseStringUTFChars(env, mime, mime_utf);
|
||||
return (jlong) (intptr_t) ctx;
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeVpxDecodeFrame(
|
||||
JNIEnv *env, jclass clazz, jlong handle, jbyteArray data, jlong ptsUs, jobject surface) {
|
||||
(void) clazz;
|
||||
(void) ptsUs;
|
||||
#if !ANDROIDCAST_HAVE_LIBVPX
|
||||
(void) env;
|
||||
(void) handle;
|
||||
(void) data;
|
||||
(void) surface;
|
||||
return JNI_FALSE;
|
||||
#else
|
||||
VpxDecoderCtx *ctx = dec_from_handle(handle);
|
||||
if (ctx == NULL || data == NULL || surface == NULL) {
|
||||
return JNI_FALSE;
|
||||
}
|
||||
jsize len = (*env)->GetArrayLength(env, data);
|
||||
if (len <= 0) {
|
||||
return JNI_FALSE;
|
||||
}
|
||||
jbyte *bytes = (*env)->GetByteArrayElements(env, data, NULL);
|
||||
if (bytes == NULL) {
|
||||
return JNI_FALSE;
|
||||
}
|
||||
vpx_codec_err_t err = vpx_codec_decode(&ctx->codec, (const uint8_t *) bytes, (unsigned int) len,
|
||||
NULL, 0);
|
||||
(*env)->ReleaseByteArrayElements(env, data, bytes, JNI_ABORT);
|
||||
/* With error concealment, vpx_codec_decode may warn yet still output a frame (see vpxdec.c). */
|
||||
(void) err;
|
||||
|
||||
ANativeWindow *window = ANativeWindow_fromSurface(env, surface);
|
||||
if (window == NULL) {
|
||||
return JNI_FALSE;
|
||||
}
|
||||
|
||||
vpx_codec_iter_t iter = NULL;
|
||||
vpx_image_t *img;
|
||||
jboolean rendered = JNI_FALSE;
|
||||
while ((img = vpx_codec_get_frame(&ctx->codec, &iter)) != NULL) {
|
||||
render_i420_to_window(window, img);
|
||||
rendered = JNI_TRUE;
|
||||
}
|
||||
ANativeWindow_release(window);
|
||||
return rendered;
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeVpxDecoderRelease(
|
||||
JNIEnv *env, jclass clazz, jlong handle) {
|
||||
(void) env;
|
||||
(void) clazz;
|
||||
#if ANDROIDCAST_HAVE_LIBVPX
|
||||
VpxDecoderCtx *ctx = dec_from_handle(handle);
|
||||
if (ctx == NULL) {
|
||||
return;
|
||||
}
|
||||
ctx->magic = 0;
|
||||
vpx_codec_destroy(&ctx->codec);
|
||||
free(ctx);
|
||||
#endif
|
||||
}
|
||||
25
ndk/jni/opus_bridge.c
Normal file
25
ndk/jni/opus_bridge.c
Normal file
@@ -0,0 +1,25 @@
|
||||
/* package ndk/jni/opus_bridge.c */
|
||||
|
||||
/*********************************************************************
|
||||
* opus_bridge.c
|
||||
* Created at: Sun 17 May 2026 22:49:49 +0200
|
||||
* Updated at: Wed 20 May 2026 15:17:13 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
|
||||
* Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8
|
||||
* Contributors:
|
||||
* - Anton Afanasyeu <a.afanasieff@gmail.com> (2 commits, 21 lines)
|
||||
* - Cursor Agent (project assistant)
|
||||
* Digest: SHA256 401e8483138f40becb1bcd3bfb46693c651c44dbc6f5d85cbcc645df93b51bfb
|
||||
**********************************************************************/
|
||||
#include <jni.h>
|
||||
|
||||
#ifndef ANDROIDCAST_HAVE_OPUS
|
||||
#define ANDROIDCAST_HAVE_OPUS 0
|
||||
#endif
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeIsOpusAvailable(
|
||||
JNIEnv *env, jclass clazz) {
|
||||
(void)env;
|
||||
(void)clazz;
|
||||
return ANDROIDCAST_HAVE_OPUS ? JNI_TRUE : JNI_FALSE;
|
||||
}
|
||||
25
ndk/jni/speex_bridge.c
Normal file
25
ndk/jni/speex_bridge.c
Normal file
@@ -0,0 +1,25 @@
|
||||
/* package ndk/jni/speex_bridge.c */
|
||||
|
||||
/*********************************************************************
|
||||
* speex_bridge.c
|
||||
* Created at: Sun 17 May 2026 22:49:49 +0200
|
||||
* Updated at: Wed 20 May 2026 15:17:13 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
|
||||
* Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8
|
||||
* Contributors:
|
||||
* - Anton Afanasyeu <a.afanasieff@gmail.com> (2 commits, 21 lines)
|
||||
* - Cursor Agent (project assistant)
|
||||
* Digest: SHA256 5085e4a255dcad08ac2eb746ad1d1f64408a191f035d9a6ca83dab896b3e9a09
|
||||
**********************************************************************/
|
||||
#include <jni.h>
|
||||
|
||||
#ifndef ANDROIDCAST_HAVE_SPEEX
|
||||
#define ANDROIDCAST_HAVE_SPEEX 0
|
||||
#endif
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeIsSpeexAvailable(
|
||||
JNIEnv *env, jclass clazz) {
|
||||
(void)env;
|
||||
(void)clazz;
|
||||
return ANDROIDCAST_HAVE_SPEEX ? JNI_TRUE : JNI_FALSE;
|
||||
}
|
||||
8
ndk/obfuscate/README.md
Normal file
8
ndk/obfuscate/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Entitlements obfuscation (C)
|
||||
|
||||
Shared by `libandroidcast_entitlements.so` and the archived spike under `reference-design/obfuscate_entitlements/`.
|
||||
|
||||
- `encode` / `decode` — 40-bit payload + 24-bit verification tag, keyed by per-account `uint64_t` **key** (not wall clock).
|
||||
- `write_to` / `read_from` — file layout: 32-bit dummy header, N×uint64_t payloads, 32-bit dummy tail.
|
||||
|
||||
Reference demo: `tmp/obfuscate_test/` (standalone; not wired into the main app).
|
||||
122
ndk/obfuscate/obfuscate.c
Normal file
122
ndk/obfuscate/obfuscate.c
Normal file
@@ -0,0 +1,122 @@
|
||||
#include "obfuscate.h"
|
||||
#include <stddef.h>
|
||||
|
||||
static uint16_t rotl16(uint16_t x, unsigned int n)
|
||||
{
|
||||
n &= 15u;
|
||||
return (uint16_t)((x << n) | (x >> (16u - n)));
|
||||
}
|
||||
|
||||
static uint64_t rotl40(uint64_t x, unsigned int n)
|
||||
{
|
||||
n %= OBF_PAYLOAD_BITS;
|
||||
x &= OBF_PAYLOAD_MASK;
|
||||
if (n == 0u)
|
||||
return x;
|
||||
return ((x << n) | (x >> (OBF_PAYLOAD_BITS - n))) & OBF_PAYLOAD_MASK;
|
||||
}
|
||||
|
||||
static uint64_t rotr40(uint64_t x, unsigned int n)
|
||||
{
|
||||
n %= OBF_PAYLOAD_BITS;
|
||||
x &= OBF_PAYLOAD_MASK;
|
||||
if (n == 0u)
|
||||
return x;
|
||||
return ((x >> n) | (x << (OBF_PAYLOAD_BITS - n))) & OBF_PAYLOAD_MASK;
|
||||
}
|
||||
|
||||
/* Lightweight seed mixer — same output for same seed on any platform. */
|
||||
static uint64_t derive_key(uint64_t seed)
|
||||
{
|
||||
uint64_t x = seed ^ 0xA5A5A5A5A5A5A5A5ULL;
|
||||
|
||||
x ^= x >> 33;
|
||||
x *= 0xFF51AFD7ED558CCDULL;
|
||||
x ^= x >> 33;
|
||||
x *= 0xC4CEB9FE1A85EC53ULL;
|
||||
x ^= x >> 33;
|
||||
return x & OBF_PAYLOAD_MASK;
|
||||
}
|
||||
|
||||
static unsigned int derive_rotation(uint64_t seed)
|
||||
{
|
||||
uint64_t mix = seed * 0x9E3779B97F4A7C15ULL;
|
||||
|
||||
return (unsigned int)((mix >> 59) + 1u); /* 1..8 */
|
||||
}
|
||||
|
||||
/*
|
||||
* 24-bit FEC-style tag: four 16-bit shards XOR-mixed with key and seed.
|
||||
* Detects single-bit flips and wrong seed with high probability.
|
||||
*/
|
||||
static uint32_t compute_tag(uint64_t val, uint64_t seed)
|
||||
{
|
||||
uint64_t k = derive_key(seed);
|
||||
uint64_t a = (val & 0xFFFFULL) ^ (k & 0xFFFFULL);
|
||||
uint64_t b = ((val >> 16) & 0xFFFFULL) ^ ((k >> 16) & 0xFFFFULL);
|
||||
uint64_t c = ((val >> 32) & 0xFFFFULL) ^ ((k >> 32) & 0xFFFFULL);
|
||||
uint64_t d = ((val >> 48) & 0xFFFFULL) ^ ((k >> 48) & 0xFFFFULL);
|
||||
uint64_t p1 = a ^ rotl16((uint16_t)b, 3) ^ c;
|
||||
uint64_t p2 = b ^ rotl16((uint16_t)c, 7) ^ d;
|
||||
uint64_t p3 = c ^ rotl16((uint16_t)d, 11) ^ a;
|
||||
uint64_t mix = (p1 * 0x01000193ULL) ^ (p2 << 16) ^ (p3 << 8);
|
||||
|
||||
mix ^= seed;
|
||||
mix *= 0x85EBCA77C2B2AE63ULL;
|
||||
mix ^= mix >> 33;
|
||||
return (uint32_t)(mix & 0xFFFFFFULL);
|
||||
}
|
||||
|
||||
static uint64_t obfuscate(uint64_t val, uint64_t seed)
|
||||
{
|
||||
uint64_t key = derive_key(seed);
|
||||
unsigned int rot = derive_rotation(seed);
|
||||
|
||||
return rotl40(val & OBF_PAYLOAD_MASK, rot) ^ key;
|
||||
}
|
||||
|
||||
static uint64_t deobfuscate(uint64_t obscured, uint64_t seed)
|
||||
{
|
||||
uint64_t key = derive_key(seed);
|
||||
unsigned int rot = derive_rotation(seed);
|
||||
|
||||
return rotr40(obscured ^ key, rot);
|
||||
}
|
||||
|
||||
int32_t encode(uint64_t val, uint64_t seed, uint64_t *out)
|
||||
{
|
||||
uint64_t packed;
|
||||
uint32_t tag;
|
||||
|
||||
if (out == NULL)
|
||||
return OBF_ERR_NULL;
|
||||
if (val > OBF_PAYLOAD_MASK)
|
||||
return OBF_ERR_RANGE;
|
||||
|
||||
tag = compute_tag(val, seed);
|
||||
packed = ((uint64_t)tag << OBF_PAYLOAD_BITS) | obfuscate(val, seed);
|
||||
*out = packed;
|
||||
return OBF_OK;
|
||||
}
|
||||
|
||||
int32_t decode(uint64_t encoded_val, uint64_t seed, uint64_t *out)
|
||||
{
|
||||
uint32_t stored_tag;
|
||||
uint32_t expected_tag;
|
||||
uint64_t obscured;
|
||||
uint64_t val;
|
||||
|
||||
if (out == NULL)
|
||||
return OBF_ERR_NULL;
|
||||
|
||||
stored_tag = (uint32_t)(encoded_val >> OBF_PAYLOAD_BITS);
|
||||
obscured = encoded_val & OBF_PAYLOAD_MASK;
|
||||
val = deobfuscate(obscured, seed);
|
||||
expected_tag = compute_tag(val, seed);
|
||||
|
||||
if (stored_tag != expected_tag)
|
||||
return OBF_ERR_VERIFY;
|
||||
|
||||
*out = val;
|
||||
return OBF_OK;
|
||||
}
|
||||
30
ndk/obfuscate/obfuscate.h
Normal file
30
ndk/obfuscate/obfuscate.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#ifndef OBFUSCATE_H
|
||||
#define OBFUSCATE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Payload uses lower 40 bits; upper 24 bits hold verification tag. */
|
||||
#define OBF_PAYLOAD_BITS 40u
|
||||
#define OBF_PAYLOAD_MASK ((1ULL << OBF_PAYLOAD_BITS) - 1ULL)
|
||||
|
||||
/* Return codes */
|
||||
#define OBF_OK 0
|
||||
#define OBF_ERR_NULL -1 /* out pointer is NULL */
|
||||
#define OBF_ERR_RANGE -2 /* val exceeds encodable payload width */
|
||||
#define OBF_ERR_VERIFY -3 /* tag mismatch (wrong seed or corrupted data) */
|
||||
|
||||
/*
|
||||
* Encode uint64_t payload (e.g. flag bits) using seed-derived keystream.
|
||||
* seed is typically unix time in whole seconds shared by encoder and decoder.
|
||||
*
|
||||
* On success writes single uint64_t to *out and returns OBF_OK.
|
||||
*/
|
||||
int32_t encode(uint64_t val, uint64_t seed, uint64_t *out);
|
||||
|
||||
/*
|
||||
* Decode and verify. seed must match the value used at encode time.
|
||||
* On success writes recovered payload to *out and returns OBF_OK.
|
||||
*/
|
||||
int32_t decode(uint64_t encoded_val, uint64_t seed, uint64_t *out);
|
||||
|
||||
#endif /* OBFUSCATE_H */
|
||||
215
ndk/obfuscate/obfuscate_file.c
Normal file
215
ndk/obfuscate/obfuscate_file.c
Normal file
@@ -0,0 +1,215 @@
|
||||
#include "obfuscate_file.h"
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static uint32_t dummy_u32(uint64_t seed, uint32_t salt)
|
||||
{
|
||||
uint64_t x = seed ^ ((uint64_t)salt * 0x9E3779B97F4A7C15ULL);
|
||||
|
||||
x ^= x >> 33;
|
||||
x *= 0xFF51AFD7ED558CCDULL;
|
||||
x ^= x >> 33;
|
||||
return (uint32_t)(x ^ (x >> 32));
|
||||
}
|
||||
|
||||
static void put_u32_le(uint8_t *dst, uint32_t v)
|
||||
{
|
||||
dst[0] = (uint8_t)(v);
|
||||
dst[1] = (uint8_t)(v >> 8);
|
||||
dst[2] = (uint8_t)(v >> 16);
|
||||
dst[3] = (uint8_t)(v >> 24);
|
||||
}
|
||||
|
||||
static void put_u64_le(uint8_t *dst, uint64_t v)
|
||||
{
|
||||
unsigned int i;
|
||||
|
||||
for (i = 0; i < 8u; i++)
|
||||
dst[i] = (uint8_t)(v >> (8u * i));
|
||||
}
|
||||
|
||||
static uint32_t get_u32_le(const uint8_t *src)
|
||||
{
|
||||
return (uint32_t)src[0] | ((uint32_t)src[1] << 8) | ((uint32_t)src[2] << 16) |
|
||||
((uint32_t)src[3] << 24);
|
||||
}
|
||||
|
||||
static uint64_t get_u64_le(const uint8_t *src)
|
||||
{
|
||||
uint64_t v = 0;
|
||||
unsigned int i;
|
||||
|
||||
for (i = 0; i < 8u; i++)
|
||||
v |= (uint64_t)src[i] << (8u * i);
|
||||
return v;
|
||||
}
|
||||
|
||||
int32_t obf_file_payload_count(long file_size, size_t *count_out)
|
||||
{
|
||||
long payload_bytes;
|
||||
|
||||
if (file_size < 0)
|
||||
return OBF_ERR_IO;
|
||||
if ((unsigned long)file_size < OBF_FILE_OVERHEAD + sizeof(uint64_t))
|
||||
return OBF_ERR_FORMAT;
|
||||
|
||||
payload_bytes = file_size - (long)OBF_FILE_OVERHEAD;
|
||||
if (payload_bytes % (long)sizeof(uint64_t) != 0)
|
||||
return OBF_ERR_FORMAT;
|
||||
|
||||
*count_out = (size_t)payload_bytes / sizeof(uint64_t);
|
||||
return OBF_OK;
|
||||
}
|
||||
|
||||
static int32_t write_encoded_file(const char *path, const uint64_t *encoded, size_t count,
|
||||
uint64_t seed)
|
||||
{
|
||||
FILE *fp;
|
||||
uint8_t hdr[OBF_FILE_HDR_SIZE];
|
||||
uint8_t tail[OBF_FILE_TAIL_SIZE];
|
||||
uint8_t block[sizeof(uint64_t)];
|
||||
size_t i;
|
||||
|
||||
if (path == NULL || encoded == NULL || count == 0)
|
||||
return OBF_ERR_NULL;
|
||||
|
||||
fp = fopen(path, "wb");
|
||||
if (fp == NULL)
|
||||
return OBF_ERR_IO;
|
||||
|
||||
put_u32_le(hdr, dummy_u32(seed, 0xA11CEu));
|
||||
if (fwrite(hdr, 1, sizeof(hdr), fp) != sizeof(hdr)) {
|
||||
fclose(fp);
|
||||
return OBF_ERR_IO;
|
||||
}
|
||||
|
||||
for (i = 0; i < count; i++) {
|
||||
put_u64_le(block, encoded[i]);
|
||||
if (fwrite(block, 1, sizeof(block), fp) != sizeof(block)) {
|
||||
fclose(fp);
|
||||
return OBF_ERR_IO;
|
||||
}
|
||||
}
|
||||
|
||||
put_u32_le(tail, dummy_u32(seed, 0x7A1Lu));
|
||||
if (fwrite(tail, 1, sizeof(tail), fp) != sizeof(tail)) {
|
||||
fclose(fp);
|
||||
return OBF_ERR_IO;
|
||||
}
|
||||
|
||||
if (fclose(fp) != 0)
|
||||
return OBF_ERR_IO;
|
||||
return OBF_OK;
|
||||
}
|
||||
|
||||
int32_t write_many_to(const char *path, const uint64_t *vals, size_t count, uint64_t seed)
|
||||
{
|
||||
uint64_t *encoded;
|
||||
size_t i;
|
||||
int32_t rv;
|
||||
|
||||
if (path == NULL || vals == NULL || count == 0)
|
||||
return OBF_ERR_NULL;
|
||||
|
||||
encoded = (uint64_t *)malloc(count * sizeof(uint64_t));
|
||||
if (encoded == NULL)
|
||||
return OBF_ERR_IO;
|
||||
|
||||
for (i = 0; i < count; i++) {
|
||||
rv = encode(vals[i], seed, &encoded[i]);
|
||||
if (rv != OBF_OK) {
|
||||
free(encoded);
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
||||
rv = write_encoded_file(path, encoded, count, seed);
|
||||
free(encoded);
|
||||
return rv;
|
||||
}
|
||||
|
||||
int32_t write_to(const char *path, uint64_t val, uint64_t seed)
|
||||
{
|
||||
return write_many_to(path, &val, 1, seed);
|
||||
}
|
||||
|
||||
int32_t read_many_from(const char *path, uint64_t seed, uint64_t *out_vals, size_t out_cap,
|
||||
size_t *out_count)
|
||||
{
|
||||
FILE *fp;
|
||||
long file_size;
|
||||
size_t count;
|
||||
size_t i;
|
||||
uint8_t hdr[OBF_FILE_HDR_SIZE];
|
||||
uint8_t tail[OBF_FILE_TAIL_SIZE];
|
||||
uint8_t block[sizeof(uint64_t)];
|
||||
uint64_t encoded;
|
||||
uint64_t decoded;
|
||||
int32_t rv;
|
||||
|
||||
if (path == NULL || out_vals == NULL || out_count == NULL)
|
||||
return OBF_ERR_NULL;
|
||||
|
||||
fp = fopen(path, "rb");
|
||||
if (fp == NULL)
|
||||
return OBF_ERR_IO;
|
||||
|
||||
if (fseek(fp, 0, SEEK_END) != 0) {
|
||||
fclose(fp);
|
||||
return OBF_ERR_IO;
|
||||
}
|
||||
file_size = ftell(fp);
|
||||
if (file_size < 0 || fseek(fp, 0, SEEK_SET) != 0) {
|
||||
fclose(fp);
|
||||
return OBF_ERR_IO;
|
||||
}
|
||||
|
||||
rv = obf_file_payload_count(file_size, &count);
|
||||
if (rv != OBF_OK) {
|
||||
fclose(fp);
|
||||
return rv;
|
||||
}
|
||||
if (count > out_cap) {
|
||||
fclose(fp);
|
||||
return OBF_ERR_OVERFLOW;
|
||||
}
|
||||
|
||||
if (fread(hdr, 1, sizeof(hdr), fp) != sizeof(hdr)) {
|
||||
fclose(fp);
|
||||
return OBF_ERR_IO;
|
||||
}
|
||||
(void)get_u32_le(hdr);
|
||||
|
||||
for (i = 0; i < count; i++) {
|
||||
if (fread(block, 1, sizeof(block), fp) != sizeof(block)) {
|
||||
fclose(fp);
|
||||
return OBF_ERR_IO;
|
||||
}
|
||||
encoded = get_u64_le(block);
|
||||
rv = decode(encoded, seed, &decoded);
|
||||
if (rv != OBF_OK) {
|
||||
fclose(fp);
|
||||
return rv;
|
||||
}
|
||||
out_vals[i] = decoded;
|
||||
}
|
||||
|
||||
if (fread(tail, 1, sizeof(tail), fp) != sizeof(tail)) {
|
||||
fclose(fp);
|
||||
return OBF_ERR_IO;
|
||||
}
|
||||
(void)get_u32_le(tail);
|
||||
|
||||
fclose(fp);
|
||||
*out_count = count;
|
||||
return OBF_OK;
|
||||
}
|
||||
|
||||
int32_t read_from(const char *path, uint64_t seed, uint64_t *out)
|
||||
{
|
||||
size_t count = 0;
|
||||
|
||||
return read_many_from(path, seed, out, 1, &count);
|
||||
}
|
||||
28
ndk/obfuscate/obfuscate_file.h
Normal file
28
ndk/obfuscate/obfuscate_file.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#ifndef OBFUSCATE_FILE_H
|
||||
#define OBFUSCATE_FILE_H
|
||||
|
||||
#include "obfuscate.h"
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* 32-bit dummy header/tail — skipped by decode; only uint64_t payloads are processed. */
|
||||
#define OBF_FILE_HDR_SIZE 4u
|
||||
#define OBF_FILE_TAIL_SIZE 4u
|
||||
#define OBF_FILE_OVERHEAD (OBF_FILE_HDR_SIZE + OBF_FILE_TAIL_SIZE)
|
||||
|
||||
#define OBF_ERR_IO -4
|
||||
#define OBF_ERR_FORMAT -5
|
||||
#define OBF_ERR_OVERFLOW -6
|
||||
|
||||
int32_t write_to(const char *path, uint64_t val, uint64_t seed);
|
||||
int32_t read_from(const char *path, uint64_t seed, uint64_t *out);
|
||||
|
||||
int32_t write_many_to(const char *path, const uint64_t *vals, size_t count, uint64_t seed);
|
||||
|
||||
int32_t read_many_from(const char *path, uint64_t seed, uint64_t *out_vals, size_t out_cap,
|
||||
size_t *out_count);
|
||||
|
||||
/* How many uint64_t payloads are stored (excludes 32-bit head/tail). */
|
||||
int32_t obf_file_payload_count(long file_size, size_t *count_out);
|
||||
|
||||
#endif /* OBFUSCATE_FILE_H */
|
||||
Reference in New Issue
Block a user