techgirl
techgirl

Reputation: 323

Recorded iPhone X videos won't play after upload to PHP Server

Code Revised based on suggestion solution but still not working. Videos are uploading as 0 bytes. I have an app where a user should be able to record a video from their phone and when 'Use Video' is selected it uploads the video to our PHP Server. The file is uploading successfully and based on the size shown it is not empty. However when I go to play the video in the app or even directly through the browser it won't play. There's no issue with the playback code as I've hardcoded links to other videos on other sites and it works fine. Code is below, any help is much appreciated.

// Finished recording a video
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
    print("Got a video")

    if let pickedVideo:URL = (info[UIImagePickerControllerMediaURL] as? URL) {
        // Save video to the main photo album
        let selectorToCall = #selector(CameraVideoViewController.videoWasSavedSuccessfully(_:didFinishSavingWithError:context:))
        UISaveVideoAtPathToSavedPhotosAlbum(pickedVideo.relativePath, self, selectorToCall, nil)
        imageSelected = true
        uuid = UUID().uuidString

        if imageSelected == true {
            saveFileName = "video-\(uuid).mp4"
        }
        // Save the video to the app directory so we can play it later
        let videoData = try? Data(contentsOf: pickedVideo)
        let paths = NSSearchPathForDirectoriesInDomains(
            FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)
        let documentsDirectory: URL = URL(fileURLWithPath: paths[0])
        let dataPath = documentsDirectory.appendingPathComponent(saveFileName)
        try! videoData?.write(to: dataPath, options: [])
        print("Saved to " + dataPath.absoluteString)

        imagePicker.dismiss(animated: true, completion: {
            // Anything you want to happen when the user saves an video
            self.encodeVideo(dataPath: dataPath)
            self.uploadVideo(videoData!)



        })
    } }

// MP4 Conversion of video

  func encodeVideo(dataPath: URL){
    let avAsset = AVURLAsset(url: dataPath)
    let startDate = Date()
    let exportSession = AVAssetExportSession(asset: avAsset, presetName: AVAssetExportPresetPassthrough)

    let docDir = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
    let myDocPath = NSURL(fileURLWithPath: docDir).appendingPathComponent("temp.mp4")?.absoluteString

    let docDir2 = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] as NSURL

    let filePath = docDir2.appendingPathComponent("rendered-Video.mp4")
    //uploadVideo(filePath)
    //self.encodeVideo(dataPath: dataPath)
    deleteFile(filePath!)


    if FileManager.default.fileExists(atPath: myDocPath!){
        do{
            try FileManager.default.removeItem(atPath: myDocPath!)

        }catch let error{
            print(error)
        }
    }
    //self.uploadVideo((myDocPath as AnyObject) as! URL)

    exportSession?.outputURL = filePath
    exportSession?.outputFileType = AVFileType.mp4
    exportSession?.shouldOptimizeForNetworkUse = true

    let start = CMTimeMakeWithSeconds(0.0, 0)
    let range = CMTimeRange(start: start, duration: avAsset.duration)
    exportSession?.timeRange = range

    exportSession!.exportAsynchronously{() -> Void in
        switch exportSession!.status{
        case .failed:
            print("\(exportSession!.error!)")
        case .cancelled:
            print("Export cancelled")
        case .completed:
            let endDate = Date()
            let time = endDate.timeIntervalSince(startDate)
            print(time)
            print("Successful")
            print(exportSession?.outputURL ?? "")
        default:
            break
        }

    }
}
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).")
    }
}

// Create Params

   func createBodyWithParams(_ parameters: [String: String]?, filePathKey: String?, videoData: Data, boundary: String) -> Data {

    var body = ""

    if let params = parameters {
        for (key, value) in params {
            body += "--\(boundary)\r\n"
            body += "Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n"
            body += "\(value)\r\n"
        }
    }
    var filename = ""

    if imageSelected {
        filename = "video-\(uuid).mp4"
    }

    let mimetype = "video/mp4"
    body += "--\(boundary)\r\n"
    body += "Content-Disposition: form-data; name=\"\(filePathKey!)\"; filename=\"\(filename)\"\r\n"
    body += "Content-Type: \(mimetype)\r\n\r\n"
    body += String(data: videoData, encoding: .utf8)!
    body += "\r\n"

    body += "--\(boundary)--\r\n"

    return Data(body.utf8)

}

