Summary
Since 1.17.0 (PR #392), on iOS Video.compress can resolve "successfully" with an MP4 that contains only an audio track — the video track is silently dropped. The promise resolves with no error, so callers upload/use an audio-only file. 1.16.2 and earlier are fine.
Environment
- react-native-compressor: 1.18.2 (the iOS video path is byte-identical across 1.18.0 / 1.18.1 / 1.18.2)
- iOS (reproduced on the iOS Simulator; real-device confirmation welcome — see note in Root cause)
- Input: 4K HEVC from Photos (3840×2160, ~32 MB, H.265 + AAC)
- Options:
{ compressionMethod: "manual", maxSize: 1080, bitrate: 5000000 } (also reproduces with "auto")
Steps to reproduce
- Pick/copy a 4K HEVC video (e.g. an iPhone
.mov, 3840×2160).
await Video.compress(uri, { compressionMethod: "manual", maxSize: 1080, bitrate: 5000000 })
ffprobe the output.
Expected
Output MP4 has a video track (H.264) + audio.
Actual
Output has 0 video streams, 1 AAC stream (~70 KB / 4.3 s). No error is thrown. Downstream tools report 0 Frames found / corrupted.
# ffprobe of the input (Video.compress argument)
video: hevc 3840x2160, audio: aac (~32 MB)
# ffprobe of the output (Video.compress result)
video: <none>, audio: aac (~70 KB)
Bisect
Root cause (analysis)
NextLevelSessionExporter.setupVideoOutput only creates the video AVAssetWriterInput when writer.canApply(videoOutputConfiguration, .video) == true; otherwise it logs "Unsupported output configuration" and returns with _videoInput = nil. The export loop then writes audio only, yet writer.status ends as .completed, so the call resolves as .success — a silent audio-only result. (validateVideoOutputConfiguration() only checks for the presence of width/height keys.)
#392 rewrote the videoOutputConfiguration that VideoMain.swift passes to the exporter. Versus 1.16.x it:
- adds
AVVideoExpectedSourceFrameRateKey and AVVideoAverageNonDroppableFrameRateKey to AVVideoCompressionPropertiesKey;
- changes scaling
AVVideoScalingModeResizeAspectFill → AVVideoScalingModeResizeAspect;
- changes
AVVideoWidthKey / AVVideoHeightKey value type Float → Int;
- replaces the dimension/bitrate logic (
scaledDimensions / estimateBitrate).
The same configuration is tolerated by macOS AVFoundation (standalone test: canApply == true, video preserved) but drops the video track on the iOS encoder, so the failure is specific to the iOS encoder + the new config. The two added frame-rate keys are the prime suspects (AVVideoAverageNonDroppableFrameRateKey in particular is not a documented compression property for avc1/H.264). Because canApply can still return true, the gate in setupVideoOutput doesn't catch it.
Also: the onFailure fallback #392 added only triggers when the export ends as .failure. Here it ends as .completed (audio succeeded), so the audio-only result is returned as success.
Suggested fix
- Remove
AVVideoAverageNonDroppableFrameRateKey (and likely AVVideoExpectedSourceFrameRateKey) from the H.264 compressionDict, or gate them behind verified-supported values; and/or
- After export, assert the output actually contains a video track and surface a real error (or fall back to the original) instead of silently returning an audio-only file.
References
Summary
Since 1.17.0 (PR #392), on iOS
Video.compresscan resolve "successfully" with an MP4 that contains only an audio track — the video track is silently dropped. The promise resolves with no error, so callers upload/use an audio-only file. 1.16.2 and earlier are fine.Environment
{ compressionMethod: "manual", maxSize: 1080, bitrate: 5000000 }(also reproduces with"auto")Steps to reproduce
.mov, 3840×2160).await Video.compress(uri, { compressionMethod: "manual", maxSize: 1080, bitrate: 5000000 })ffprobethe output.Expected
Output MP4 has a video track (H.264) + audio.
Actual
Output has 0 video streams, 1 AAC stream (~70 KB / 4.3 s). No error is thrown. Downstream tools report
0 Frames found/ corrupted.Bisect
ios/Video/*betweenv1.16.2andv1.17.0is Triage issues and harden high-resolution video compression paths #392 (afa4da7).VideoMain.swift/NextLevelSessionExporter.swiftare byte-identical across 1.18.0 / 1.18.1 / 1.18.2, so all are affected.value of type 'NextLevelSessionExporter' has no member 'error'#394 — so the practically-affected released versions are 1.18.1 / 1.18.2.)Root cause (analysis)
NextLevelSessionExporter.setupVideoOutputonly creates the videoAVAssetWriterInputwhenwriter.canApply(videoOutputConfiguration, .video) == true; otherwise it logs"Unsupported output configuration"and returns with_videoInput = nil. The export loop then writes audio only, yetwriter.statusends as.completed, so the call resolves as.success— a silent audio-only result. (validateVideoOutputConfiguration()only checks for the presence of width/height keys.)#392 rewrote the
videoOutputConfigurationthatVideoMain.swiftpasses to the exporter. Versus 1.16.x it:AVVideoExpectedSourceFrameRateKeyandAVVideoAverageNonDroppableFrameRateKeytoAVVideoCompressionPropertiesKey;AVVideoScalingModeResizeAspectFill→AVVideoScalingModeResizeAspect;AVVideoWidthKey/AVVideoHeightKeyvalue typeFloat→Int;scaledDimensions/estimateBitrate).The same configuration is tolerated by macOS AVFoundation (standalone test:
canApply == true, video preserved) but drops the video track on the iOS encoder, so the failure is specific to the iOS encoder + the new config. The two added frame-rate keys are the prime suspects (AVVideoAverageNonDroppableFrameRateKeyin particular is not a documented compression property foravc1/H.264). BecausecanApplycan still returntrue, the gate insetupVideoOutputdoesn't catch it.Also: the
onFailurefallback #392 added only triggers when the export ends as.failure. Here it ends as.completed(audio succeeded), so the audio-only result is returned as success.Suggested fix
AVVideoAverageNonDroppableFrameRateKey(and likelyAVVideoExpectedSourceFrameRateKey) from the H.264compressionDict, or gate them behind verified-supported values; and/orReferences
value of type 'NextLevelSessionExporter' has no member 'error'#394 (separate regression from the same PR), feat: add stripAudio option to remove audio track during video compre… #393 (addedstripAudio, first shipped in 1.18.0).