Сжатие видео в iOS Swift Поврежденный видеофайл iOS 8

Я пытаюсь сжать видео, снятое камерой пользователя из UIImagePickerController (не существующее видео, а одно на лету), чтобы загрузить на мой сервер, и это займет немного времени, поэтому меньший размер идеален вместо 30- 45 мб на камерах более нового качества.

Вот код для быстрого сжатия для iOS 8, и он прекрасно сжимается, я легко перехожу с 35 МБ до 2,1 МБ.

   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
}

Вот код моего 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.


person Andrew Edwards    schedule 08.04.2015    source источник


Ответы (4)


Этот ответ был полностью переписан и аннотирован для поддержки Swift 4.0. Имейте в виду, что изменение значений AVFileType и presetName позволяет настроить окончательный результат с точки зрения размера и качества.

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)
        }
    }
}
person CodeBender    schedule 03.08.2016
comment
Спасибо за обновленный ответ CodeBender. От 20 МБ до 500 КБ - это просто потрясающе. Я обязательно проголосую за это. - person Andrew Edwards; 04.08.2016
comment
Я пробовал это, но всегда получаю статус сеанса как Неудачный. - person Sneha; 25.09.2017
comment
@Sneha Я обновил ответ для Swift 4.0. Возможно, это поможет с вашей проблемой? - person CodeBender; 20.10.2017

Догадаться! Итак, было 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
              }
            }
        })

Второй проблемой, с которой я столкнулся, был статус Failed, потому что я продолжал писать в тот же временный каталог, как показано в коде метода UIImagePickerController didFinishSelectingMediaWithInfo в моем вопросе. Поэтому я просто использовал текущую дату в качестве имени каталога, чтобы он был уникальным.

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

[РЕДАКТИРОВАТЬ]: ЛУЧШЕЕ РЕШЕНИЕ

Итак, после долгих экспериментов и месяцев спустя я нашел чертовски хорошее и гораздо более простое решение для уменьшения видео с 45 МБ до 1,42 МБ с довольно хорошим качеством.

Ниже приведена функция, которую нужно вызвать вместо исходной функции 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()

                    })
                }
             })
person Andrew Edwards    schedule 09.04.2015
comment
Эй, в настоящее время я внедряю этот код в свой проект, и он постоянно ломается по адресу exportSession.outputURL = outputURL. Я проверил outputURL, и его значение 0x0000000000000. У вас есть идея решить эту проблему? Я новичок в программировании под IOS, поэтому позвольте мне, если этот вопрос очень простой. - person Kahsn; 19.07.2015
comment
@Kahsn Убедитесь, что вы действительно передаете UploadURL функции compressVideo. - person Andrew Edwards; 19.07.2015
comment
// Получить видео по URL-адресу файла var originalVideoURL = info [UIImagePickerControllerMediaURL] as! NSURL // Создайте временный URL-адрес для сохранения сжатой версии нашего видео var compressedVideoOutputUrl = NSURL.fileURLWithPath (NSTemporaryDirectory (). StringByAppendingPathComponent ((NSDate ())). StringByAppendingString (.mov))! - person Andrew Edwards; 19.07.2015
comment
Это работает как шарм! Большое вам спасибо :) Ты мой спаситель жизни, ха-ха. У меня есть еще один вопрос. Пока видео загружаются на сервер, я обнаружил, что мое приложение замедляется в 2–3 раза. Как вы думаете, это связано с тем, что я загружаю файлы на сервер, или это может быть связано с вашим кодом? Я не был уверен. Если есть способ улучшить код, я готов прислушаться к вашему мнению. Еще раз большое вам спасибо! - person Kahsn; 19.07.2015
comment
Вам нужно загружать видео на сервер в фоновой очереди, а не в основной очереди. Просто убедитесь, что вы используете NSURLSession для загрузки файла let task = session.dataTaskWithURL (url! ) task.resume () - person Andrew Edwards; 19.07.2015
comment
просто загляните в NSURLSession, это очень просто, я знаю, что может быть интересно просто просмотреть документацию и заставить код работать. Я делаю это все время, но это помогает, когда вы действительно понимаете API. - person Andrew Edwards; 19.07.2015
comment
Я знаю концепцию обратного потока, но на самом деле я не знал о сеансе NSURL. Я обязательно прочитаю это. Спасибо за помощь. - person Kahsn; 19.07.2015
comment
@AndrewEdwards, ты настоящий MVP. это, наконец, решило мою проблему через 5 часов :) Большое спасибо. Я разместил здесь объективную версию C: stackoverflow.com/questions/36096928/compressing-a-video-in-ios/ - person joey; 19.03.2016
comment
Разве простая установка качества видеозаписи на среднее не уменьшит размер файла? Есть ли разница в качестве, если мы используем var videoRecorder = UIImagePickerController ()… videoRecorder.videoQuality = .medium… вместо сжатия видео после записи? - person Hemant Bavle; 03.02.2017

Ваш метод преобразования является асинхронным, но не имеет блока завершения. Итак, как ваш код может узнать, когда файл готов? Возможно, вы используете файл до того, как он будет полностью записан.

Сама конвертация тоже выглядит странно - аудио и видео обычно пишутся параллельно, а не последовательно.

Ваша чудесная степень сжатия может указывать на то, что вы записали меньше кадров, чем вы думаете.

person Rhythmic Fistman    schedule 08.04.2015
comment
Звучит как хороший аргумент. Что бы вы посоветовали в этом случае? конечно, я попытаюсь провести еще несколько исследований по функции finishWritingWithCompletionHandler, но я решил, что это вызывается блок завершения. Я также пробовал использовать AVExportSession, но получаю тот же результат с помощью всего нескольких строк кода. - person Andrew Edwards; 09.04.2015

Вот код, совместимый со Swift 4.0 Сжать размер видео перед прикреплением к электронному письму в быстром темпе Вы также можете отслеживать прогресс сжатия видео

person Diken Shah    schedule 31.07.2018