// function sending request to PHP to upload a file func uploadVideo(_ videoData: Data) {

    let id = user!["id"] as! String
    uuid = UUID().uuidString


    let url = URL(string: "http://www.foo.com/videoposts.php")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"

    let param = [
        "id" : id,
        "uuid" : uuid
    ]

    // body
    let boundary = "Boundary-\(UUID().uuidString)"
    request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")

    // if picture is selected, compress it by half
    let imageData = Data()


    // ... body
    request.httpBody = createBodyWithParams(param, filePathKey: "file", videoData: imageData, boundary: boundary)

    // launch session
    URLSession.shared.dataTask(with: request) { data, response, error in

        // get main queu to communicate back to user
        DispatchQueue.main.async(execute: {


            if error == nil {

                do {

                    // json containes $returnArray from php
                    let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? NSDictionary

                    // declare new var to store json inf
                    guard let parseJSON = json else {
                        print("Error while parsing")
                        return
                    }

                    // get message from $returnArray["message"]
                    let message = parseJSON["message"]

                    // if there is some message - post is made
                    if message != nil {

                        // reset UI

                        self.postBtn.alpha = 0.4
                        self.imageSelected = false

                        // switch to another scene
                        self.tabBarController?.selectedIndex = 4

                    }

                } catch {

                    // get main queue to communicate back to user
                    DispatchQueue.main.async(execute: {
                        let message = "\(error)"
                        appDelegate.infoView(message: message, color: colorSmoothRed)
                    })
                    return

                }

            } else {

                // get main queue to communicate back to user
                DispatchQueue.main.async(execute: {
                    let message = error!.localizedDescription
                    appDelegate.infoView(message: message, color: colorSmoothRed)
                })
                return

            }


        })

        }.resume()

}

Upvotes: 0

Views: 3812

Answers (1)

Alejandro Iván
Alejandro Iván

Reputation: 4051

Okay, I did code something to actually encode and upload a video. Unfortunately, doing a multipart/form-data in URLSession is actually pretty hard, so I used Alamofire to actually upload the video.

This is the code in parts:

UIImagePickerControllerDelegate (this is the one you should modify, check the //TODO comments)

// MARK: UIImagePickerControllerDelegate
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
    print("Cancelled video operation.")
    dismiss(animated: true, completion: nil)
}

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
    guard let mediaType = info[UIImagePickerControllerMediaType] as? String else {
        print("Error with media type. Cancelling.")
        dismiss(animated: true, completion: nil)
        return;
    }

    print("Media type: \(mediaType)")

    if ( mediaType == "public.movie" ) { // Video selected
        let videoURL: URL
        if info[UIImagePickerControllerMediaURL] != nil {
            videoURL = info[UIImagePickerControllerMediaURL] as! URL
        }
        else {
            videoURL = info[UIImagePickerControllerReferenceURL] as! URL
        }


        if ( picker.sourceType == .camera ) {
            // The original video came from the camera, so it's considered new
            // Save it to the photo library
            saveVideo(url: videoURL, albumName: "MyApp")
        }

        // Dismiss the media picker and then re-encode the video
        dismiss(animated: true) {
            self.exportVideoToMP4(url: videoURL) { (exportedVideoURL) in

                guard let tempURL = exportedVideoURL else {
                    print("ERROR: Unknown error. The exported video URL is nil.")
                    return
                }

                print("Temporary video successfully exported to: \(tempURL.absoluteString)")





                // TODO: Add your own POST parameters
                let uuid = UUID().uuidString
                let params = [
                    "uuid" : uuid,
                    "user" : "myUserNameOrWhatever"
                ]


                // TODO: Change the parameters for uploading
                self.upload(    to: "http://yourweb.com/uploadVideo.php", // The URL to send the upload to
                                videoURL: tempURL, // The file URL of the temporary video file
                                parameters: params, // The POST parameters you want to send along with the upload
                                fileName: "vid-\(uuid).mp4", // The filename you want the server to receive.
                                fieldName: "video_file" // This is "name" from <input type="file" name="video_file" ... /> in HTML
                ) { (response) in

                    guard let resp = response else {
                        print("ERROR: Empty or unrecognizable response from server.")
                        return
                    }

                    print("Video uploaded. RESPONSE: \(resp)")

                    //: TODO Parse the server response after uploading
                }

            }
        }
    }
}

Photo Library helper methods

// MARK: Photo Library

func saveVideo(url: URL, albumName: String) {

    // Check authorization status before trying to save the video
    switch PHPhotoLibrary.authorizationStatus() {

    case .notDetermined:
        PHPhotoLibrary.requestAuthorization() { (status) in
            switch status {
            case .authorized:
                self.saveVideo(url: url, albumName: albumName) // Re-try to save the video after authorization
                return

            default:
                return
            }
        }



    case .authorized:
        // Save the video to the Photo Library here

        if let assetCollection = assetCollection(albumName: albumName) {

            // Asset collection exists, insert directly
            insertVideo(url: url, assetCollection: assetCollection)

        }
        else {

            // Asset collection doesn't exist, create it and then insert
            PHPhotoLibrary.shared().performChanges({

                PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: albumName)

            }, completionHandler: { (success, error) in
                guard success else {
                    print("ERROR: \(error!.localizedDescription)")
                    return
                }

                let createdAssetCollection = self.assetCollection(albumName: albumName)!
                self.insertVideo(url: url, assetCollection: createdAssetCollection)
            })

        }

        return



    default:
        // Not authorized
        print("Not authorized to save a video to the Photo Library.")
        return
    }


}


