Skip to content

Fix/import nitro#407

Merged
numandev1 merged 9 commits into
numandev1:mainfrom
HuuNguyen312:fix/import_nitro
Jun 16, 2026
Merged

Fix/import nitro#407
numandev1 merged 9 commits into
numandev1:mainfrom
HuuNguyen312:fix/import_nitro

Conversation

@HuuNguyen312

Copy link
Copy Markdown
Contributor

Summary

Fixes #404.

On older React Native versions, the Android build of the Nitro module fails to compile with:

Class 'NitroPromiseAdapter' is not abstract and does not implement abstract members:
fun reject(code: String, message: String?): Unit
fun reject(code: String, throwable: Throwable?): Unit
fun reject(code: String, message: String?, throwable: Throwable?): Unit
fun reject(code: String, userInfo: WritableMap): Unit
fun reject(code: String, throwable: Throwable?, userInfo: WritableMap): Unit
fun reject(code: String, message: String?, userInfo: WritableMap): Unit

Why this happens — a React Native version difference, surfaced by Kotlin's type rules:

The bridge Promise interface declares the code parameter of reject(...) differently across RN versions:

React Native code in reject(...)
Older RN code: Stringnon-null
RN 0.85 code: String?nullable

NitroPromiseAdapter was written in Kotlin with code: String? to match RN 0.85. Since Kotlin override parameter types are invariant, a single Kotlin source can only match one nullability variant — it compiles on RN 0.85 but breaks on
older RN (the 6 overloads above stop counting as "implemented"). No single Kotlin source can satisfy both ranges at once.

The fix: Rewrite NitroPromiseAdapter in Java. Java erases nullability annotations when matching overrides, so one Java implementation satisfies the reject(...) contract on every RN version — matching the library's "react-native": "*" peer range. The adapter is the only class in the repo that implements the bridge Promise; the single construction site in HybridCompressor.kt needs no change because the Kotlin lambdas ((Any?) -> T, () -> Unit) flow straight into
the Java Function1 / Function0 constructor params.

Changelog

[ANDROID] [FIXED] - Rewrite NitroPromiseAdapter in Java so the Nitro module compiles across all React Native versions (older RN declared Promise.reject's code as non-null, RN 0.85 as nullable; Kotlin's invariant override params could not
satisfy both) (#404)

Test Plan

  • Ran local JS PR gate: yarn test:pr

Test Suites: 1 passed, 1 total
Tests: 13 passed, 13 total
✖ 9 problems (0 errors, 9 warnings) # pre-existing warnings only
typecheck: pass

  • Library module compiles (Kotlin call site + new Java adapter) on RN 0.85:

./gradlew :react-native-compressor:compileDebugKotlin :react-native-compressor:compileDebugJavaWithJavac
BUILD SUCCESSFUL

  • Ran Harness on Android (Pixel_9a emulator, rebuilt debug APK):

yarn test:harness:android
Test Suites: 1 passed, 1 total
Tests: 9 passed, 9 total

  • Ran Harness on iOS (iPhone 17 Pro / iOS 26.4) as a regression check (iOS native untouched):

yarn test:harness:ios
Test Suites: 1 passed, 1 total
Tests: 9 passed, 9 total

Note: the failing path (non-null code) is the older-RN signature reported in #404. It can't be compiled on this machine since the repo targets RN 0.85, but the Java-erasure approach is exactly what makes the adapter valid across both
signature variants.

HuuNguyen312 and others added 9 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 bridge `Promise` interface declares `reject(...)`'s `code` parameter
as non-null `String` on older React Native versions but nullable
`String?` on RN 0.85. Kotlin override parameter types are invariant, so
the Kotlin adapter (which matched `String?`) failed to compile on older
RN with "is not abstract and does not implement abstract members".

Rewrite the adapter in Java: Java erases nullability annotations for
override matching, so a single implementation satisfies every RN version
(matching the `react-native: "*"` peer range). No call-site changes —
the Kotlin lambdas pass straight into the Java `Function1`/`Function0`
constructor params.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@numandev1 numandev1 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks

@numandev1 numandev1 merged commit 150edd0 into numandev1:main Jun 16, 2026
4 checks passed
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.

Version 2.0 does not import 'nitro'

2 participants