web-dev-qa-db-ja.com

IOSビデオ圧縮Swift iOS8の破損したビデオファイル

UIImagePickerControllerからユーザーのカメラで撮影したビデオ(既存のビデオではなくオンザフライ)を圧縮してサーバーにアップロードしようとしていますが、アップロードに時間がかかるため、30ではなく小さいサイズが理想的です-新しい品質のカメラでは45MB。

これがiOS8のSwiftで圧縮を行うためのコードであり、それは素晴らしく圧縮されます。私は35mbから2.1mbに簡単に移動します。

   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
}

これは、compressメソッドを呼び出すUIImagePickerControllerのコードです。

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

        })*/
    }
}

上で述べたように、これはファイルサイズの縮小に関しては機能しますが、ファイルを取り戻すと(まだタイプ.movです)、quicktimeで再生できません。 Quicktimeは最初は変換しようとしますが、途中で失敗します(ファイルを開いてから1〜2秒後)。AVPlayerControllerでビデオファイルをテストしたこともありますが、ムービーに関する情報は表示されません。再生ボタンがないだけです。アリのロードと長さなしで、時間は通常プレーヤーにある「-」だけです。 IE再生されない破損したファイル。

それがビデオ書き込みであるかオーディオ書き込みであるかにかかわらず、アセットを書き出すための設定と関係があると確信しています。私にはまったくわかりません。資産の読み取りが資産の破損を引き起こしている可能性もあります。変数を変更し、読み取りと書き込みに異なるキーを設定しようとしましたが、正しい組み合わせが見つかりませんでした。これは、圧縮できても破損したファイルを取得できるという点で問題があります。よくわかりませんので、よろしくお願いします。 Pleeeeeeeeease。

17
Andrew Edwards

この回答は完全に書き直され、サポートするように注釈が付けられていますSwift 4.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)
        }
    }
}
17
CodeBender

理解した! Ok 2つの問題がありました:1つの問題はvideoWriter.finishWritingWithCompletionHandler関数呼び出しにありました。この完了ブロックが実行されたとき、ビデオライターが出力URLへの書き込みを終了したことを意味しません。実際のビデオファイルをアップロードする前に、ステータスが完了したかどうかを確認する必要がありました。これは一種のハックですが、これは私が行ったことです。

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

私が抱えていた2番目の問題は、失敗ステータスでした。これは、質問のUIImagePickerControllerdidFinishSelectingMediaWithInfoメソッドのコードに示されているのと同じ一時ディレクトリに書き込みを続けたためです。そのため、現在の日付をディレクトリ名として使用したので、一意になります。

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

[編集]:より良いソリューション

さて、たくさんの実験と数ヶ月後、私はビデオを45mbから1.42mbにかなり良い品質で下げるための非常に優れたはるかに簡単な解決策を見つけました。

以下は、元のconvertVideo関数の代わりに呼び出す関数です。非同期エクスポートが終了した後に呼び出される独自の完了ハンドラーパラメータを作成する必要があることに注意してください。私はそれをハンドラーと呼んだ。

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

                    })
                }
             })
17
Andrew Edwards

変換方法は非同期ですが、完了ブロックがありません。では、ファイルの準備ができたことをコードはどのようにして知ることができますか?完全に書き込まれる前にファイルを使用している可能性があります。

変換自体も奇妙に見えます-オーディオとビデオは通常、直列ではなく並列に書き込まれます。

奇跡的な圧縮率は、実際に考えているよりも少ないフレームを書き込んだことを示している可能性があります。

1

Swift 4.0 Swiftでメールに添付する前にビデオサイズを圧縮する ビデオ圧縮の進行状況を追跡することもできます

0
Diken Shah