Swift - Firestore - Large video upload fails

So the use case I want to do is easy.
  1. ) Select a video from library

  2. ) Trim video and apply a CIFilter 

  3. ) Upload it to FireStore

Here is my code:

1.) Select a video from library

Code Block
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
    var image = UIImage()
      
    let applyEffectController = STORYBOARD.instantiateViewController(withIdentifier: "applyEffects") as! ApplyEffectsViewController
           
    if let mediaType = info[UIImagePickerController.InfoKey.mediaType] as? String {
      if mediaType == "public.image" {
        let pickedImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage
        image = pickedImage!
      }
            
      if mediaType == "public.movie" {
    
        let videoURL = info[.mediaURL] as! URL
        image = AVUtil.createThumbnail(videoURL: videoURL)
         
        let avAsset = AVUtil.trimVideo(videoURL: videoURL)
        let fileManager = FileManager.default
        let documents = try! fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
         
        try! avAsset.export(to: documents.appendingPathComponent(VIDEO_NAME))
         
        applyEffectController.avAsset = avAsset
        applyEffectController.IS_VIDEO_SELECTED = true
      }
    }
    self.dismiss(animated: true, completion: nil)
          
    applyEffectController.challengeImage = image
    self.navigationController?.pushViewController(applyEffectController, animated: true)
  }

2.) Trim video and apply a CIFilter 

Code Block
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    var chosenFilter = filterImagesArray[indexPath.row]
    chosenFilter = stripFileExtension(chosenFilter)
     
    let contentFilter = CIFilter(name: chosenFilter)
    let challengeCIImage = CIImage(image: challengeImage)
         
    contentFilter!.setValue(challengeCIImage, forKey: kCIInputImageKey)
    let editedChallengeImage = contentFilter!.value(forKey: kCIOutputImageKey) as! CIImage
    challengeImageView.image = UIImage(ciImage: editedChallengeImage)
    
    if (IS_VIDEO_SELECTED) {
      playerItem?.videoComposition = AVVideoComposition(asset: avAsset!, applyingCIFiltersWithHandler: { (request) in
        let source = request.sourceImage.clampedToExtent()
        contentFilter?.setValue(source, forKey: kCIInputImageKey)
         
        _ = CMTimeGetSeconds(request.compositionTime)
         
        let output = contentFilter?.outputImage!.cropped(to: request.sourceImage.extent)
         
        request.finish(with: output!, context: nil)
      })
    }
  }

2.1 Confirm filter and redirect to the view controller where I do the upload

Code Block
@objc func confirmButton_clicked() {
    let createChallengeController = STORYBOARD.instantiateViewController(withIdentifier: "createChallenge") as! CreateChallengeViewController
    createChallengeController.IS_VIDEO_SELECTED = IS_VIDEO_SELECTED
    createChallengeController.challengeImage = challengeImageView.image!
    createChallengeController.challengeObject = challengeObject
         
    if (IS_VIDEO_SELECTED) {
      let fileManager = FileManager.default
      let documents = try! fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
       
      if (fileManager.fileExists(atPath: documents.appendingPathComponent(EDITED_VIDEO_NAME).path)) {
        DispatchQueue.main.async(execute: {
          try! fileManager.removeItem(atPath: documents.appendingPathComponent(EDITED_VIDEO_NAME).path)
        })
      }
       
      let videoExport = AVAssetExportSession(asset: (playerItem?.asset)!, presetName: AVAssetExportPresetMediumQuality)
      videoExport?.outputFileType = .mov
      videoExport?.outputURL = documents.appendingPathComponent(EDITED_VIDEO_NAME)
      videoExport?.videoComposition = playerItem?.videoComposition
      createChallengeController.challengeImage = self.challengeImageView.image!
      print(videoExport?.outputURL)
       
      let group = DispatchGroup()
      group.enter()
      self.view.addSubview(activityView)
       
      videoExport?.exportAsynchronously(completionHandler: {
        createChallengeController.videoURL = videoExport?.outputURL
        group.leave()
      })
       
      group.notify(queue: .main) {
        self.navigationController?.pushViewController(createChallengeController, animated: true)
        self.activityView.removeFromSuperview()
      }
    }
    else {
      self.navigationController?.pushViewController(createChallengeController, animated: true)
    }
  }

  • *3.) Upload it to FireStore



Code Block
var videoData = NSData()
do {
videoData = try NSData(contentsOf: self.videoURL!, options: .dataReadingMapped)
}
catch {
cancelChallenge(challengeId: challengeId) // always goes here -> it fails
return
}

The exception I get is:
"The file “editedVideo.mov” couldn’t be opened because there is no such file."

The approach above works with videos which are about 30-35 sec. (without trmming). If I upload a video which is about two minutes, then it always fails.

I need your help. Thank you and stay healthy!


  • *

Replies

What do you get if you put the following code between line 29 and 30 of your confirmButton_clicked?
Code Block
if let exportSession = videoExport {
print(exportSession.status, exportSession.error)
}


Hello @OOper,

thanks for the reply. I can always count on you in this forum :)
I put the code snippet. This is what it logs


AVAssetExportSessionStatus Optional(Error Domain=AVFoundationErrorDomain Code=-11838 "Operation Stopped" UserInfo={NSLocalizedFailureReason=The operation is not supported for this media., NSLocalizedDescription=Operation Stopped, NSUnderlyingError=0x28376e250 {Error Domain=NSOSStatusErrorDomain Code=-16976 "(null)"}})


