web-dev-qa-db-de.com

IOS Videokomprimierung Swift iOS 8 beschädigt Videodatei

Ich versuche, Videos zu komprimieren, die mit der Benutzerkamera von UIImagePickerController aufgenommen wurden (kein vorhandenes Video, sondern eines im laufenden Betrieb), um sie auf meinen Server hochzuladen. 45 mb auf neueren Qualitätskameras.

Hier ist der Code, um eine Komprimierung in Swift für iOS 8 durchzuführen, und er lässt sich wunderbar komprimieren. Ich gehe einfach von 35 MB auf 2,1 MB herunter.

   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 meinen UIImagePickerController, der die Methode compress aufruft

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

        })*/
    }
}

Wie ich oben erwähnte, funktioniert dies bis zur Reduzierung der Dateigröße, aber wenn ich die Datei zurück bekomme (sie ist immer noch vom Typ .mov), kann Quicktime sie nicht abspielen. Quicktime versucht es zunächst zu konvertieren, schlägt jedoch nach der Hälfte der Zeit (1-2 Sekunden nach dem Öffnen der Datei) fehl. Ich habe die Videodatei sogar in AVPlayerController getestet, aber es gibt keine Informationen über den Film, sondern nur eine Wiedergabetaste ohne ameisenladen und ohne länge nur "-" wo die zeit normalerweise im player steht. IE eine beschädigte Datei, die nicht abgespielt werden kann.

Ich bin mir sicher, dass es etwas mit den Einstellungen zu tun hat, mit denen das Asset herausgeschrieben wird, ob es sich um das Video- oder das Audio-Schreiben handelt. Ich bin mir überhaupt nicht sicher. Es könnte sogar das Lesen des Assets sein, das dazu führt, dass es beschädigt wird. Ich habe versucht, die Variablen zu ändern und verschiedene Schlüssel zum Lesen und Schreiben zu setzen, aber ich habe nicht die richtige Kombination gefunden, und das ist scheiße, dass ich komprimieren kann, aber eine beschädigte Datei daraus bekomme. Ich bin mir überhaupt nicht sicher und jede Hilfe wäre dankbar. Pleeeeeeeeease.

17
Andrew Edwards

Diese Antwort wurde komplett umgeschrieben und mit Anmerkungen versehen, um Swift 4.0 zu unterstützen. Denken Sie daran, dass Sie durch Ändern der Werte für 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
                }
            }
        }
    }
}

Nachfolgend finden Sie die ursprüngliche Antwort für 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)
        }
    }
}
17
CodeBender

Herausgefunden! Ok Es gab also 2 Probleme: 1 Problem war mit dem Funktionsaufruf videoWriter.finishWritingWithCompletionHandler. Wenn dieser Beendigungsblock ausgeführt wird, bedeutet dies NICHT, dass der Video Writer das Schreiben in die Ausgabe-URL beendet hat. Ich musste also überprüfen, ob der Status abgeschlossen war, bevor ich die eigentliche Videodatei hochlud. Es ist eine Art Hack, aber das habe ich getan

   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, das ich hatte, war ein Fehlgeschlagener Status, und das lag daran, dass ich in das gleiche temporäre Verzeichnis geschrieben habe, wie es im Code für die UIImagePickerController-Methode didFinishSelectingMediaWithInfo in meiner Frage angegeben ist. Also habe ich nur das aktuelle Datum als Verzeichnisnamen verwendet, damit es eindeutig ist.

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

[EDIT]: BESSERE LÖSUNG

Ok, also nach vielen Experimenten und Monaten später habe ich eine verdammt gute und viel einfachere Lösung gefunden, um ein Video von 45 MB auf 1,42 MB mit ziemlich guter Qualität runter zu bringen.

Unten sehen Sie die aufzurufende Funktion anstelle der ursprünglichen Funktion convertVideo. Beachten Sie, dass ich meinen eigenen Completion-Handler-Parameter schreiben musste, der aufgerufen wird, nachdem der asynchrone Export abgeschlossen wurde. Ich habe es einfach 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 Funktion 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()

                    })
                }
             })
17
Andrew Edwards

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

Die Konvertierung selbst sieht ebenfalls seltsam aus - Audio und Video werden normalerweise parallel und nicht in Reihe geschrieben.

Ihre wundersame Komprimierungsrate weist möglicherweise darauf hin, dass Sie weniger Frames geschrieben haben, als Sie tatsächlich denken.

1

Hier ist der Code, der mit Swift 4.0 kompatibel ist Videogröße komprimieren, bevor eine E-Mail in Swift angehängt wird Sie können auch den Fortschritt der Videokomprimierung verfolgen

0
Diken Shah