IOS Video Compression Swift iOS 8 archivo de video corrupto

Intento comprimir videos tomados con la cámara del usuario de UIImagePickerController (no es un video existente, sino uno sobre la marcha) para subir a mi servidor y tomar un poco de tiempo para hacerlo, por lo que un tamaño más pequeño es ideal en lugar de 30- 45 mb en cámaras de calidad más nuevas.

Aquí está el código para hacer una compresión rápida para iOS 8 y se comprime maravillosamente, voy de 35 mb a 2.1 mb fácilmente.

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 } 

Aquí está el código para mi UIImagePickerController que llama al método de compresión

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

Como mencioné anteriormente, esto funciona en cuanto a la reducción del tamaño del archivo, pero cuando recupero el archivo (todavía es de tipo .mov) quicktime no puede reproducirlo. Quicktime intenta convertirlo inicialmente pero falla a la mitad (1-2 segundos después de abrir el archivo). Incluso probé el archivo de video en AVPlayerController pero no proporciona información sobre la película, solo es un botón de reproducción sin carga de ants y sin ninguna longitud solo “-” donde el tiempo suele ser en el jugador. IE es un archivo corrupto que no se reproducirá.

Estoy seguro de que tiene algo que ver con la configuración para escribir el activo, ya sea que sea la grabación de video o la grabación de audio, no estoy seguro del todo. Incluso podría ser la lectura del activo que está causando que esté dañado. Intenté cambiar las variables y establecer diferentes claves para leer y escribir, pero no encontré la combinación correcta, y esto apesta porque puedo comprimir y obtener un archivo corrupto. No estoy seguro del todo y cualquier ayuda sería apreciada. Pleeeeeeeeease.

Esta respuesta ha sido completamente reescrita y anotada para admitir Swift 4.0 . Tenga en cuenta que cambiar los valores AVFileType y presetName permite ajustar el resultado final en términos de tamaño y calidad.

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

A continuación se encuentra la respuesta original tal como está escrita para 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) } } } 

¡Lo averigué! Ok, hubo 2 problemas: 1 problema con la llamada a la función videoWriter.finishWritingWithCompletionHandler. Cuando se ejecuta este bloque de finalización, NO SIGNIFICA que el escritor de video haya terminado de escribir en la url de salida. Así que tuve que verificar si el estado se completó antes de cargar el archivo de video real. Es una especie de truco, pero esto es lo que hice

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

El segundo problema que tenía era un estado Fallido y eso se debía a que seguí escribiendo en el mismo directorio temporal que se muestra en el código para el método UIImagePickerController didFinishSelectingMediaWithInfo en mi pregunta. Así que acabo de utilizar la fecha actual como nombre de directorio, por lo que sería único.

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

[EDITAR]: MEJOR SOLUCIÓN

De acuerdo, después de mucha experimentación y meses después, encontré una solución condenadamente buena y mucho más simple para bajar un video de 45 mb a 1.42 mb con una calidad bastante buena.

A continuación se muestra la función para llamar en lugar de la función convertVideo original. tenga en cuenta que tuve que escribir mi propio parámetro de controlador de finalización que se llama después de que finalizó la exportación asíncrona. Acabo de llamarlo controlador.

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

Y aquí está el código en la función 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() }) } }) 

Su método de conversión es asincrónico, pero no tiene un bloque de finalización. Entonces, ¿cómo puede saber su código cuando el archivo está listo? Tal vez estés usando el archivo antes de que esté completamente escrito.

La conversión en sí misma también parece extraña: el audio y el video generalmente se escriben en paralelo, no en serie.

Su relación de compresión milagrosa puede indicar que ha escrito menos cuadros de lo que realmente piensa.

Aquí está el código compatible con Swift 4.0 Compress video size antes de adjuntarlo a un correo electrónico en swift También puede seguir el progreso de la compresión de video