2

I am using UIImagePickerController to record short (<30s) videos which are then saved and uploaded via our API. The app is cross-platform and so I need recorded videos to be encoded into mp4 format so that Android devices can play them.

I used instructions from the following questions to create my solution:

Swift - How to record video in MP4 format with UIImagePickerController?

AVFoundation record video in MP4 format

https://forums.developer.apple.com/thread/94762

I record my video through the UIImagePickerController like so:

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
    // Local variable inserted by Swift 4.2 migrator.
    let info = convertFromUIImagePickerControllerInfoKeyDictionary(info)


    let videoNSURL = info[convertFromUIImagePickerControllerInfoKey(UIImagePickerController.InfoKey.mediaURL)] as? NSURL

    videoURL = videoNSURL!.absoluteURL
    if let videoURL = videoURL {
        let avAsset = AVURLAsset(url: videoURL, options: nil)

        avAsset.exportVideo { (exportedURL) in
            if let uploadVC = self.uploadVC {
                uploadVC.incomingFileURL = exportedURL
                uploadVC.myJewelleryID = self.myJewelleryID
                uploadVC.topicID = self.topicID
            }
            DispatchQueue.main.async { [weak self] in
              //Update UI with results from previous closure
                self?.dismiss(animated: true, completion: nil)
                self?.showUploadContainer()
                self?.updateVideoContainerWithURL(url: exportedURL)
            }
        }
    }
}

This then passes the exported MP4 url to the upload container view, where it saves the file to the device:

private func saveVideoFileToDevice() {

    //Filename Struct = [AssetID]_[TopicID]_[CustomerID]_[Datestamp]
    let date = Date()
    let formater = DateFormatter()
    formater.locale = Locale(identifier: "en_US_POSIX")
    formater.dateFormat = "YYYY-MM-dd-HH-mm-ss"

    uploadFileName = ""
    if let mjID = myJewelleryID {
        uploadFileName = "ASID_\(mjID)_\(User.instance.customerID)_\(formater.string(from: date)).mp4"
    } else if let tID = topicID {
        uploadFileName = "ASID_\(tID)_\(User.instance.customerID)_\(formater.string(from: date)).mp4"
    }

    let fileManager = FileManager.default

    if let destURL = URL(string: "file://\(NSHomeDirectory())/Documents/\(uploadFileName!)") {

        var fileData: Data!
        print("destURL = \(destURL)")
        do {
            try fileManager.copyItem(at: incomingFileURL! as URL, to: destURL)
            fileData = try Data(contentsOf: incomingFileURL! as URL)

            try fileData.write(to: destURL)


        }
        catch {
            print("DEBUG: Failed to save video data")
        }
    }
}

and then uploads the file to our API. Although the file is MP4, it does not play on Android. On inspection, the file looks very similar to a file that will actually play on an Android device when we compare the codec data:

Screenshot of codec comparison

Does anyone have any ideas on how I can fix this?

Thanks!

Iain Coleman
  • 166
  • 1
  • 13

2 Answers2

3
var exportSession: AVAssetExportSession!


func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
            picker.dismiss(animated: true, completion: nil)
            
        guard let videoURL = (info[UIImagePickerController.InfoKey.mediaURL] as? URL) else { return }
        encodeVideo(videoURL)
    }

func encodeVideo(_ videoURL: URL)  {
        let avAsset = AVURLAsset(url: videoURL, options: nil)
        
        //Create Export session
        exportSession = AVAssetExportSession(asset: avAsset, presetName: AVAssetExportPresetPassthrough)
        
        let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] as URL
        let filePath = documentsDirectory.appendingPathComponent("rendered-Video.mp4")
        deleteFile(filePath)
        
        exportSession!.outputURL = filePath
        exportSession!.outputFileType = AVFileType.mp4
        exportSession!.shouldOptimizeForNetworkUse = true
        let start = CMTimeMakeWithSeconds(0.0, preferredTimescale: 0)
        let range = CMTimeRangeMake(start: start, duration: avAsset.duration)
        exportSession.timeRange = range
        
        exportSession!.exportAsynchronously(completionHandler: {() -> Void in
            DispatchQueue.main.async {
                Utility.stopActivityIndicator()
                
                switch self.exportSession!.status {
                case .failed:
                    self.view.makeToast(self.exportSession?.error?.localizedDescription ?? "")
                case .cancelled:
                    self.view.makeToast("Export canceled")
                case .completed:
                    if let url = self.exportSession.outputURL {
                        //Rendered Video URL
                    }
                default:
                    break
                }
            }
        })
    }

