【问题标题】:IOS Video Compression Swift iOS 8 corrupt video fileIOS Video Compression Swift iOS 8 损坏的视频文件
【发布时间】:2015-04-08 17:49:06
【问题描述】:

我正在尝试压缩用户相机从 UIImagePickerController 拍摄的视频(不是现有视频,而是动态视频)上传到我的服务器并花费少量时间,因此较小的尺寸是理想的在较新质量的相机上为 30-45 mb。

这是在 iOS 8 中快速压缩的代码,它压缩得非常好,我很容易从 35 mb 降到 2.1 mb。

   func convertVideo(inputUrl: NSURL, outputURL: NSURL) 
   {
    //setup video writer
    var videoAsset = AVURLAsset(URL: inputUrl, options: nil) as AVAsset

    var videoTrack = videoAsset.tracksWithMediaType(AVMediaTypeVideo)[0] as AVAssetTrack

    var videoSize = videoTrack.naturalSize

    var videoWriterCompressionSettings = Dictionary(dictionaryLiteral:(AVVideoAverageBitRateKey,NSNumber(integer:960000)))

    var videoWriterSettings = Dictionary(dictionaryLiteral:(AVVideoCodecKey,AVVideoCodecH264),
        (AVVideoCompressionPropertiesKey,videoWriterCompressionSettings),
        (AVVideoWidthKey,videoSize.width),
        (AVVideoHeightKey,videoSize.height))

    var videoWriterInput = AVAssetWriterInput(mediaType: AVMediaTypeVideo, outputSettings: videoWriterSettings)

    videoWriterInput.expectsMediaDataInRealTime = true

    videoWriterInput.transform = videoTrack.preferredTransform


    var videoWriter = AVAssetWriter(URL: outputURL, fileType: AVFileTypeQuickTimeMovie, error: nil)

    videoWriter.addInput(videoWriterInput)

    var videoReaderSettings: [String:AnyObject] = [kCVPixelBufferPixelFormatTypeKey:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange]

    var videoReaderOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: videoReaderSettings)

    var videoReader = AVAssetReader(asset: videoAsset, error: nil)

    videoReader.addOutput(videoReaderOutput)



    //setup audio writer
    var audioWriterInput = AVAssetWriterInput(mediaType: AVMediaTypeAudio, outputSettings: nil)

    audioWriterInput.expectsMediaDataInRealTime = false

    videoWriter.addInput(audioWriterInput)


    //setup audio reader

    var audioTrack = videoAsset.tracksWithMediaType(AVMediaTypeAudio)[0] as AVAssetTrack

    var audioReaderOutput = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: nil) as AVAssetReaderOutput

    var audioReader = AVAssetReader(asset: videoAsset, error: nil)


    audioReader.addOutput(audioReaderOutput)

    videoWriter.startWriting()


    //start writing from video reader
    videoReader.startReading()

    videoWriter.startSessionAtSourceTime(kCMTimeZero)

    //dispatch_queue_t processingQueue = dispatch_queue_create("processingQueue", nil)

    var queue = dispatch_queue_create("processingQueue", nil)

    videoWriterInput.requestMediaDataWhenReadyOnQueue(queue, usingBlock: { () -> Void in
        println("Export starting")

        while videoWriterInput.readyForMoreMediaData
        {
            var sampleBuffer:CMSampleBufferRef!

            sampleBuffer = videoReaderOutput.copyNextSampleBuffer()

            if (videoReader.status == AVAssetReaderStatus.Reading && sampleBuffer != nil)
            {
                videoWriterInput.appendSampleBuffer(sampleBuffer)

            }

            else
            {
                videoWriterInput.markAsFinished()

                if videoReader.status == AVAssetReaderStatus.Completed
                {
                    if audioReader.status == AVAssetReaderStatus.Reading || audioReader.status == AVAssetReaderStatus.Completed
                    {

                    }
                    else {


                        audioReader.startReading()

                        videoWriter.startSessionAtSourceTime(kCMTimeZero)

                        var queue2 = dispatch_queue_create("processingQueue2", nil)


                        audioWriterInput.requestMediaDataWhenReadyOnQueue(queue2, usingBlock: { () -> Void in

                            while audioWriterInput.readyForMoreMediaData
                            {
                                var sampleBuffer:CMSampleBufferRef!

                                sampleBuffer = audioReaderOutput.copyNextSampleBuffer()

                                println(sampleBuffer == nil)

                                if (audioReader.status == AVAssetReaderStatus.Reading && sampleBuffer != nil)
                                {
                                    audioWriterInput.appendSampleBuffer(sampleBuffer)

                                }

                                else
                                {
                                    audioWriterInput.markAsFinished()

                                    if (audioReader.status == AVAssetReaderStatus.Completed)
                                    {

                                        videoWriter.finishWritingWithCompletionHandler({ () -> Void in

                                            println("Finished writing video asset.")

                                            self.videoUrl = outputURL

                                                var data = NSData(contentsOfURL: outputURL)!

                                                 println("Byte Size After Compression: \(data.length / 1048576) mb")

                                                println(videoAsset.playable)

                                                //Networking().uploadVideo(data, fileName: "Test2")

                                            self.dismissViewControllerAnimated(true, completion: nil)

                                        })
                                        break
                                    }
                                }
                            }
                        })
                        break
                    }
                }
            }// Second if

        }//first while

    })// first block
   // return
}

