AVMutableComposition Merged Videos Not Saving in Correct Order - Swift 3

I am having trouble merging my videos in the correct order even though I am waiting for them to be done with a AVAsynchronousKeyValueLoading. The videos are playing back perfectly fine, but I can't the final result to show in the correct order.


At first I thought it was an issue with my videos being saved to the NSTemporaryDirectory, but I made sure that they were being saved correctly. (I can provide logs upon request).


Here is the order that I want the videos to playback in:



1st video --> 2nd video --> 3rd video



Here is the order that it is playing back in:



2nd video --> 1st video --> 3rd video



It works perfectly fine when I don't switch the camera though. Can anyone please help? It would be greatly appreciated and put me back on schedule for a production release of my app.


func mergeVideo()
  var mixComposition = AVMutableComposition()
  var resultMergedVideoTime = CMTime(seconds: 0, preferredTimescale: 1)
  var resultTrack = mixComposition.addMutableTrack(withMediaType: AVMediaTypeVideo, preferredTrackID: Int32(kCMPersistentTrackID_Invalid))

  let myGroup = DispatchGroup()

  for var i in 0..<self.resultVideoAssets.keys.count {
  myGroup.enter()
  let asset = Array(self.resultVideoAssets.keys)[i]
  asset.loadValuesAsynchronously(forKeys: ["playable"], completionHandler: {
  if asset.statusOfValue(forKey: "playable", error: nil) == .loaded {
  do {

  let videoDuration: CMTime = asset.duration
  let videoTrack = asset.tracks(withMediaType: AVMediaTypeVideo).first!
  var audioTrack:AVAssetTrack?
  if asset.tracks(withMediaType: AVMediaTypeAudio).count > 0 {
  audioTrack = asset.tracks(withMediaType: AVMediaTypeAudio).first
  }

  let timeRange = CMTimeRange(start: kCMTimeZero, duration: videoDuration)
  let audioCompTrack = mixComposition.addMutableTrack(withMediaType: AVMediaTypeAudio, preferredTrackID: Int32(kCMPersistentTrackID_Invalid))

  try resultTrack.insertTimeRange(timeRange, of: videoTrack, at: kCMTimeZero)
  resultTrack.preferredTransform = videoTrack.preferredTransform

  if let audioTrack = audioTrack {
  try audioCompTrack.insertTimeRange(timeRange, of: audioTrack, at: resultMergedVideoTime)
  }

  resultMergedVideoTime = CMTimeAdd(resultMergedVideoTime, videoDuration)

  } catch {
  //TODO: Error handler
  }
  }
  myGroup.leave()
  })
 }

  myGroup.notify(queue: .main) {
  let videoURL = NSURL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("mergeVideo\(arc4random()%1000)d")!.appendingPathExtension("mp4")
  let assetExport: AVAssetExportSession
  if #available(iOS 11.0, *) {
  assetExport = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetHEVC1920x1080)!
  } else {
  assetExport = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPreset1920x1080)!
  }
  assetExport.outputURL = videoURL
  assetExport.outputFileType = AVFileTypeMPEG4
  assetExport.shouldOptimizeForNetworkUse = true
  assetExport.exportAsynchronously {
  .........

Accepted Reply

Your code inserts a video as soon as it is loaded; since loading is done in parallel, the final ordering depends on the time it takes a video to load, and not the initial ordering in the array.

Wait until all videos are loaded, then iterate again over the array of videos and do insertTimeRange in a defined order. The right place to do this is probably at the beginning of the notification group handler, before starting to prepare the export.

Replies

Can you explain which part of this code is supposed to order the videos in the correct sequence, because I can't see it. The order at which their metadata (keys) finishes loading doesn't seem relevant, but you have this line:


  try resultTrack.insertTimeRange(timeRange, of: videoTrack, at: kCMTimeZero)


which appears to insert each video in reverse order of the order their keys finish loading. Is the desired order given by the array "resultVideoAssets.keys" (which you don't show the declaration for)?


Surely you want to finish all of the asynchronous loading first, then insert the video track in the proper order afterwards?

I am essentially inserting the video at the given time with the range from 0 to the video's duration.


Yes you are correct, the videos are inserting in reverse/wrong order and that is the problem that I am having. I want to be able to merge all the video files that are in resultVideoAssets (which is basically an array of all the AVAssets that I want to merge) into a single video file for playback.


I thought when I was waiting for the videos to load asynchronously that is when I would insert the videos in the proper order, but please correct me if I am wrong because everytime insert the tracks, they are out of order.


asset.loadValuesAsynchronously(forKeys: ["playable"], completionHandler: { 

Your code inserts a video as soon as it is loaded; since loading is done in parallel, the final ordering depends on the time it takes a video to load, and not the initial ordering in the array.

Wait until all videos are loaded, then iterate again over the array of videos and do insertTimeRange in a defined order. The right place to do this is probably at the beginning of the notification group handler, before starting to prepare the export.

Ok I thought I might've been doing the insert at the wrong time just didn't know where. I'll give it a shot later today and let you know how it goes. Thanks!

It worked! Thanks again for your help! I really appreciate it! Also, thank you for being so quick to provided feedback QuinceyMorris .