Swift - 将视频转换为数据,字符串使用 UTF8 编码返回 nil - 通过 HTTP POST 将视频发送到 AWS S3 存储桶

问题描述

问题的高级解释(在 Swift 5 中)

  1. 我正在使用 AVAssetWriter

    在 MOV 中录制视频
  2. 我正在使用 exportSession.exportAsynchronously 对 MP4 中的视频进行编码(但是我可以跳过这一步,但我仍然遇到同样的问题)

  3. 我通过 HTTP POST 将视频发送到 AWS S3 存储桶:

    let fileData = try NSData(contentsOfFile:videoPathMP4.path,options:[]) as Data

    let fileContent = String(data: fileData,encoding: .utf8)

fileContent 现在是 nil 意味着视频数据不能用 UTF8 解释。如果我使用 UTF16 它可以工作(我得到一个字符串)但是当我在服务器端收到消息时它不是可读的 MP4 文件(它已损坏?)。我感觉是因为它应该是UTF8的字符串,但我无法将视频数据转换为UTF8字符串发送到服务器。

如何以 UTF8 格式发送此数据,或者如何仅以 NSData 格式发送视频数据?我是不是看错了?

以下是我在不同步骤中的代码片段:

第 1 步 - 在 MOV 中录制视频:

func captureOutput(_ output: AVCaptureOutput,didOutput sampleBuffer: CMSampleBuffer,from connection: AVCaptureConnection) {
    let timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer).seconds
            switch _captureState {
            case .start:
                print ("starting to record")
                // Set up recorder
                _filename = UUID().uuidString
                let videoPath = FileManager.default.urls(for: .documentDirectory,in: .userDomainMask).first!.appendingPathComponent("\(_filename).mov")
                let writer = try! AVAssetWriter(outputURL: videoPath,fileType: .mov)
                let settings = _videoOutput!.recommendedVideoSettingsForAssetWriter(writingTo: .mov)
                let input = AVAssetWriterInput(mediaType: .video,outputSettings: settings) 
                input.mediaTimeScale = CMTimeScale(bitPattern: 600)
                input.expectsMediaDataInRealTime = true
                input.transform = CGAffineTransform(rotationAngle: .pi/2)
                let adapter = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: input,sourcePixelBufferAttributes: nil)
                if writer.canAdd(input) {
                    writer.add(input)
                }
                writer.startWriting()
                writer.startSession(atSourceTime: .zero)
                _assetWriter = writer
                _assetWriterInput = input
                _adpater = adapter
                _captureState = .capturing
                _time = timestamp
            case .capturing:
                if _assetWriterInput?.isReadyForMoreMediaData == true {
                    let time = CMTime(seconds: timestamp - _time,preferredTimescale: CMTimeScale(600))
                    _adpater?.append(CMSampleBufferGetimageBuffer(sampleBuffer)!,withPresentationTime: time)
                }
                break
            case .end:
                guard _assetWriterInput?.isReadyForMoreMediaData == true,_assetWriter!.status != .Failed else { break }
                _assetWriterInput?.markAsFinished()
                _assetWriter?.finishWriting { [weak self] in
                    self?._captureState = .idle
                    self?._assetWriter = nil
                    self?._assetWriterInput = nil
                    print ("Finished writing video file: \(self!._filename)")
                }
            default:
                break
            }

}

第 2 步 - 在 MP4 中编码视频(同步以避免发送数据时出现竞争条件):

    func encodeVideo(at videoURL: URL,completionHandler: ((URL?,Error?) -> Void)?)  {
    let avAsset = AVURLAsset(url: videoURL,options: nil)

    let startDate = Date()

    //Create Export session
    guard let exportSession = AVAssetExportSession(asset: avAsset,presetName: AVAssetExportPresetPassthrough) else {
        completionHandler?(nil,nil)
        return
    }

    //Creating temp path to save the converted video
    let documentsDirectory = FileManager.default.urls(for: .documentDirectory,in: .userDomainMask)[0] as URL
    let filePath = documentsDirectory.appendingPathComponent("rendered-Video.mp4")

    //Check if the file already exists then remove the prevIoUs file
    if FileManager.default.fileExists(atPath: filePath.path) {
        do {
            try FileManager.default.removeItem(at: filePath)
        } catch {
            completionHandler?(nil,error)
        }
    }

    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

    let group = dispatchGroup()
    group.enter()
    
    exportSession.exportAsynchronously(completionHandler: {() -> Void in
        switch exportSession.status {
        case .Failed:
            print(exportSession.error ?? "NO ERROR")
            completionHandler?(nil,exportSession.error)
        case .cancelled:
            print("Export canceled")
            completionHandler?(nil,nil)
        case .completed:
            //Video conversion finished
            let endDate = Date()

            let time = endDate.timeIntervalSince(startDate)
            print(time)
            print("Successful!")
            print(exportSession.outputURL ?? "NO OUTPUT URL")
            completionHandler?(exportSession.outputURL,nil)

            default: break
        }
        group.leave()
    })
    group.wait(timeout: .Now() + 10.0) // blocks current queue
}