这是我的 UIImagePickerController 调用压缩方法的代码

func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [NSObject : AnyObject])
{
    // Extract the media type from selection

    let type = info[UIImagePickerControllerMediaType] as String

    if (type == kUTTypeMovie)
    {

        self.videoUrl = info[UIImagePickerControllerMediaURL] as? NSURL

        var uploadUrl = NSURL.fileURLWithPath(NSTemporaryDirectory().stringByAppendingPathComponent("captured").stringByAppendingString(".mov"))

        var data = NSData(contentsOfURL: self.videoUrl!)!

        println("Size Before Compression: \(data.length / 1048576) mb")


        self.convertVideo(self.videoUrl!, outputURL: uploadUrl!)

        // Get the video from the info and set it appropriately.

        /*self.dismissViewControllerAnimated(true, completion: { () -> Void in


        //self.next.enabled = true

        })*/
    }
}

正如我上面提到的,这可以减少文件大小,但是当我取回文件时(它仍然是 .mov 类型),quicktime 无法播放它。 Quicktime 最初确实尝试转换它,但中途失败(打开文件后 1-2 秒。)我什至在 AVPlayerController 中测试了视频文件,但它没有提供有关电影的任何信息,它只是一个没有的播放按钮蚂蚁加载,没有任何长度,只是“--”,时间通常在播放器中。 IE 一个无法播放的损坏文件。

我确定它与将资产写入的设置有关,无论是视频写入还是音频写入,我都不确定。甚至可能是对资产的读取导致其损坏。我尝试更改变量并设置不同的读取和写入键,但我没有找到正确的组合,这很糟糕,我可以压缩但从中得到一个损坏的文件。我完全不确定,任何帮助将不胜感激。 Pleeeeeeeeease.

