fix(video): Dolby Vision crash, 50% faster transcode, fps/bitrate/GPS fixes#399
fix(video): Dolby Vision crash, 50% faster transcode, fps/bitrate/GPS fixes#399HuuNguyen312 wants to merge 5 commits into
Conversation
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>
There was a problem hiding this comment.
Pull request overview
This PR updates the video (and some image) compression pipelines to improve Android stability/performance and preserve important metadata across transcodes, while aligning bitrate/fps behavior across Android and iOS.
Changes:
- Fix Android Dolby Vision
.MOVcrashes by remappingvideo/dolby-visionto HEVC decoding where possible, and tightening codec selection/teardown behavior. - Improve Android transcode throughput via better decoder feeding, frame dropping logic when downsampling fps, dedicated
SurfaceTexturecallback thread, and hardware AVC encoder selection. - Align output characteristics and metadata handling: raise fps cap to 60, reduce bitrate “envelope”, and preserve GPS/location/asset metadata (Android MP4
©xyz+ iOS exporter metadata forwarding). Also improves Android image EXIF copy path handling.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| ios/Video/VideoMain.swift | Raises fps cap to 60, halves bitrate bands, and forwards asset metadata into the exporter. |
| android/src/main/java/com/reactnativecompressor/Video/VideoCompressorHelper.kt | Adds derived fps detection fallback (frameCount/duration) and uses it in manual compression. |
| android/src/main/java/com/reactnativecompressor/Video/AutoVideoCompression.kt | Switches auto compression to use the new derived fps detection helper. |
| android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/OutputSurface.kt | Moves onFrameAvailable onto a dedicated HandlerThread, improves teardown, and increases frame wait timeout. |
| android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/Mp4Movie.kt | Adds a location field to carry GPS through MP4 building. |
| android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/MP4Builder.kt | Writes GPS into moov/udta/©xyz when available to preserve location metadata. |
| android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/videoHelpers/LocationBox.kt | Introduces an MP4 ©xyz box implementation for persisting ISO 6709 location strings. |
| android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/utils/LocationExtractor.kt | Adds a raw MP4 box walker + Samsung trailer scan to extract GPS when retriever misses it. |
| android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/utils/CompressorUtils.kt | Adds MP4 movie setup support for location and switches encoder bitrate mode/settings for throughput. |
| android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/compressor/Compressor.kt | Implements DV mime remap, improved decoder feeding, conditional frame dropping, smarter encoder selection, streamable rewrite gating, and safer teardown. |
| android/src/main/java/com/reactnativecompressor/Video/VideoCompressionProfile.kt | Raises max fps cap to 60 and updates bitrate bands to match iOS envelope. |
| android/src/main/java/com/reactnativecompressor/Image/ImageCompressor.kt | Normalizes URI strings to filesystem paths for EXIF operations and ensures output stream is closed before EXIF writes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Log.i( | ||
| TAG, | ||
| "LocationExtractor box scan: xyz=${state.xyz} itunes=${state.itunesLocation} loci=${state.loci} chosen=$viaBox" | ||
| ) |
| val retrievedLocation = | ||
| mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION) | ||
| val locationData = if (!retrievedLocation.isNullOrEmpty()) { | ||
| retrievedLocation | ||
| } else { | ||
| LocationExtractor.extract(context, srcUri) | ||
| } | ||
| Log.i("Compressor", "source location resolved: $locationData (retriever=$retrievedLocation)") |
| -1 | ||
| } | ||
| // DV profile 5 = 0x20, no HEVC fallback. Profiles 8.x carry HEVC base layer. | ||
| if (profile == 0x20) { | ||
| throw IllegalStateException("Dolby Vision profile 5 has no HEVC base layer; cannot transcode") | ||
| } | ||
| inputFormat.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_HEVC) |
| } else { | ||
| LocationExtractor.extract(context, srcUri) | ||
| } | ||
| Log.i("Compressor", "source location resolved: $locationData (retriever=$retrievedLocation)") |
There was a problem hiding this comment.
Preserving the source location in the compressed video is fine, but we should not print the exact gps/location string in logcat because it exposes user coordinates
please log only something like hasLocation=true or locationSource=retriever/extractor
| val viaBox = chooseBest(state) | ||
| Log.i( | ||
| TAG, | ||
| "LocationExtractor box scan: xyz=${state.xyz} itunes=${state.itunesLocation} loci=${state.loci} chosen=$viaBox" |
There was a problem hiding this comment.
this logs the actual extracted gps values like xyz, itunes, loci and chosen location
location preservation is okay, but exact coordinates should not be logged
please replace this with booleans like hasXyz, hasItunesLocation, hasLoci and hasChosenLocation
| buf.get(bytes) | ||
| val text = String(bytes, StandardCharsets.ISO_8859_1) | ||
| val match = ISO6709_REGEX.find(text) | ||
| Log.i(TAG, "LocationExtractor SEF trailer scan match=${match?.value}") |
There was a problem hiding this comment.
this can print the exact gps value found from the samsung sef trailer
please avoid logging match.value and log only whether a match was found
| } | ||
| // DV profile 5 = 0x20, no HEVC fallback. Profiles 8.x carry HEVC base layer. | ||
| if (profile == 0x20) { | ||
| throw IllegalStateException("Dolby Vision profile 5 has no HEVC base layer; cannot transcode") |
There was a problem hiding this comment.
this throw can happen after the encoder and surfaces are already created and started
because the catch returns without calling dispose, encoder and egl resources can leak and may break the next compression
please preflight dolby vision before creating encoder/inputSurface/outputSurface or add a finally cleanup path for partially initialized resources
| // StreamableVideo rewrites the whole MP4 to move the moov atom to the front, | ||
| // which doubles disk I/O. Only run it when the caller explicitly requested a | ||
| // streamable copy (non-null streamableFile). Chat uploads do not need it. | ||
| if (streamableFile != null) { |
There was a problem hiding this comment.
this changes the previous default behavior
before this PR, StreamableVideo was also run when streamableFile was null, so default compressed mp4 output was browser/progressive playback friendly by moving the moov atom to the front
please confirm this behavior change is intentional or keep the old default behavior
| setInteger( | ||
| MediaFormat.KEY_BITRATE_MODE, | ||
| MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR | ||
| MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR |
There was a problem hiding this comment.
this forces vbr, priority and max operating rate for every encoder
some android encoders may reject these settings during configure
please guard this with encoder capability checks or fallback to cbr/default settings if configure fails
…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>
|
@numandev1 I've resolved all the comments, please help me review them |
numandev1
left a comment
There was a problem hiding this comment.
please fix this last comment
| val movie = setUpMP4Movie(rotation, cacheFile, location) | ||
|
|
||
| // MediaMuxer outputs MP4 in this app | ||
| val mediaMuxer = MP4Builder().createMovie(movie) |
There was a problem hiding this comment.
mediaMuxer is created before encoder setup
if prepareEncoder or later setup fails before finishMovie, MP4Builder file streams may stay open because the catch only disposes codec and surface resources
please add MP4Builder cleanup in the failure path or create it after encoder/decoder setup succeeds
Summary
Bug (#398) — Dolby Vision crash on .MOV from iPhone:
createDecoderByType("video/dolby-vision") throws on Android — no standalone DV decoder. DV profile 8.x has HEVC base layer, so remap mime → video/hevc. Reject profile 5 (0x20) explicitly (no HEVC fallback).
Perf (#384) — ~50% faster transcode:
fps detection:
Bitrate:
GPS preservation:
Stability:
Changelog
[ANDROID] [FIXED] Dolby Vision .MOV no longer crashes (#398)
[ANDROID] [CHANGED] Transcode pipeline ~50% faster (#384)
[ANDROID] [FIXED] Source fps derived from frame count when CAPTURE_FRAMERATE absent
[ANDROID] [FIXED] GPS metadata preserved via raw MP4 box walker + Samsung SEF scan
[iOS] [FIXED] fps cap raised to 60; bitrate bands halved; source metadata forwarded
[ANDROID] [FIXED] Codec teardown wrapped in runCatching; OutputSurface thread joined
Test Plan