From da0898632e4911fd3a388f48819bb0e139f211fc Mon Sep 17 00:00:00 2001 From: Yayo Gutierrez Date: Fri, 9 Oct 2026 19:03:48 -0500 Subject: [PATCH] fix(android): drop KEY_PRIORITY/KEY_OPERATING_RATE so Exynos encoders can start (#423) Exynos H.264 encoders (c2.exynos.h264.encoder) accept the realtime priority + max operating rate hint in configure() but fail start() with NO_MEMORY ("real-time(32767 fps) is not supported") at higher resolutions such as 1080p. The prepareEncoder fallback only covers configure() failures, so Video.compress rejected. Fixes #423 Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 4 ++++ TRIAGE.md | 2 ++ .../Video/VideoCompressor/compressor/Compressor.kt | 6 +++--- .../Video/VideoCompressor/utils/CompressorUtils.kt | 9 ++------- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9d0a2a9..aa7e929 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -118,6 +118,10 @@ When you change native Swift/Kotlin code and can't run it on a device/simulator, remaps such inputs to their backward-compatible HEVC/AVC base layer (profiles 8/4 → HEVC, profile 9 → AVC) or throws a clear error for non-compatible profiles (5/7). See #398. +- Don't set `KEY_PRIORITY = 0` / `KEY_OPERATING_RATE` on the encoder format. Exynos + encoders (e.g. `c2.exynos.h264.encoder`) accept them in `configure()`, then fail + `start()` with `NO_MEMORY` ("real-time(32767 fps) is not supported"), and the + `prepareEncoder` fallback only covers `configure()` failures. - The encoder is intentionally `c2.android.avc.encoder` (when QTI codecs exist) or `MediaCodec.createEncoderByType("video/avc")`; QTI AVC encoders can produce MP4s that do not play on Mac/iPhone, so avoid switching this without testing. diff --git a/TRIAGE.md b/TRIAGE.md index 40fee07..3fc5382 100644 --- a/TRIAGE.md +++ b/TRIAGE.md @@ -13,6 +13,7 @@ Legend: | Issue | Triage | Notes | | --- | --- | --- | +| #423 | real, fixed here | Android `Video.compress` rejects with `NO_MEMORY` on Samsung Exynos encoders (`c2.exynos.h264.encoder`) at higher resolutions (e.g. 1080p manual mode). #399 set `KEY_PRIORITY = 0` + `KEY_OPERATING_RATE = Short.MAX_VALUE`; Exynos accepts them in `configure()` but fails `start()` ("real-time(32767 fps) is not supported"), which the `prepareEncoder` fallback doesn't cover. Small `auto` outputs (360×640) still fit the realtime budget. This branch removes both keys. Verified on a Galaxy A56 (Exynos 1580): 1080p manual fails with the keys and succeeds without them (94.1 MB → 32.0 MB). | | #406 | real, fixed here | Nitro module not linked in Android **release** builds (works in debug). R8 strips the `HybridCompressor` implementation, which is instantiated from C++/JNI by class name (`NitroCompressorOnLoad.cpp` `kJavaDescriptor`) and was the only Nitro class lacking keep protection — the generated spec has `@DoNotStrip @Keep`, but the hand-written impl didn't. This branch annotates `HybridCompressor` with `@DoNotStrip @Keep` (the same convention nitrogen and other Nitro modules like react-native-mmkv use), so no consumer ProGuard rules are needed. Verified with a minified release build: without the annotation R8 lists the class in `usage.txt` (removed); with it, the class + no-arg constructor are kept in `mapping.txt`. | | #404 | real, fixed here | Android Nitro module failed to compile on older React Native: the bridge `Promise.reject(...)` `code` parameter is non-null `String` on older RN but nullable `String?` on RN 0.85, and Kotlin's invariant override params can't satisfy both. This branch rewrites `NitroPromiseAdapter` in Java (nullability-erased override matching) so one implementation compiles across all RN versions. | | #400 | real, fixed here | iOS regression from #392: H.264 `videoOutputConfiguration` added `AVVideoExpectedSourceFrameRateKey` / `AVVideoAverageNonDroppableFrameRateKey`, which `canApply(...)` accepts but the iOS encoder silently drops the video track for, yielding an audio-only MP4 reported as success. This branch removes those keys and verifies the exported file actually contains a video track. | @@ -98,3 +99,4 @@ These should be closed upstream unless a current repro still exists on the lates - iOS: return background-upload response bodies consistently - Android: annotate the `HybridCompressor` impl with `@DoNotStrip @Keep` so the Nitro `Compressor` HybridObject (instantiated via JNI by name) survives R8 in consumer release builds (#406) - Android: rewrite `NitroPromiseAdapter` in Java so the Nitro module compiles across all React Native versions regardless of `Promise.reject`'s `code` nullability (#404) +- Android: drop `KEY_PRIORITY`/`KEY_OPERATING_RATE` from the encoder format so Exynos H.264 encoders don't fail `start()` with `NO_MEMORY` (#423) diff --git a/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/compressor/Compressor.kt b/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/compressor/Compressor.kt index 01ddc5a..6fc9342 100644 --- a/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/compressor/Compressor.kt +++ b/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/compressor/Compressor.kt @@ -248,7 +248,7 @@ object Compressor { // Prepare the video encoder. If the encoder rejects the throughput-tuned // format at configure() time, prepareEncoder reconfigures using this - // baseline format (same params, no VBR/priority/operating-rate keys). + // baseline format (same params, no VBR key). val encoder = prepareEncoder(outputFormat, hasQTI) { MediaFormat.createVideoFormat(MIME_TYPE, newWidth, newHeight).also { setOutputFileParameters( @@ -695,8 +695,8 @@ object Compressor { Log.i("Compressor", "encoder selected: ${encoder.name}") return encoder } catch (e: Exception) { - // Some encoders reject the throughput-tuning keys (VBR bitrate mode, - // priority, operating rate) at configure() time. A codec that throws + // Some encoders reject the throughput-tuning key (VBR bitrate mode) + // at configure() time. A codec that throws // from configure() is unusable, so release it and retry on a fresh // codec with a baseline format (default rate control) rather than // failing the whole compression. diff --git a/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/utils/CompressorUtils.kt b/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/utils/CompressorUtils.kt index b24b9cb..edc749a 100644 --- a/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/utils/CompressorUtils.kt +++ b/android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/utils/CompressorUtils.kt @@ -99,13 +99,8 @@ object CompressorUtils { MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR ) - // Hint the hardware codec to run as fast as it can (not throttled to - // realtime playback) and at the highest scheduling priority. These keys - // unlock full throughput on Qualcomm / Exynos / MTK SoCs that accept them. - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - setInteger(MediaFormat.KEY_PRIORITY, 0) - setInteger(MediaFormat.KEY_OPERATING_RATE, Short.MAX_VALUE.toInt()) - } + // KEY_PRIORITY = 0 (realtime) + KEY_OPERATING_RATE are intentionally not set: + // Exynos encoders accept them in configure() but fail start() with NO_MEMORY. } getColorStandard(inputFormat)?.let {