Skip to content

fix(video): fail iOS export instead of silently returning an audio-only MP4 (#400)#403

Merged
numandev1 merged 1 commit into
numandev1:mainfrom
hjick:fix/silent-audio-only-export-400
Jun 11, 2026
Merged

fix(video): fail iOS export instead of silently returning an audio-only MP4 (#400)#403
numandev1 merged 1 commit into
numandev1:mainfrom
hjick:fix/silent-audio-only-export-400

Conversation

@hjick

@hjick hjick commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes the remaining half of #400 (iOS: Video.compress silently drops the video track and still resolves as success).

The configuration root cause (the undocumented frame-rate keys added in #392) was already removed in #402VideoMain.swift now carries a NOTE about it. However, the exporter itself can still produce a silent audio-only success, which is the failure mode that made #400 so damaging (callers upload corrupted files without any error):

  1. setupVideoOutput() swallowed writer rejections. When AVAssetWriter.canApply(outputSettings:forMediaType:) returned false, it only printed "Unsupported output configuration", exported audio only, and completed with .success. It now aborts the export with a new unsupportedVideoOutputConfiguration error.

  2. complete() trusted .completed blindly. As analyzed in iOS: Video.compress silently drops the video track (audio-only output) since 1.17.0 (#392) #400, the iOS encoder can accept a configuration via canApply and still drop the video track at write time, ending as .completed. complete() now verifies that an output produced from a video source actually contains a video track; if not, it deletes the bad file and fails with missingVideoTrackInOutput.

With both guards, any future regression of this class surfaces as an explicit error on the JS side instead of a corrupted upload.

Changes

  • ios/Video/NextLevelSessionExporter.swift only
  • Two new NextLevelSessionExporterError cases (existing cases untouched)
  • setupVideoOutput(withAsset:) returns Bool (private API; audio-only assets still return true and behave as before)
  • Post-export video-track verification in complete()

Testing

Refs #400, #392, #402

…ly MP4

Completes the remaining half of numandev1#400. The root-cause configuration fix
already landed in numandev1#402, but the exporter could still resolve successfully
while dropping the video track:

- setupVideoOutput() only printed "Unsupported output configuration" when
  AVAssetWriter rejected the video output settings, then exported audio
  only and completed with .success. It now fails the export with a new
  unsupportedVideoOutputConfiguration error.
- Even when canApply(...) returns true, the iOS encoder can drop the video
  track at write time and the writer still ends as .completed (the exact
  failure mode of numandev1#400). complete() now verifies that an output produced
  from a video source actually contains a video track, deletes the bad
  file, and fails with missingVideoTrackInOutput otherwise.
@XChikuX

XChikuX commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Could you please replace the function prepareDecoder in android/src/main/java/com/reactnativecompressor/Video/VideoCompressor/compressor/Compressor.kt

Before you merge this.. @numandev1's previous review on my PR was not addressed

// Function to prepare the video decoder
    private fun prepareDecoder(
        inputFormat: MediaFormat,
        outputSurface: OutputSurface,
    ): MediaCodec {
        
        // Some inputs (e.g. iPhone .MOV files) report a "video/dolby-vision" MIME
        // type that many devices cannot decode. Remap to a decodable base-layer
        // codec, or fail with a clear error, before creating the decoder (#398).
        ensureDecodableVideoFormat(inputFormat)

        // Clear Dolby Vision specific profile and level to prevent configuration failures
        // when the MIME type has been remapped to AVC/HEVC.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            inputFormat.removeKey(MediaFormat.KEY_PROFILE)
            inputFormat.removeKey(MediaFormat.KEY_LEVEL)
        }
        // ---------------------------------

        val decoder = MediaCodec.createDecoderByType(inputFormat.getString(MediaFormat.KEY_MIME)!!)
        
        decoder.configure(inputFormat, outputSurface.getSurface(), null, 0)

        return decoder
    }

Changes look good otherwise.

@hjick

hjick commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Changes look good otherwise.

Got it. Thank u for the review bro😀

@numandev1 numandev1 merged commit fca5d2e into numandev1:main Jun 11, 2026
@numandev1

Copy link
Copy Markdown
Owner

@XChikuX will add prepareDecoder in next PR

@numandev1

Copy link
Copy Markdown
Owner

@XChikuX added

@numandev1

Copy link
Copy Markdown
Owner

relased in 2.0.0

@XChikuX

XChikuX commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

@numandev1 Could you please add the fix to v1 version.

I don't feel like v2 is really tested properly. I'd be happier sticking to v1 for now.

@numandev1

Copy link
Copy Markdown
Owner

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.

3 participants