【问题讨论】:

    标签: ios swift video compression corrupt


    【解决方案1】:

    这个答案已被完全重写和注释以支持 Swift 4.0。请记住,更改 AVFileTypepresetName 值可以让您在大小和质量方面调整最终输出。

    import AVFoundation
    
    extension ViewController: AVCaptureFileOutputRecordingDelegate {
        // Delegate function has been updated
        func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
            // This code just exists for getting the before size. You can remove it from production code
            do {
                let data = try Data(contentsOf: outputFileURL)
                print("File size before compression: \(Double(data.count / 1048576)) mb")
            } catch {
                print("Error: \(error)")
            }
            // This line creates a generic filename based on UUID, but you may want to use your own
            // The extension must match with the AVFileType enum
            let path = NSTemporaryDirectory() + UUID().uuidString + ".m4v"
            let outputURL = URL.init(fileURLWithPath: path)
            let urlAsset = AVURLAsset(url: outputURL)
            // You can change the presetName value to obtain different results
            if let exportSession = AVAssetExportSession(asset: urlAsset,
                                                        presetName: AVAssetExportPresetMediumQuality) {
                exportSession.outputURL = outputURL
                // Changing the AVFileType enum gives you different options with
                // varying size and quality. Just ensure that the file extension
                // aligns with your choice
                exportSession.outputFileType = AVFileType.mov
                exportSession.exportAsynchronously {
                    switch exportSession.status {
                    case .unknown: break
                    case .waiting: break
                    case .exporting: break
                    case .completed:
                        // This code only exists to provide the file size after compression. Should remove this from production code
                        do {
                            let data = try Data(contentsOf: outputFileURL)
                            print("File size after compression: \(Double(data.count / 1048576)) mb")
                        } catch {
                            print("Error: \(error)")
                        }
                    case .failed: break
                    case .cancelled: break
                    }
                }
            }
        }
    }
    

    以下是为 Swift 3.0 编写的原始答案:

    extension ViewController: AVCaptureFileOutputRecordingDelegate {
        func capture(_ captureOutput: AVCaptureFileOutput!, didFinishRecordingToOutputFileAt outputFileURL: URL!, fromConnections connections: [Any]!, error: Error!) {
            guard let data = NSData(contentsOf: outputFileURL as URL) else {
                return
            }
    
            print("File size before compression: \(Double(data.length / 1048576)) mb")
            let compressedURL = NSURL.fileURL(withPath: NSTemporaryDirectory() + NSUUID().uuidString + ".m4v")
            compressVideo(inputURL: outputFileURL as URL, outputURL: compressedURL) { (exportSession) in
                guard let session = exportSession else {
                    return
                }
    
                switch session.status {
                case .unknown:
                    break
                case .waiting:
                    break
                case .exporting:
                    break
                case .completed:
                    guard let compressedData = NSData(contentsOf: compressedURL) else {
                        return
                    }
    
                    print("File size after compression: \(Double(compressedData.length / 1048576)) mb")
                case .failed:
                    break
                case .cancelled:
                    break
                }
            }
        }
    
        func compressVideo(inputURL: URL, outputURL: URL, handler:@escaping (_ exportSession: AVAssetExportSession?)-> Void) {
            let urlAsset = AVURLAsset(url: inputURL, options: nil)
            guard let exportSession = AVAssetExportSession(asset: urlAsset, presetName: AVAssetExportPresetMediumQuality) else {
                handler(nil)
    
                return
            }
    
            exportSession.outputURL = outputURL
            exportSession.outputFileType = AVFileTypeQuickTimeMovie
            exportSession.shouldOptimizeForNetworkUse = true
            exportSession.exportAsynchronously { () -> Void in
                handler(exportSession)
            }
        }
    }
    

    【讨论】:

    • 感谢 CodeBender 的更新答案。 20MB 到 500k 相当惊人。我会确保对此表示赞同。
    • 我试过这个,但我总是得到会话状态为失败。
    • @Sneha 我已经更新了 Swift 4.0 的答案。也许这对您的问题有帮助?
    【解决方案2】:

    想通了! 好的,所以有 2 个问题: 1 个问题与 videoWriter.finishWritingWithCompletionHandler 函数调用有关。当这个完成块被执行时,并不意味着视频作者已经完成了对输出 url 的写入。所以我必须在上传实际视频文件之前检查状态是否已完成。这有点像黑客,但这就是我所做的

       videoWriter.finishWritingWithCompletionHandler({() -> Void in
    
              while true
              {
                if videoWriter.status == .Completed 
                {
                   var data = NSData(contentsOfURL: outputURL)!
    
                   println("Finished: Byte Size After Compression: \(data.length / 1048576) mb")
    
                   Networking().uploadVideo(data, fileName: "Video")
    
                   self.dismissViewControllerAnimated(true, completion: nil)
                   break
                  }
                }
            })
    

    我遇到的第二个问题是 Failed 状态,这是因为我一直在写入相同的临时目录,如我的问题中 UIImagePickerController didFinishSelectingMediaWithInfo 方法的代码所示。所以我只是使用当前日期作为目录名称,所以它是唯一的。

    var uploadUrl = NSURL.fileURLWithPath(NSTemporaryDirectory().stringByAppendingPathComponent("\(NSDate())").stringByAppendingString(".mov"))
    

    [编辑]:更好的解决方案

    好的,经过大量试验,几个月后,我找到了一个非常好的和更简单的解决方案,可以将视频从 45 mb 降低到 1.42 mb,质量非常好。

    下面是调用的函数,而不是原来的 convertVideo 函数。请注意,我必须编写自己的完成处理程序参数,该参数在异步导出完成后调用。我只是称它为处理程序。

     func compressVideo(inputURL: NSURL, outputURL: NSURL, handler:(session: AVAssetExportSession)-> Void)
    {
        var urlAsset = AVURLAsset(URL: inputURL, options: nil)
    
        var exportSession = AVAssetExportSession(asset: urlAsset, presetName: AVAssetExportPresetMediumQuality)
    
        exportSession.outputURL = outputURL
    
        exportSession.outputFileType = AVFileTypeQuickTimeMovie
    
        exportSession.shouldOptimizeForNetworkUse = true
    
        exportSession.exportAsynchronouslyWithCompletionHandler { () -> Void in
    
            handler(session: exportSession)
        }
    
    }
    

    这里是 uiimagepickercontrollerDidFinisPickingMediaWithInfo 函数中的代码。

    self.compressVideo(inputURL!, outputURL: uploadUrl!, handler: { (handler) -> Void in
    
                    if handler.status == AVAssetExportSessionStatus.Completed
                    {
                        var data = NSData(contentsOfURL: uploadUrl!)
    
                        println("File size after compression: \(Double(data!.length / 1048576)) mb")
    
                        self.picker.dismissViewControllerAnimated(true, completion: nil)
    
    
                    }
    
                    else if handler.status == AVAssetExportSessionStatus.Failed
                    {
                            let alert = UIAlertView(title: "Uh oh", message: " There was a problem compressing the video maybe you can try again later. Error: \(handler.error.localizedDescription)", delegate: nil, cancelButtonTitle: "Okay")
    
                            alert.show()
    
                        })
                    }
                 })
    

    【讨论】:

    • 嘿,我目前正在我的项目中实现此代码,并且它一直在“exportSession.outputURL = outputURL”处中断。我检查了 outputURL,它的值为 0x0000000000000。你有什么想法来解决这个问题吗?我是IOS编程的新手,所以如果这个问题非常基本,请允许我。
    • @Kahsn 确保您实际上将 UploadURL 传递给 compressVideo 函数。
    • // 从文件 url 中获取视频 var originalVideoURL = info[UIImagePickerControllerMediaURL] as! NSURL // 为保存视频的压缩版本创建一个临时 url var compressedVideoOutputUrl = NSURL.fileURLWithPath(NSTemporaryDirectory().stringByAppendingPathComponent("(NSDate())").stringByAppendingString(".mov"))!
    • 这就像一个魅力!非常感谢你:)你是我的救星哈哈。我还有一个问题。在将视频上传到服务器时,我发现我的应用程序变慢了 2x~3x。您认为这仅仅是因为我将文件上传到服务器还是来自您的代码?我不太确定。如果有改进代码的方法,我愿意听取您的意见。再次,非常感谢!
    • 您需要将视频上传到后台队列中的服务器,而不是主队列中。只需确保您使用 NSURLSession 上传文件 let task = session.dataTaskWithURL(url!, completionHandler: { (data, response, error) -> Void in //Do what ever error handling you may need etc.} ) task.resume()
    【解决方案3】:

    您的转换方法是异步的,但没有完成块。那么您的代码如何知道文件何时准备就绪?也许您在文件完全写入之前就在使用它。

    转换本身看起来也很奇怪——音频和视频通常是并行编写的,而不是串行编写的。

    您神奇的压缩比可能表明您写出的帧数比您实际想象的要少。

    【讨论】:

    • 这听起来不错。在这种情况下你会推荐什么?当然,我会尝试对函数 finishWritingWithCompletionHandler 进行更多研究,但我认为这是被调用的完成块。我也尝试过使用 AVExportSession,但我只用几行代码就得到了相同的结果。
    【解决方案4】:

    这里是兼容Swift 4.0的代码Compress video size before attach to an email in swift你还可以跟踪视频压缩的进度

    【讨论】:

      猜你喜欢
      • 2014-12-21
      • 1970-01-01
      • 1970-01-01
      • 2014-11-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多