Search code examples
androidiosffmpegmp4mov

How do I encode iOS videos as .mp4 from UIImagePickerController so that Android devices can play them?


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!


Solution

  • 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!