Skip to content

Commit fca5d2e

Browse files
authored
fix(video): fail iOS export instead of silently returning an audio-only MP4 (#400) (#403)
1 parent e27f8f1 commit fca5d2e

1 file changed

Lines changed: 43 additions & 8 deletions

File tree

ios/Video/NextLevelSessionExporter.swift

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ public enum NextLevelSessionExporterError: Error, CustomStringConvertible {
1313
case readingFailure
1414
case writingFailure
1515
case cancelled
16-
16+
case unsupportedVideoOutputConfiguration
17+
case missingVideoTrackInOutput
18+
1719
public var description: String {
1820
get {
1921
switch self {
@@ -25,6 +27,10 @@ public enum NextLevelSessionExporterError: Error, CustomStringConvertible {
2527
return "Writing failure"
2628
case .cancelled:
2729
return "Cancelled"
30+
case .unsupportedVideoOutputConfiguration:
31+
return "The writer rejected the video output configuration"
32+
case .missingVideoTrackInOutput:
33+
return "Export finished without a video track in the output"
2834
}
2935
}
3036
}
@@ -249,7 +255,16 @@ extension NextLevelSessionExporter {
249255
}
250256
}
251257

252-
self.setupVideoOutput(withAsset: asset)
258+
// Fail loudly when the writer rejects the video output configuration.
259+
// Continuing here used to export only the audio track and still resolve
260+
// as a success (issue #400).
261+
guard self.setupVideoOutput(withAsset: asset) else {
262+
DispatchQueue.main.async {
263+
self._completionHandler?(.failure(NextLevelSessionExporterError.unsupportedVideoOutputConfiguration))
264+
self._completionHandler = nil
265+
}
266+
return
267+
}
253268
if !self.stripAudio {
254269
self.setupAudioOutput(withAsset: asset)
255270
self.setupAudioInput()
@@ -316,11 +331,14 @@ extension NextLevelSessionExporter {
316331

317332
extension NextLevelSessionExporter {
318333

319-
private func setupVideoOutput(withAsset asset: AVAsset) {
334+
/// Returns `false` when the asset has video tracks but a video writer input
335+
/// could not be created — exporting would silently produce an audio-only
336+
/// file (see numandev1/react-native-compressor#400).
337+
private func setupVideoOutput(withAsset asset: AVAsset) -> Bool {
320338
let videoTracks = asset.tracks(withMediaType: AVMediaType.video)
321-
339+
322340
guard videoTracks.count > 0 else {
323-
return
341+
return true
324342
}
325343

326344
self._videoOutput = AVAssetReaderVideoCompositionOutput(videoTracks: videoTracks, videoSettings: self.videoInputConfiguration)
@@ -344,8 +362,8 @@ extension NextLevelSessionExporter {
344362
self._videoInput = AVAssetWriterInput(mediaType: AVMediaType.video, outputSettings: self.videoOutputConfiguration)
345363
self._videoInput?.expectsMediaDataInRealTime = self.expectsMediaDataInRealTime
346364
} else {
347-
print("Unsupported output configuration")
348-
return
365+
print("NextLevelSessionExporter, unsupported video output configuration, failing export instead of writing an audio-only file")
366+
return false
349367
}
350368

351369
if let writer = self._writer,
@@ -367,8 +385,9 @@ extension NextLevelSessionExporter {
367385

368386
self._pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: videoInput, sourcePixelBufferAttributes: pixelBufferAttrib)
369387
}
388+
return true
370389
}
371-
390+
372391
private func setupAudioOutput(withAsset asset: AVAsset) {
373392
let audioTracks = asset.tracks(withMediaType: AVMediaType.audio)
374393
var audioTracksToUse: [AVAssetTrack] = []
@@ -632,6 +651,22 @@ extension NextLevelSessionExporter {
632651
break
633652
}
634653

654+
// Guard against silently returning an audio-only file: when the source
655+
// has a video track and a video output was configured, the exported file
656+
// must contain a video track as well. A configuration the encoder rejects
657+
// at write time can still end with `.completed` while the video track is
658+
// dropped (issue #400) — surface that as an error instead of a success.
659+
if self.videoOutputConfiguration != nil,
660+
let asset = self.asset,
661+
let outputURL = self.outputURL,
662+
asset.tracks(withMediaType: AVMediaType.video).count > 0,
663+
AVAsset(url: outputURL).tracks(withMediaType: AVMediaType.video).count == 0 {
664+
try? FileManager.default.removeItem(at: outputURL)
665+
self._completionHandler?(.failure(NextLevelSessionExporterError.missingVideoTrackInOutput))
666+
self._completionHandler = nil
667+
return
668+
}
669+
635670
self._completionHandler?(.success(self.status))
636671
self._completionHandler = nil
637672
}

0 commit comments

Comments
 (0)