第 3 步 - 发送视频(获取 S3 的临时 URL 并授权发送实际视频文件,然后发送视频文件 - 所有调用都阻止调用):

    private func sendVideo(filename: String,recordID: String){
    
    //Obtain temporary URL and credentials
    let url = URL(string: "https://<OMITTED>")!

    //JSON Payload for server
    let data = [
                "recordid" : recordID,] as [String : Any]
    
    let group = dispatchGroup()
    group.enter()
    Helper.sendVideoURLRequestToRestAPI(url: url,data: data)
        { response,json,error in
                // will be called at either completion or at an error.
        
                guard let statusCode = response?.statusCode else{
                    print ("[sendVideo] no status code returned - request Failed")
                    return
                }
                print ("Status Code from server: \(statusCode)")
        
                if statusCode != 200 {
                    dispatchQueue.main.async {
                        let alert2 = UIAlertController(title: "Error",message: "Error submitting data - please try again! - \(json["body"] as? String)",preferredStyle: .alert)
                        alert2.addAction(UIAlertAction(title: "OK",style: .default,handler: nil))
                        self.present(alert2,animated: true)
                    }

                } else { //success
                    
                    let videoURL = json["url"] as! String

                    let parameters = [
                      [
                        "key": "key","value": (json["fields"] as! [String : Any])["key"]!,"type": "text"
                      ],[
                        "key": "AWSAccessKeyId","value": (json["fields"] as! [String : Any])["AWSAccessKeyId"]!,[
                        "key": "signature","value": (json["fields"] as! [String : Any])["signature"]!,[
                        "key": "policy","value": (json["fields"] as! [String : Any])["policy"]!,[
                        "key": "x-amz-security-token","value": (json["fields"] as! [String : Any])["x-amz-security-token"]!,[
                        "key": "file","src": filename,"type": "file","contentType": "video/mp4"
                      ]
                    ] as [[String : Any]]

                    let boundary = "Boundary-\(UUID().uuidString)"
                    var body = ""
                    var error: Error? = nil
                    for param in parameters {
                        if param["disabled"] == nil {
                            let paramName = param["key"]!
                            body += "--\(boundary)\r\n"
                            body += "Content-disposition:form-data; name=\"\(paramName)\""
                            if param["contentType"] != nil {
                              body += "\r\nContent-Type: \(param["contentType"] as! String)"
                            }
                            let paramType = param["type"] as! String
                            if paramType == "text" {
                              let paramValue = param["value"] as! String
                              body += "\r\n\r\n\(paramValue)\r\n"
                            } else {
                                let paramSrc = param["src"] as! String
                                print("paramSrc: \(paramSrc)")
                                let videoPath = FileManager.default.urls(for: .documentDirectory,in: .userDomainMask).first!.appendingPathComponent("\(paramSrc).mov")
                                
                                self.encodeVideo(at: videoPath,completionHandler: { url,error in
                                    guard error == nil else {
                                        print ("error video mp4 conversion")
                                        print(String(describing: error))
                                        return
                                    }
                                })
                                let videoPathMP4 = FileManager.default.urls(for: .documentDirectory,in: .userDomainMask).first!.appendingPathComponent("rendered-Video.mp4")

                                do {
                                    let fileData = try NSData(contentsOfFile:videoPathMP4.path,options:[]) as Data
                                    let fileContent = String(data: fileData,encoding: .utf8)! //Exception thrown here - IF I change this to UTF16 it doesn't throw an exception but the file arrives corrupted on server side.

                                    body += "; filename=\"\(paramName)\"\r\n"
                                    + "Content-Type: \"content-type header\"\r\n\r\n\(fileContent)\r\n" //paramName = paramSrc
                                
                                  //  print ("Body: \(body)")
                                } catch {
                                    print ("Could not open video file: \(videoPathMP4.path)")
                                    return
                                }
                            }
                        }
                    }
                    body += "--\(boundary)--\r\n";
                    let postData = body.data(using: .utf8) //UTF8

                    var request = URLRequest(url: URL(string: videoURL)!,timeoutInterval: Double.infinity)
                    request.addValue("multipart/form-data; boundary=\(boundary)",forHTTPHeaderField: "Content-Type")
                    request.httpMethod = "POST"
                    request.httpBody = postData


                    let session = URLSession.shared
                    let task = session.dataTask(with: request as URLRequest,completionHandler: { data,response,error in

                        guard error == nil else {
                            print ("error")
                            print(String(describing: error))
                            return
                        }

                        guard let data = data else {
                            print ("Error unpacking data")
                            print(String(describing: error))
                            //semaphore.signal()
                            return
                        }

                        print ("data \(data) - \(response)")
                        print(String(data: data,encoding: .utf8)!)
                    })
                    task.resume()

                    group.leave()
                }
        }
    group.wait() // blocks current queue
}

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)