/* 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 * Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8 * Contributors: * - Anton Afanasyeu (6 commits, 432 lines) * - Cursor Agent (project assistant) * Digest: SHA256 3bdffcead822ea7b00582f416ffe18d3f885f8b91ddab3e87752c23dcf326a02 **********************************************************************/ #include #include #include #include #include #ifndef ANDROIDCAST_HAVE_LIBVPX #define ANDROIDCAST_HAVE_LIBVPX 0 #endif #if ANDROIDCAST_HAVE_LIBVPX #include #include #include #include #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, "", "([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 }