2015-04-08 22 views
16

Ich versuche, Video aufgenommen mit der Kamera des Benutzers von UIImagePickerController (Nicht ein vorhandenes Video, sondern ein on the fly) auf meinen Server hochladen und nehmen Sie eine kleine Menge Zeit zu tun Also, so eine kleinere Größe ist ideal anstelle von 30-45 mb auf neueren Qualitätskameras.IOS Videokompression Swift iOS 8 korrupte Videodatei

Hier ist der Code, um eine Komprimierung in Swift für iOS 8 zu tun, und es komprimiert wunderbar, ich gehe von 35 mb bis 2,1 mb leicht.

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 
} 

Hier ist der Code für meine UIImagePickerController die

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 

     })*/ 
    } 
} 

die Kompresse Methode ruft Wie ich bereits erwähnt dies, soweit Reduzierung der Dateigröße funktioniert, aber wenn ich die Datei zurück (es ist nach wie vor vom Typ .mov) quicktime kann es nicht abspielen. Quicktime versucht zunächst, es zu konvertieren, schlägt aber in der Mitte (1-2 Sekunden nach dem Öffnen der Datei) fehl. Ich habe sogar die Videodatei in AVPlayerController getestet, aber es gibt keine Informationen über den Film, es ist nur ein Play-Taste ohne ant loading und ohne Länge nur "-" wo die Zeit normalerweise im Player ist. IE eine beschädigte Datei, die nicht abgespielt wird.

Ich bin mir sicher, dass es etwas mit den Einstellungen zum Schreiben des Assets zu tun hat, ob es das Video schreiben oder das Audio schreiben ist, bin ich mir überhaupt nicht sicher. Es könnte sogar das Lesen des Vermögenswerts sein, das dafür sorgt, dass es korrupt wird. Ich habe versucht, die Variablen zu ändern und unterschiedliche Schlüssel zum Lesen und Schreiben zu setzen, aber ich habe nicht die richtige Kombination gefunden und das ist zum Kotzen, dass ich komprimieren kann, aber eine beschädigte Datei daraus bekomme. Ich bin mir überhaupt nicht sicher und jede Hilfe würde geschätzt werden. Pleeeeeeeeeease.

Antwort

16

Diese Antwort wurde bisher Swift 4.0 komplett neu geschrieben und mit Anmerkungen versehen zu unterstützen. Beachten Sie, dass Sie durch Ändern der Werte AVFileType und presetName die endgültige Ausgabe in Bezug auf Größe und Qualität optimieren können.

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

Unten ist der Original Antwort wie für Swift geschrieben 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

Danke für eine aktualisierte Antwort CodeBender. 20MB bis 500k ist ziemlich erstaunlich. Ich werde dafür sorgen, dass dies verbessert wird. –

+0

Ich habe das versucht, aber ich bekomme immer den Sitzungsstatus als Fehlgeschlagen. – Sneha

+0

@Sneha Ich habe die Antwort für Swift 4.0 aktualisiert. Vielleicht hilft das bei Ihrem Problem? – CodeBender

1

Ihre Konvertierungsmethode ist asynchron, hat jedoch keinen Abschlussblock. Wie kann Ihr Code wissen, wann die Datei fertig ist? Vielleicht verwenden Sie die Datei, bevor sie vollständig geschrieben wurde.

Die Umwandlung selbst sieht auch seltsam aus - Audio und Video werden normalerweise parallel, nicht in Serie geschrieben.

Ihre wundersame Komprimierungsrate könnte darauf hinweisen, dass Sie weniger Frames ausgegeben haben, als Sie tatsächlich denken.

+1

, das wie ein guter Punkt klingt. Was würdest du in diesem Fall empfehlen? Natürlich werde ich versuchen, mehr über die Funktion finishWritingWithCompletionHandler zu recherchieren, aber ich dachte mir, dass der Completion Block aufgerufen wurde. Ich habe auch versucht, AVExportSession zu verwenden, aber ich bekomme das gleiche Ergebnis mit nur ein paar Zeilen Code. –

17

Ich habe es herausgefunden! Ok, also gab es 2 Probleme: 1 Problem war mit dem Funktionsaufruf videoWriter.finishWritingWithCompletionHandler. Wenn dieser Completion-Block ausgeführt wird, bedeutet dies NICHT, dass der Videorekorder mit dem Schreiben in die Ausgabe-URL fertig ist. Also musste ich überprüfen, ob der Status abgeschlossen war, bevor ich die eigentliche Videodatei hochgeladen habe. Es ist eine Art eines Hack, aber das ist, was ich

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

Das zweite Problem habe ich wurde mit einem gescheiterten Status und das war, weil ich auf das gleiche temporäre Verzeichnis in dem Code für die UIImagePickerController didFinishSelectingMediaWithInfo Methode, wie gezeigt gehalten schreiben in meiner Frage. Also habe ich gerade das aktuelle Datum als Verzeichnisnamen verwendet, so dass es eindeutig wäre.

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

[EDIT]: bessere Lösung

Ok so nach viel experimentieren und Monate später habe ich eine verdammt gute und viel einfachere Lösung für die Zeugung eines Video unten von 45 mb bis zu 1,42 mb gefunden habe mit ziemlich guter Qualität.

Unten ist die Funktion, um anstelle der ursprünglichen ConvertVideo-Funktion aufzurufen. Beachten Sie, dass ich meinen eigenen Completion-Handler-Parameter schreiben musste, der aufgerufen wird, nachdem der asynchrone Export beendet wurde. Ich habe es nur Handler genannt.

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

} 

Und hier ist der Code in der uiimagepickercontrollerDidFinisPickingMediaWithInfo Funktion.

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

Hey, ich implementiere gerade diesen Code in meinem Projekt und es bricht ständig bei "exportSession.outputURL = outputURL". Ich habe das outputURL überprüft und sein Wert ist 0x0000000000000. Haben Sie eine Idee, um dieses Problem zu beheben? Ich bin ein ziemlich Neuling in IOS-Programmierung, also bitte erlauben Sie mir, wenn diese Frage sehr einfach ist. – Kahsn

+0

@Kahsn Stellen Sie sicher, dass Sie eine UploadURL an die compressVideo-Funktion übergeben. –

+0

// Holen Sie sich das Video aus der Datei url var originalVideoURL = info [UIImagePickerControllerMediaURL] as! NSURL // Erstellen Sie eine temporäre URL für eine komprimierte Version unserer Video var compressedVideoOutputUrl Speichern = NSURL.fileURLWithPath (NSTemporaryDirectory(). StringByAppendingPathComponent ("\ (NSDate())"). StringByAppendingString ("mov"))! –