Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions TRIAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down Expand Up @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down