上传到 PHP 服务器后,录制的 iPhone X 视频将无法播放

Posted

技术标签:

【中文标题】上传到 PHP 服务器后,录制的 iPhone X 视频将无法播放【英文标题】:Recorded iPhone X videos won't play after upload to PHP Server 【发布时间】:2017-12-20 16:41:28 【问题描述】:

根据建议解决方案修改了代码,但仍然无法正常工作。视频将上传为0字节。我有一个应用程序,用户应该能够从手机录制视频,选择“使用视频”时,它将视频上传到我们的php服务器。文件上传成功,根据显示的大小,它不是空的。但是,当我在应用程序中播放视频时,甚至直接通过浏览器播放视频时,它都不会播放。播放代码没有问题,因为我已经硬编码了指向其他网站上其他视频的链接,并且效果很好。代码如下,非常感谢任何帮助。

// 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转换

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

// 创建参数

   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)


// 向 PHP 发送请求以上传文件的函数 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()


【问题讨论】:

ios 11 添加en.wikipedia.org/wiki/High_Efficiency_Video_Coding 作为新的默认视频格式。 感谢 ceejayoz,我开始阅读这篇文章,但不清楚实际的文件扩展名是什么。你碰巧知道吗? 我不认为这是一个新的文件扩展名,只是一个新的编解码器。 .mov.mp4 可能。 您为什么不将您的视频重新编码为真正的 MP4?您收到的视频可能是 HEC。您可能想使用AVAssetExportSession 来执行此操作。 @techgirl08 能否请您上传更新后的代码,以便我测试可能有什么问题? 【参考方案1】:

好的,我确实编写了一些代码来实际编码和上传视频。 不幸的是,在URLSession 中做一个multipart/form-data 实际上很难,所以我使用Alamofire 来实际上传视频。

这是部分代码:

UIImagePickerControllerDelegate(这是你应该修改的,检查//TODO cmets)

// 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
                

            
        
    

照片库辅助方法

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

视频上传(使用Alamofire,从 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(编码方式)

// 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
    


现在,要使其正常工作,您显然必须录制视频、导入框架并指定您的视图控制器符合协议,所以:

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)
        
    

希望对你有用,欢迎提问。

【讨论】:

以上是关于上传到 PHP 服务器后,录制的 iPhone X 视频将无法播放的主要内容,如果未能解决你的问题,请参考以下文章

我将视频与其他视频合并后将其上传到服务器,但录制的视频顺时针旋转

将图像从 iphone 上传到服务器文件夹

iPhone:上传图片到 web aspx 文件

iOS - 使用 AVFoundation 录制声音后 iPhone 声音级别低

iPhone AVAudioRecorder,AudioQueueServices,检测到声音时如何自动录制音频

在 android 设备上捕获视频并在 iOS 设备上播放