This is what it logs

Thanks for testing.
(exportSession.status should be exportSession.status.rawValue to show it in logs, but it does not have significant meanings when error is not nil.)
One thing sure is that you should better check the error property of the AVAssetExportSession inside the completionHandler of exportAsynchronously. The completionHandler may be called on some errors and you should better handle such cases.


Unfortunately, the error shown does not give us much clue about how to fix.

The code -11838 represents AVErrorOperationNotSupportedForAsset as shown in the description The operation is not supported for this media.
I could not get any reliable info about Error Domain=NSOSStatusErrorDomain Code=-16976.

Someone reported that changing the quality of the AVAssetExportSession fixed his issue.
(https://stackoverflow.com/a/53963805/6541007)
Another told us that outputFileType was also important.
(https://stackoverflow.com/a/59687466/6541007)

And the file size (or at least, the video length) may also be affecting as you described.


One more.
Seeing line 13...15 of confirmButton_clicked, fileManager.removeItem(atPath:)would be executed after exportAsynchronously(completionHandler:) is started. It might be executed while exportAsynchronously(completionHandler:) is running.

I'm not sure if this could be the cause of your issue, but you should better fix this issue soon as it may cause other issues.
Hi @OOPer,

yuhuu :) I found out the cause and a solution.
All video were failing which were longer than 30 seconds. This made me look into the method where I do trim the video

Code Block   
func assetByTrimming(timeOffStart: Double) throws -> AVAsset {
    let duration = CMTime(seconds: timeOffStart, preferredTimescale: 1)
    let timeRange = CMTimeRange(start: CMTime.zero, duration: duration)
         
    let composition = AVMutableComposition()
    let videoTrack = self.tracks(withMediaType: AVMediaType.video).first
        
    let size = videoTrack!.naturalSize 
    let txf = videoTrack!.preferredTransform
     
    var recordType = ""
    if (size.width == txf.tx && size.height == txf.ty){
      recordType = "UIInterfaceOrientationLandscapeRight"
    }
    else if (txf.tx == 0 && txf.ty == 0){
      recordType = "UIInterfaceOrientationLandscapeLeft"
    }
    else if (txf.tx == 0 && txf.ty == size.width){
      recordType = "UIInterfaceOrientationPortraitUpsideDown"
    }
    else{
      recordType = "UIInterfaceOrientationPortrait"
    }
     
    do {
      for track in tracks {
        // let compositionTrack = composition.addMutableTrack(withMediaType: track.mediaType, preferredTrackID: track.trackID)
        // try compositionTrack?.insertTimeRange(timeRange, of: track, at: CMTime.zero)
         
        if let videoCompositionTrack = composition.addMutableTrack(withMediaType: track.mediaType, preferredTrackID: kCMPersistentTrackID_Invalid) {
          try videoCompositionTrack.insertTimeRange(timeRange, of: videoTrack!, at: CMTime.zero)
           
          if recordType == "UIInterfaceOrientationPortrait" {
            let t1: CGAffineTransform = CGAffineTransform(translationX: videoTrack!.naturalSize.height, y: -(videoTrack!.naturalSize.width - videoTrack!.naturalSize.height)/2)
            let t2: CGAffineTransform = t1.rotated(by: CGFloat(Double.pi / 2))
            let finalTransform: CGAffineTransform = t2
            videoCompositionTrack.preferredTransform = finalTransform
          }
          else if recordType == "UIInterfaceOrientationLandscapeRight" {
            let t1: CGAffineTransform = CGAffineTransform(translationX: videoTrack!.naturalSize.height, y: -(videoTrack!.naturalSize.width - videoTrack!.naturalSize.height)/2)
            let t2: CGAffineTransform = t1.rotated(by: -CGFloat(Double.pi))
            let finalTransform: CGAffineTransform = t2
            videoCompositionTrack.preferredTransform = finalTransform
          }
          else if recordType == "UIInterfaceOrientationPortraitUpsideDown" {
            let t1: CGAffineTransform = CGAffineTransform(translationX: videoTrack!.naturalSize.height, y: -(videoTrack!.naturalSize.width - videoTrack!.naturalSize.height)/2)
            let t2: CGAffineTransform = t1.rotated(by: -CGFloat(Double.pi/2))
            let finalTransform: CGAffineTransform = t2
            videoCompositionTrack.preferredTransform = finalTransform
          }
        }
      }
    } catch let error {
      throw TrimError("error during composition", underlyingError: error)
    }
    return composition
  }

Looks like, the output of this file changes the media type which is then not supported. Instead, to trim the video, I can apply a more simple solution

Code Block   
let startTime = CMTime(seconds: Double(0), preferredTimescale: 1000)
    let endTime = CMTime(seconds: Double(30), preferredTimescale: 1000)
    let timeRange = CMTimeRange(start: startTime, end: endTime)
     
    exportSession.outputURL = destination
    exportSession.outputFileType = .mov
    exportSession.shouldOptimizeForNetworkUse = true
    exportSession.timeRange = timeRange // trim video here


Do you know what exactly might be the cause why my assetsByTrimming method causes an error?
Thank you again for your assistance!

 I found out the cause and a solution.

Thanks for sharing the info.

Do you know what exactly might be the cause why my assetsByTrimming method causes an error?

Frankly, no idea. And could not find any reliable info about this on the web.
If you find something, please tell us what may cause such an error..