如何减小使用UIImagePickerController创建的视频文件大小?

59

我有一个应用程序,允许用户使用 UIImagePickerController 录制视频,然后上传到YouTube。问题是,即使视频只有5秒钟长,UIImagePickerController 创建的视频文件也非常大,例如,5秒钟长的视频大小为16-20兆字节。我想保留540或720质量的视频,但我想减小文件大小。

我一直在尝试使用AVFoundation和AVAssetExportSession来尝试获取更小的文件大小。我已经尝试了以下代码:

AVAsset *video = [AVAsset assetWithURL:videoURL];
AVAssetExportSession *exportSession = [AVAssetExportSession exportSessionWithAsset:video presetName:AVAssetExportPresetPassthrough];
exportSession.shouldOptimizeForNetworkUse = YES;
exportSession.outputFileType = AVFileTypeMPEG4;
exportSession.outputURL = [pathToSavedVideosDirectory URLByAppendingPathComponent:@"vid1.mp4"];
[exportSession exportAsynchronouslyWithCompletionHandler:^{
    NSLog(@"done processing video!");
}];

但是这对文件大小没有任何缩减作用。我知道我在做的事情是可能的,因为在苹果的照片应用中,当你选择"在YouTube上分享"时,将会自动处理视频文件,这样就可以上传了。我想在我的应用程序中做同样的事情。

我该怎么做才能实现这个目标?


照片上传会保持质量和分辨率不变吗?我怀疑它会减少两者以使视频更小。 - davidfrancis
不会,它会保留上传时的视频。YouTube 可以支持1080p视频。 - zakdances
将文件输出类型设置为AVFileTypeQuickTimeMovie是否可以减小文件大小?或者尝试使用yourPickerController.videoQuality属性来降低视频质量,从而减小文件大小? - Just a coder
在我的帖子中,我提到我想将质量保持在720或540。我会尝试将其转换为MOV格式,但据我所知,它比MP4格式要大得多。 - zakdances
标题有误导性,因为您没有在任何地方使用UIImagePickerController,您应该更改它以避免给未来的用户带来困惑。 - thibaut noah
13个回答

1

有一个很棒的自定义类(SDAVAssetExportSession)可以进行视频压缩。您可以从这个链接下载它。

下载后,将SDAVAssetExportSession.h和SDAVAssetExportSession.m文件添加到您的项目中,然后使用以下代码进行压缩。在下面的代码中,您可以通过指定分辨率和比特率来压缩视频

#import "SDAVAssetExportSession.h"


- (void)compressVideoWithInputVideoUrl:(NSURL *) inputVideoUrl
{
    /* Create Output File Url */

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSString *finalVideoURLString = [documentsDirectory stringByAppendingPathComponent:@"compressedVideo.mp4"];
    NSURL *outputVideoUrl = ([[NSURL URLWithString:finalVideoURLString] isFileURL] == 1)?([NSURL URLWithString:finalVideoURLString]):([NSURL fileURLWithPath:finalVideoURLString]); // Url Should be a file Url, so here we check and convert it into a file Url


    SDAVAssetExportSession *compressionEncoder = [SDAVAssetExportSession.alloc initWithAsset:[AVAsset assetWithURL:inputVideoUrl]]; // provide inputVideo Url Here
    compressionEncoder.outputFileType = AVFileTypeMPEG4;
    compressionEncoder.outputURL = outputVideoUrl; //Provide output video Url here
    compressionEncoder.videoSettings = @
    {
    AVVideoCodecKey: AVVideoCodecH264,
    AVVideoWidthKey: @800,   //Set your resolution width here
    AVVideoHeightKey: @600,  //set your resolution height here
    AVVideoCompressionPropertiesKey: @
        {
        AVVideoAverageBitRateKey: @45000, // Give your bitrate here for lower size give low values
        AVVideoProfileLevelKey: AVVideoProfileLevelH264High40,
        },
    };
    compressionEncoder.audioSettings = @
    {
    AVFormatIDKey: @(kAudioFormatMPEG4AAC),
    AVNumberOfChannelsKey: @2,
    AVSampleRateKey: @44100,
    AVEncoderBitRateKey: @128000,
    };

    [compressionEncoder exportAsynchronouslyWithCompletionHandler:^
     {
         if (compressionEncoder.status == AVAssetExportSessionStatusCompleted)
         {
            NSLog(@"Compression Export Completed Successfully");
         }
         else if (compressionEncoder.status == AVAssetExportSessionStatusCancelled)
         {
             NSLog(@"Compression Export Canceled");
         }
         else
         {
              NSLog(@"Compression Failed");

         }
     }];

}

使用以下代码行取消压缩。
 [compressionEncoder cancelExport]; //Video compression cancel

1
我支持etayluz的答案,SDAVAssetExportSession是一个很棒的自定义类来进行视频压缩。以下是我的代码示例。你可以从这个链接下载SDAVAssetExportSession
下载后将SDAVAssetExportSession.h和SDAVAssetExportSession.m文件添加到您的项目中,然后使用以下代码进行压缩。在下面的代码中,您可以通过指定分辨率和比特率来压缩视频。
#import "SDAVAssetExportSession.h"