func assetCollection(albumName: String) -> PHAssetCollection? {
    let fetchOptions = PHFetchOptions()
    fetchOptions.predicate = NSPredicate(format:"title == '\(albumName)'")

    let fetchResult = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .albumRegular, options: fetchOptions)

    return fetchResult.firstObject
}


func insertVideo(url: URL?, assetCollection: PHAssetCollection) {
    guard let videoURL = url else {
        print("ERROR: The URL to insert into the Photo Library is empty.")
        return
    }

    PHPhotoLibrary.shared().performChanges({

        let createAssetRequest = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: videoURL)
        let assetPlaceholder = createAssetRequest?.placeholderForCreatedAsset
        let changeRequest = PHAssetCollectionChangeRequest(for: assetCollection)

        let enumeration: NSArray = [assetPlaceholder!]

        changeRequest?.addAssets(enumeration)

    }, completionHandler: { (success, error) in
        guard success else {
            print("ERROR: \(error!.localizedDescription)")
            return
        }

        print("Video saved successfully to the Photo Library album.")
    })
}

Video Uploading (using Alamofire, installed from CocoaPods)

// MARK: Video upload
func upload(to uploadAddress: String, videoURL: URL, parameters: [String:Any]?, fileName: String, fieldName: String, _ completion: ((String?) -> Void)?) {

    Alamofire.upload(multipartFormData: { (multipartFormData) in

        // Add the video file (if data is correct)
        if let videoData = FileManager.default.contents(atPath: videoURL.path) {
            multipartFormData.append(videoData, withName: fileName)
        }

        // Add the post params (if available)
        if let params = parameters {
            for (key, value) in params {
                multipartFormData.append( (value as! String).data(using: .utf8)! , withName: key)
            }
        }

    }, to: uploadAddress)
    { (result) in

        switch result {
        case .success(let upload, _, _):
            upload.responseString { (response) in
                if let completionHandler = completion {
                    completionHandler(response.result.value)
                }
            }

        case .failure(let encodingError):
            print("ERROR: \(encodingError.localizedDescription)")
            if let completionHandler = completion {
                completionHandler(nil)
            }
        }

    }

}

AVFoundation (the encoding method)

// MARK: AVFoundation
func exportVideoToMP4(url: URL, _ completion: @escaping ((URL?) -> Void)) {

    // Show some sort of indicator here, as this could take a while


    // Generate a temporary URL path to export the video
    let relativePath = "myAppTempVideoExport.mp4";
    let outputFilePath = NSTemporaryDirectory() + relativePath;

    print("Temp file path: \(outputFilePath)")


    // If there's any temp file from before at that path, delete it
    if FileManager.default.fileExists(atPath: outputFilePath) {
        do {
            try FileManager.default.removeItem(atPath: outputFilePath)
        }
        catch {
            print("ERROR: Can't remove temporary file from before. Cancelling export.")
            completion(nil)
            return
        }
    }


    // Export session setup
    let outputFileURL = URL(fileURLWithPath: outputFilePath)

    let asset = AVAsset(url: url) // Original (source) video

    // The original video codec is probably HEVC, so we'll force the system to re-encode it at the highest quality in MP4
    // You probably want to use medium quality if this video is intended to be uploaded (as this example is doing)
    if let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) {

        exportSession.outputURL = outputFileURL
        exportSession.outputFileType = .mp4

        exportSession.exportAsynchronously {

            // Hide the indicator for the export session

            switch exportSession.status {
            case .completed:
                print("Video export completed.")
                completion(outputFileURL)
                return

            case .failed:
                print("ERROR: Video export failed. \(exportSession.error!.localizedDescription)")
                completion(nil)
                return

            case .cancelled:
                print("Video export cancelled.")
                completion(nil)
                return

            default:
                break
            }

        }

    }
    else {
        print("ERROR: Cannot create an AVAssetExportSession.")
        return
    }

}

Now, for this to work, you obviously have to record a video, import frameworks and specify that your view controller conforms to protocols, so:

import AVFoundation
import Photos
import Alamofire



class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    let imagePicker = UIImagePickerController()


    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        imagePicker.sourceType = .camera
        imagePicker.delegate = self
        imagePicker.showsCameraControls = true
        imagePicker.allowsEditing = true
        imagePicker.mediaTypes = ["public.movie"]

        DispatchQueue.main.async {
            self.present(self.imagePicker, animated: true, completion: nil)
        }
    }
}

I hope this is useful to you, feel free to ask.

Upvotes: 1

Related Questions