1
0
mirror of git://f0xx.org/ac/ac-mobile-android synced 2026-07-29 02:58:06 +03:00
This commit is contained in:
Anton Afanasyeu
2026-06-23 12:30:10 +02:00
commit 8675c2df3a
2972 changed files with 176371 additions and 0 deletions

118
ndk/CMakeLists.txt Normal file
View 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
View 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)

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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 */

View 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);
}

View 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 */