- (void)compressVideoWithInputVideoUrl:(NSURL *) inputVideoUrl
{
    /* Create Output File Url */

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSString *finalVideoURLString = [documentsDirectory stringByAppendingPathComponent:@"compressedVideo.mp4"];
    NSURL *outputVideoUrl = ([[NSURL URLWithString:finalVideoURLString] isFileURL] == 1)?([NSURL URLWithString:finalVideoURLString]):([NSURL fileURLWithPath:finalVideoURLString]); // Url Should be a file Url, so here we check and convert it into a file Url


    SDAVAssetExportSession *compressionEncoder = [SDAVAssetExportSession.alloc initWithAsset:[AVAsset assetWithURL:inputVideoUrl]]; // provide inputVideo Url Here
    compressionEncoder.outputFileType = AVFileTypeMPEG4;
    compressionEncoder.outputURL = outputVideoUrl; //Provide output video Url here
    compressionEncoder.videoSettings = @
    {
    AVVideoCodecKey: AVVideoCodecH264,
    AVVideoWidthKey: @800,   //Set your resolution width here
    AVVideoHeightKey: @600,  //set your resolution height here
    AVVideoCompressionPropertiesKey: @
        {
        AVVideoAverageBitRateKey: @45000, // Give your bitrate here for lower size give low values
        AVVideoProfileLevelKey: AVVideoProfileLevelH264High40,
        },
    };
    compressionEncoder.audioSettings = @
    {
    AVFormatIDKey: @(kAudioFormatMPEG4AAC),
    AVNumberOfChannelsKey: @2,
    AVSampleRateKey: @44100,
    AVEncoderBitRateKey: @128000,
    };

    [compressionEncoder exportAsynchronouslyWithCompletionHandler:^
     {
         if (compressionEncoder.status == AVAssetExportSessionStatusCompleted)
         {
            NSLog(@"Compression Export Completed Successfully");
         }
         else if (compressionEncoder.status == AVAssetExportSessionStatusCancelled)
         {
             NSLog(@"Compression Export Canceled");
         }
         else
         {
              NSLog(@"Compression Failed");

         }
     }];

}

取消压缩,请使用以下代码行

 [compressionEncoder cancelExport]; //Video compression cancel

1
这段代码很棒!但是为什么压缩一个20mb的视频需要太长时间?如果我在像Facebook这样的应用程序上进行相同的压缩,它会立即完成。可能出了什么问题?谢谢! - Joaquin Pereira

0

Swift 5

比特率越高,质量和空间就越大。

改进版本:https://dev59.com/yWgt5IYBdhLWcg3w3xPC#62862102

适用于iCloud上的视频,并且可以同时处理多个视频。还可以将进度更改为块参数而不是委托。

import Foundation
import AVKit

protocol VideoEditorDelegate: AnyObject {
    
    func propagate(event: VideoEditor.PropagateEvent)
    
}

final class VideoEditor {
    
    var assetReaders: [AVAssetReader] = []
    var assetWriters: [AVAssetWriter] = []
    
