2015-04-08 18 views
16

Sto provando a comprimere i video fatti con la fotocamera degli utenti da UIImagePickerController (non un video esistente ma uno al volo) per caricare sul mio server e impiegare un po 'di tempo per fare quindi, una dimensione più piccola è ideale invece di 30-45 mb su fotocamere di qualità più recente.IOS Video Compression Swift iOS 8 file video corrotto

Ecco il codice per eseguire una compressione in swift per iOS 8 e si comprime meravigliosamente, vado facilmente da 35 mb a 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 
} 

Ecco il codice per il mio UIImagePickerController che chiama il metodo di compressione

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 

     })*/ 
    } 
} 

Come ho già detto questo funziona per quanto riguarda la riduzione delle dimensioni del file, ma quando ottengo nuovamente il file (è ancora di tipo .mov) quicktime non può riprodurlo. Quicktime prova a convertirlo inizialmente ma fallisce a metà (1-2 secondi dopo l'apertura del file). Ho persino testato il file video in AVPlayerController ma non fornisce alcuna informazione sul film, è solo un pulsante di riproduzione senza caricamento delle formiche e senza alcuna lunghezza solo "-" dove solitamente il tempo è nel giocatore. IE un file corrotto che non verrà riprodotto.

Sono sicuro che ha qualcosa a che fare con le impostazioni per scrivere l'asset fuori se è la scrittura video o la scrittura audio non ne sono affatto sicuro. Potrebbe anche essere la lettura del bene che sta causando che sia corrotto. Ho provato a cambiare le variabili e impostare chiavi diverse per la lettura e la scrittura, ma non ho trovato la combinazione giusta e questo fa schifo che posso comprimere ma ottenere un file corrotto da esso. Non ne sono sicuro e qualsiasi aiuto sarebbe apprezzato. Pleeeeeeeeease.

risposta

16

Questa risposta è stato completamente riscritto e annotato per sostenere Swift 4.0. Tieni presente che la modifica dei valori AVFileType e presetName consente di modificare l'output finale in termini di dimensioni e qualità.

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

Di seguito è la risposta originale scritta per 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) 
     } 
    } 
} 
+0

Grazie per una risposta aggiornata CodeBender. 20MB a 500k è piuttosto sorprendente. Farò in modo di revocare questo. –

+0

Ho provato questo, ma ottengo sempre lo stato della sessione come Non riuscito. – Sneha

+0

@Sneha Ho aggiornato la risposta per Swift 4.0. Forse questo ti aiuterà con il tuo problema? – CodeBender

1

Il metodo di conversione è asincrono, ma non ha un blocco di completamento. Quindi, come può sapere il tuo codice quando il file è pronto? Forse stai usando il file prima che sia stato scritto completamente.

Anche la conversione sembra strana: l'audio e il video di solito sono scritti in parallelo, non in serie.

Il tuo rapporto di compressione miracoloso potrebbe indicare che hai scritto meno fotogrammi di quanto pensi.

+1

Che suona come un buon punto. Cosa consiglieresti in questo caso? ovviamente cercherò di fare qualche ricerca in più sulla funzione finishWritingWithCompletionHandler ma ho capito che era il blocco di completamento che veniva chiamato. Ho anche provato ad usare AVExportSession ma ottengo lo stesso risultato con solo poche righe di codice. –

17

Capito! Ok quindi c'erano 2 problemi: 1 problema era con la chiamata di funzione videoWriter.finishWritingWithCompletionHandler. quando questo blocco di completamento viene eseguito, NON SIGNIFICA che il video writer abbia finito di scrivere sull'URL di output. Quindi ho dovuto verificare se lo stato è stato completato prima di caricare il file video attuale. E 'una specie di hack, ma questo è quello che ho fatto

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

Il secondo problema che stavo avendo era uno stato fallito e che era perché ho continuato a scrivere nella stessa directory temporanea, come mostrato nel codice per il metodo UIImagePickerController didFinishSelectingMediaWithInfo nella mia domanda Quindi ho appena usato la data corrente come nome di una directory in modo da renderla unica.

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

[EDIT]: MIGLIORE SOLUZIONE

Ok, quindi dopo un sacco di sperimentazione e mesi più tardi ho trovato una soluzione dannatamente bene e molto più semplice per ottenere un video giù da 45 MB fino a 1,42 mb con qualità abbastanza buona.

Di seguito è la funzione da chiamare al posto della funzione convertVideo originale. si noti che ho dovuto scrivere il mio parametro di completamento del gestore che viene chiamato dopo che l'esportazione asincrona è finita. l'ho appena chiamato gestore.

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

} 

Ed ecco il codice nella funzione 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() 

        }) 
       } 
      }) 
+0

Ehi, attualmente sto implementando questo codice nel mio progetto e viene interrotto a "exportSession.outputURL = outputURL". Ho controllato l'outputURL e il suo valore è 0x0000000000000. Hai qualche idea per risolvere questo problema? Sono abbastanza un principiante nella programmazione IOS quindi per favore mi permetta se questa domanda è molto semplice. – Kahsn

+0

@Kahsn Assicurati di trasmettere un UploadURL alla funzione comprimereVideo. –

+0

// Ricevi il video dal file url var originalVideoURL = info [UIImagePickerControllerMediaURL] as! NSURL // Creare un URL temporaneo per il salvataggio di una versione compressa del nostro video var compressedVideoOutputUrl = NSURL.fileURLWithPath (NSTemporaryDirectory(). StringByAppendingPathComponent ("\ (NSDate())"). StringByAppendingString (". Mov"))! –