IOSvideo压缩Swift iOS 8损坏的video文件

我试图压缩来自UIImagePickerController(不是现有的video,而是一个dynamic的)用户相机拍摄的video上传到我的服务器,需要一点时间这样做,所以更小的尺寸是理想的,而不是30- 45 mb在较新的质量相机。

这里是在iOS 8中快速执行压缩的代码,它压缩得非常好,我轻松地从35毫秒降低到2.1毫秒。

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调用compress方法的代码

 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 })*/ } } 

正如我上面提到的这个作品尽可能减小文件的大小,但是当我得到的文件(它仍然是types.mov)quicktime不能播放它。 Quicktime确实尝试最初转换它,但一半(通过打开文件后1-2秒)失败。我甚至testing过AVPlayerController中的video文件,但它没有提供关于电影的任何信息,它只是一个播放button,没有ant加载和没有任何长度只是“ – ”时间通常在玩家。 IE一个不会播放的损坏的文件。

我确定它与编写资产的设置有关,不pipe是video编写还是audio编写,我都不确定。 甚至可能是导致资产腐败的资产阅读。 我试着改变周围的variables,并设置不同的阅读和书写键,但我还没有find正确的组合,这很糟糕,我可以压缩,但得到一个腐败的文件。 我不知道,任何帮助将不胜感激。 Pleeeeeeeeease。

这个答案已经完全重写和注释,以支持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) } } } 

弄清楚了! 好吧,所以有2个问题:1 videoWriter.finishWritingWithCompletionHandler函数调用的问题。 当这个完成块被执行时,并不意味着video编写器已经完成了对输出url的写入。 所以我不得不在我上传实际的video文件之前检查状态是否完成。 这是一种黑客,但这是我所做的

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

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

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

[编辑]:更好的解决scheme

好吧,经过大量的实验和几个月后,我发现了一个很好的,更简单的解决scheme,使video从45 MB下降到1.42 MB,质量相当好。

下面是调用函数来代替原来的convertVideo函数。 请注意,我必须编写自己的完成处理程序参数,在asynchronous导出完成后调用它。 我只是叫它处理程序。

  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() }) } }) 

您的转换方法是asynchronous的,但没有完成块。 那么你的代码怎么知道文件何时准备好? 也许你正在使用该文件之前,它已被完全写入。

转换本身也很奇怪 – audio和video通常是并行编写的,而不是串行的。

你奇迹般的压缩比可能表明你写出的帧数比实际想象的要less。