    /// Compress a video URL to H264 in mp4 format
    /// - Parameters:
    ///   - asset: video asset to compress
    ///   - uuID: unique id to identify progress of each video
    ///   - delegate: to handle progress
    ///   - completion: new compressed video url
    func compressVideo(_ asset: AVAsset,
                       uuID: String?,
                       delegate: VideoEditorDelegate?,
                       bitrate: NSNumber = NSNumber(value: 6000000),
                       completion: @escaping (Result<URL, Error>) -> Void) {
        DispatchQueue.global().async {
            //Patch for iCloud videos
            guard let videoData = asset.dataToUpload,
                  let asset = self.avAssetFrom(data: videoData) else {
                completion(.failure(Errors.nilAVAssetData))
                return
            }
            
            guard let reader = try? AVAssetReader(asset: asset) else {
                completion(.failure(Errors.nilAssetReader))
                return
            }
            self.assetReaders.append(reader) //To prevent reader being release while reading
            guard let videoTrack = asset.tracks(withMediaType: .video).first else {
                completion(.failure(Errors.nilVideoTrack))
                return
            }
            let videoReaderSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32ARGB]
            let assetReaderVideoOutput = AVAssetReaderTrackOutput(track: videoTrack,
                                                                  outputSettings: videoReaderSettings)
            
            guard let audioTrack = asset.tracks(withMediaType: .audio).first else {
                completion(.failure(Errors.failToAddAudio))
                return
            }
            
            let audioReaderSettings: [String : Any] = [
                AVFormatIDKey: kAudioFormatLinearPCM,
                AVSampleRateKey: 44100,
                AVNumberOfChannelsKey: 2
            ]
            let assetReaderAudioOutput = AVAssetReaderTrackOutput(track: audioTrack,
                                                                  outputSettings: audioReaderSettings)
            guard reader.canAdd(assetReaderAudioOutput) else {
                completion(.failure(Errors.failToAddAudio))
                return
            }
            reader.add(assetReaderAudioOutput)
            
            guard reader.canAdd(assetReaderVideoOutput) else {
                completion(.failure(Errors.failToAddVideo))
                return
            }
            reader.add(assetReaderVideoOutput)
            
            let videoSettings: [String : Any] = [
                AVVideoCompressionPropertiesKey: [AVVideoAverageBitRateKey: bitrate],
                AVVideoCodecKey: AVVideoCodecType.h264,
                AVVideoHeightKey: videoTrack.naturalSize.height,
                AVVideoWidthKey: videoTrack.naturalSize.width,
                AVVideoScalingModeKey: AVVideoScalingModeResizeAspectFill
            ]
            
            let audioSettings: [String:Any] = [AVFormatIDKey : kAudioFormatMPEG4AAC,
                                       AVNumberOfChannelsKey : 2,
                                             AVSampleRateKey : 44100.0,
                                          AVEncoderBitRateKey: 128000
            ]
            
            let audioInput = AVAssetWriterInput(mediaType: AVMediaType.audio,
                                                outputSettings: audioSettings)
            let videoInput = AVAssetWriterInput(mediaType: AVMediaType.video,
                                                outputSettings: videoSettings)
            videoInput.transform = videoTrack.preferredTransform
            
            let videoInputQueue = DispatchQueue(label: "videoQueue")
            let audioInputQueue = DispatchQueue(label: "audioQueue")
            
            let formatter = DateFormatter()
            formatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
            let date = Date()
            let tempDir = NSTemporaryDirectory()
            let outputPath = "\(tempDir)/\(formatter.string(from: date)).mp4"
            let outputURL = URL(fileURLWithPath: outputPath)
            
            guard let writer = try? AVAssetWriter(outputURL: outputURL,
                                                  fileType: AVFileType.mp4) else {
                completion(.failure(Errors.nilAssetWriter))
                return
            }
            self.assetWriters.append(writer) //To prevent writer being release while writing
            writer.shouldOptimizeForNetworkUse = true
            writer.add(videoInput)
            writer.add(audioInput)
            
            writer.startWriting()
            reader.startReading()
            writer.startSession(atSourceTime: CMTime.zero)
            
            let group = DispatchGroup()

            group.enter()
            audioInput.requestMediaDataWhenReady(on: audioInputQueue) {
                while(audioInput.isReadyForMoreMediaData) {
                    if let cmSampleBuffer = assetReaderAudioOutput.copyNextSampleBuffer() {
                        audioInput.append(cmSampleBuffer)
                    } else {
                        audioInput.markAsFinished()
                        group.leave()
                    }
                }
            }

            group.enter()
            let videoLenth = CMTimeGetSeconds(asset.duration)
            videoInput.requestMediaDataWhenReady(on: videoInputQueue) {
                while(videoInput.isReadyForMoreMediaData) {
                    if let cmSampleBuffer = assetReaderVideoOutput.copyNextSampleBuffer() {
                        videoInput.append(cmSampleBuffer)
                        //Show progress
                        if let uuID = uuID, let delegate = delegate {
                            let timeStamp = CMSampleBufferGetPresentationTimeStamp(cmSampleBuffer)
                            let timeInSecond = CMTimeGetSeconds(timeStamp)
                            let progress = Float(timeInSecond / videoLenth)
                            DispatchQueue.main.async {
                                delegate.propagate(event: .progress(progress,
                                                                     uuID: uuID))
                            }
                        }
                    } else {
                        videoInput.markAsFinished()
                        group.leave()
                    }
                }
            }

            let closeWriter: () -> Void = {
                Task {
                    await writer.finishWriting()
                    do {
                        let data = try Data(contentsOf: writer.outputURL)
                        //TODO ale: track file size
                        print("compressFile -file size after compression: \(Double(data.count / 1048576)) mb")
                    } catch {
                        completion(.failure(error))
                        return
                    }
                    completion(.success(writer.outputURL))
                    writer.cancelWriting()
                    self.assetWriters = self.assetWriters.filter { $0.outputURL != outputURL }
                    self.assetReaders = self.assetReaders.filter { $0.asset != asset }
                }
            }

            group.notify(queue: .global()) {
                closeWriter()
            }
        }
    }
    
    func avAssetFrom(data: Data) -> AVAsset? {
            let directory = NSTemporaryDirectory()
            let fileName = "\(NSUUID().uuidString).mov"
        guard let fullURL = NSURL.fileURL(withPathComponents: [directory, fileName]) else {
            return nil
        }
        do {
            try data.write(to: fullURL)
            let asset = AVAsset(url: fullURL)
            return asset
        } catch {
            return nil
        }
    }
    
}

// MARK: = Helpong Structures

extension VideoEditor {
    
    enum PropagateEvent {
        case progress(Float, uuID: String)
    }
    
    enum Errors: Error {
        case nilAssetReader
        case nilAssetWriter
        case failToAddAudio
        case failToAddVideo
        case nilVideoTrack
        case nilAVAssetData
    }
    
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接