Delete File function:

func deleteFile(_ filePath: URL) {
        guard FileManager.default.fileExists(atPath: filePath.path) else {
            return
        }
        
        do {
            try FileManager.default.removeItem(atPath: filePath.path)
        } catch {
            fatalError("Unable to delete file: \(error) : \(#function).")
        }
    }

Don't forget to import AVFoundation

Hope that will help!

Mohit Kumar
  • 2,898
  • 3
  • 21
  • 34
  • Thanks for your help, I am already using a similar method (the exportVideo extension posted below by Harry). Unfortunately, although the video transcodes to mp4, the resulting file cannot be played on an Android device. It is very infuriating! – Iain Coleman Jan 23 '20 at 13:54
  • Above coded is tried and tested you can give it a try and check once. I am using the above code in https://apps.apple.com/us/app/we-rockstar/id1459404484?ls=1 – Mohit Kumar Jan 23 '20 at 13:55
  • Will give it a go now and get back to you - thanks again! – Iain Coleman Jan 23 '20 at 13:59
  • I have just done some more testing and it is working - thanks for your help! :) – Iain Coleman Jan 23 '20 at 15:16
  • plz provide deletePath function – famfamfam Sep 22 '22 at 03:09
  • 1
    @famfamfam function added. – Mohit Kumar Sep 22 '22 at 04:32
  • hi, i got this problem ***[Error: convert failed for Users/thehe/Library/Developer/CoreSimulator/Devices/A69D905C-4531-438C-9123-85D8EE81EC54/data/Containers/Data/Application/06CC0F2D-3D96-4734-8AAB-05816F14F6B7/tmp/com.vedax.vedaxlinkv2-Inbox/IMG_1668.mp4.MOV, error: The operation could not be completed]***, i just copy your code, can u help? – famfamfam Sep 22 '22 at 04:36
  • i debug on xcode then found this error ***figAssetExportSession_IsAssetPropertyAvailable signalled err=-12939 (loadingError) (AssetProperty Failed to load) at FigAssetExportSession.c:4782*** – famfamfam Sep 22 '22 at 04:51
1
//MARK:- Convert iPhoneVideo(.mov) to mp4
extension AVURLAsset
{
    func exportVideo(presetName: String = AVAssetExportPresetHighestQuality, outputFileType: AVFileType = .mp4, fileExtension: String = "mp4", then completion: @escaping (URL?) -> Void)
    {
        let filename = url.deletingPathExtension().appendingPathExtension(fileExtension).lastPathComponent
        let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent(filename)

    if let session = AVAssetExportSession(asset: self, presetName: presetName) {
        session.outputURL = outputURL
        session.outputFileType = outputFileType
        let start = CMTimeMakeWithSeconds(0.0, preferredTimescale: 0)
        let range = CMTimeRangeMake(start: start, duration: duration)
        session.timeRange = range
        session.shouldOptimizeForNetworkUse = true
        session.exportAsynchronously {
            switch session.status {
            case .completed:
                completion(outputURL)
            case .cancelled:
                debugPrint("Video export cancelled.")
                completion(nil)
            case .failed:
                let errorMessage = session.error?.localizedDescription ?? "n/a"
                debugPrint("Video export failed with error: \(errorMessage)")
                completion(nil)
            default:
                break
            }
        }
    } else {
        completion(nil)
    }
}
}


//MARK:- ImagePicker delegate methods
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any])
    {

if let url = info[UIImagePickerController.InfoKey.mediaURL] as? URL {

                let avAsset = AVURLAsset(url: url, options: nil)
            avAsset.exportVideo(presetName: AVAssetExportPresetHighestQuality, outputFileType: AVFileType.mp4, fileExtension: "mp4") { (mp4Url) in
                print("Mp4 converted url : \(String(describing: mp4Url))")
                self.videoPath = mp4Url//videoURL//

            }

}

}
  • Thank you for your help. I meant to add that this extension is the method I have been using to convert the video to mp4. The exportVideo method gets called when the UIImagePickerController has got a video file. The resulting encoded video looks like a valid mp4 - but for some reason it will not play on Android devices. I am stumped! – Iain Coleman Jan 23 '20 at 13:52