Skip to content

Fix/release build expo#408

Open
HuuNguyen312 wants to merge 10 commits into
numandev1:mainfrom
HuuNguyen312:fix/release_build_expo
Open

Fix/release build expo#408
HuuNguyen312 wants to merge 10 commits into
numandev1:mainfrom
HuuNguyen312:fix/release_build_expo

Conversation

@HuuNguyen312

Copy link
Copy Markdown
Contributor

Summary

Fixes #406.

On Android, react-native-compressor (Nitro) works in debug but fails to link in release builds — NitroModules.createHybridObject('Compressor') throws the linking error, so the module is unusable in production (reported on RN 0.85.3
/ Expo).

Why this happens — R8 strips a JNI-instantiated class:

The Compressor HybridObject implementation is created from C++/JNI by class-name string (generated NitroCompressorOnLoad.cpp):

static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/compressor/HybridCompressor;";
... javaClassStatic()->newObject(constructorFn);   // JNI instantiation of the impl class

R8 can't see that reference (it's a string in native code). The generated HybridCompressorSpec carries @DoNotStrip @Keep and survives, but the hand-written implementation HybridCompressor had no keep protection — so a consumer's release R8
removes/renames it. The JNI findClass/newObject then fails and the HybridObject is never registered. Debug works because R8 doesn't run.

This is the same reason other Nitro modules (e.g. react-native-mmkv) annotate their implementation class — nitrogen and the Nitro convention expect @DoNotStrip @Keep on the impl. This module was simply missing it.

The fix: Annotate HybridCompressor with @DoNotStrip @Keep (matching the generated spec and the convention used by other Nitro modules). No consumer ProGuard rules are needed.

Changelog

[ANDROID] [FIXED] - Keep the HybridCompressor Nitro implementation from R8 stripping so the Compressor HybridObject links in release builds (#406)

Test Plan

- [x] Ran local JS PR gate: yarn test:pr (unaffected — native-only change)
- [x] Reproduced and verified the fix with a real minified release build (enableProguardInReleaseBuilds = true, :app:assembleRelease, R8 enabled), inspecting R8's own output:

- Without the annotation — class is stripped:
# usage.txt (R8 removed list)
com.margelo.nitro.compressor.HybridCompressor

- With @DoNotStrip @Keep — class + no-arg constructor kept, not renamed:
# mapping.txt
com.margelo.nitro.compressor.HybridCompressor -> com.margelo.nitro.compressor.HybridCompressor:
    ... void <init>() -> <init>
- [x] Confirmed the same convention in react-native-mmkv (HybridMMKVPlatformContext is annotated @DoNotStrip @Keep).

▎ Note: verified via the bare example's minified release build (R8 mapping/usage output). I did not run the user's exact Expo expo run:android --variant release, but R8 behavior on the consuming app is what the annotation protects against.

HuuNguyen312 and others added 10 commits May 14, 2026 10:04
  iPhone HDR .MOV uses video/dolby-vision mime which has no
  standalone Android decoder, so createDecoderByType throws
  "Failed to initialize video/dolby-vision". DV profiles 8.x
  carry an HEVC base layer, so remap mime to video/hevc before
  configuring the decoder. Reject profile 5 (0x20) explicitly
  since it has no HEVC fallback.

  Perf, bundled to land with the codec rework:
  - Pick HW AVC encoder via MediaCodecList(ALL_CODECS), blacklist
    c2.qti.avc.encoder (corrupt MP4 on Mac/iOS).
  - Feed decoder until input slots drain instead of one sample
    per loop; unblocks parallel decode-render-encode.
  - Drop decoded frames whose PTS precedes the next target slot
    when source fps exceeds output fps.
  - Encoder: VBR + KEY_PRIORITY=0 + KEY_OPERATING_RATE=MAX to
    unthrottle HW codec scheduling.
  - Route SurfaceTexture onFrameAvailable to a dedicated
    HandlerThread so awaitNewImage stops contending with the
    main/JS thread.
  - Skip StreamableVideo rewrite unless caller passed a
    streamableFile; halves disk I/O for chat uploads.
Android: extract METADATA_KEY_LOCATION and write an Apple-style "©xyz"
udta atom into the muxed MP4 so geotags survive transcoding.
iOS: forward asset.metadata plus every available metadata format to the
AVAssetExportSession so location, creation date, and other tags are
retained in the exported file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- fps: derive from frame_count/duration when CAPTURE_FRAMERATE absent.
  Cap 30→60. Drop-gate only when source>target, anchor to ideal grid.
- bitrate: WhatsApp envelope (~1.5 Mbps @ 720p). Android+iOS sync.
- GPS: LocationExtractor walks MP4 — ©xyz, loci, iTunes meta/keys+ilst,
  SEF trailer regex. Writer ©xyz moved to LocationBox class.
- teardown: runCatching every dispose step. join() OutputSurface thread
  after quitSafely to avoid SIGABRT on stale pthread_t.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…encoder/teardown

- LocationExtractor/Compressor: log only location presence + source, never the
  ISO 6709 coordinate string (xyz/itunes/loci/SEF/resolved values)
- Compressor: preflight Dolby Vision profile 5 before allocating
  muxer/encoder/EGL surfaces and drop the throw from prepareDecoder, so the
  unsupported case no longer leaks codec/GL resources on bail-out
- Compressor: restore always-on streamable rewrite (moov atom to front) for
  default output to preserve progressive playback (revert behavior change)
- CompressorUtils/Compressor: make VBR/priority/operating-rate throughput
  tuning optional and fall back to a default-rate-control configure when an
  encoder rejects the tuned format
- Compressor: release partially-initialized encoder/decoder/EGL surfaces on any
  setup failure or in-loop throw (dispose tolerates null handles)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MP4Builder opened its FileOutputStream/FileChannel in createMovie() but
only closed them in finishMovie(). Any failure between muxer creation and
a successful finishMovie() leaked the output file handle.

- Add idempotent MP4Builder.close() to release streams without finalizing
- createMovie() now closes its own streams if header writing throws
- Compressor closes the muxer in the setup/in-loop catch, the finishMovie
  catch, and the outer catch (processAudio/extractor.release failures)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `Compressor` HybridObject implementation is instantiated from C++/JNI
by class name (NitroCompressorOnLoad.cpp `kJavaDescriptor`). R8 can't see
that reference, so in release builds it stripped `HybridCompressor` — the
generated `HybridCompressorSpec` carries `@DoNotStrip @Keep`, but the
hand-written impl did not. Result: the module worked in debug but appeared
"not linked" in release (numandev1#406).

Annotate `HybridCompressor` with `@DoNotStrip @Keep`, the same convention
nitrogen and other Nitro modules (e.g. react-native-mmkv) use — no consumer
ProGuard rules needed. Verified with a minified release build: without the
annotation R8 lists the class in usage.txt (removed); with it, the class and
its no-arg constructor are kept in mapping.txt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v2.0.0, android, failed in release build

1 participant