最近做视频压缩上传功能 刚开始用的苹果已经简单封装好的 AVAssetExportSession 框架 视频压缩上传后 发现有些视频压缩后 反而变大了
后来参考了近期视频优化做的比较不错的douyin 发现不论上传多大的视频 被douyin压缩以后比特率(视频每秒传输的大小 单位:比特率(bps) / 千比特率(kbps))基本都维持在1500 kbps左右 经测试 这是对视频压缩后 文件大小 影响最大的一个参数
另外 还有比如帧率 视频宽高 等参数 也对视频压缩有一定的影响
用此方法 在项目中实测 基本可以满足绝大多数需求
如果你仅仅是想完成需求 不想研究代码的话
到这里可以结束阅读了
直接翻到最下面下载Demo 复制我封装好的方法 调用 就行了
继续往下看的话:
代码用到了访问相机 访问相册 访问麦克风 添加视频到相册四个权限
使用前 先把info中相机相册对应权限打开
info.plist 用Source Code方式打开的话 这样写:
<key>NSCameraUsageDescription</key>
<string>是否允许访问相机</string>
<key>NSMicrophoneUsageDescription</key>
<string>App需要您的同意才能访问麦克风</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>是否允许访问相册</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>是否允许添加视频或图片到相册</string>
info.plist 用Property List方式打开的话 这样配置:
配置好了 开始写代码:
ViewController.m
使用系统的UIImagePickerController选择或者拍摄对应的视频
使用前 先遵守<UIImagePickerControllerDelegate, UINavigationControllerDelegate>这俩协议
然后写这个
@property (nonatomic, strong) UIImagePickerController *imagePicker;
从相册中选择视频 代码注释写的比较详细了
//从相册中选择视频
- (IBAction)chooseVideoBtnClick:(id)sender {
if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeSavedPhotosAlbum])
{
self.imagePicker = [[UIImagePickerController alloc] init];
self.imagePicker.delegate = self;
/*
* 设置资源文件来源 图库 相机 相册
* UIImagePickerControllerSourceTypePhotoLibrary,
* UIImagePickerControllerSourceTypeCamera,
* UIImagePickerControllerSourceTypeSavedPhotosAlbum
**/
self.imagePicker.sourceType = UIImagePickerControllerSourceTypeSavedPhotosAlbum;
/*
* 设置媒体类型
* (NSString *)kUTTypeVideo 无声视频
* (NSString *)kUTTypeMovie 有声视频
* (NSString *)kUTTypeAudio 音频
* 等...
**/
[self.imagePicker setMediaTypes:@[(NSString *)kUTTypeMovie]];
self.imagePicker.videoQuality = UIImagePickerControllerQualityTypeHigh;
[self presentViewController:self.imagePicker animated:YES completion:nil];
}else{
[self showAlertViewWithTitle:@"相机不可用" message:@"" withCancelButtonTitle:@"知道了"];
}
}
相机拍摄视频
//拍摄视频
- (IBAction)recordVideoBtnClick:(id)sender {
if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])
{
self.imagePicker = [[UIImagePickerController alloc] init];
self.imagePicker.delegate = self;
/*
* 设置资源文件来源 图库 相机 相册
* UIImagePickerControllerSourceTypePhotoLibrary,
* UIImagePickerControllerSourceTypeCamera,
* UIImagePickerControllerSourceTypeSavedPhotosAlbum
**/
self.imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera;
/*
* 设置媒体类型
* (NSString *)kUTTypeVideo 无声视频
* (NSString *)kUTTypeMovie 有声视频
* (NSString *)kUTTypeAudio 音频
* 等...
**/
[self.imagePicker setMediaTypes:@[(NSString *)kUTTypeMovie]];
self.imagePicker.videoQuality = UIImagePickerControllerQualityTypeHigh;
[self presentViewController:self.imagePicker animated:YES completion:nil];
}else{
[self showAlertViewWithTitle:@"相机不可用" message:@"" withCancelButtonTitle:@"知道了"];
}
}
取消选择或者拍摄时的回调
UIImagePickerControllerDelegate
-(void)imagePickerControllerDidCancel:(UIImagePickerController *)picker{
[self.imagePicker dismissViewControllerAnimated:YES completion:nil];
[self showAlertViewWithTitle:@"用户取消操作" message:@"" withCancelButtonTitle:@"好的"];
}
视频选择 或者拍摄好以后回调
-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<UIImagePickerControllerInfoKey,id> *)info{
//获取媒体类型
NSString *type = [info objectForKey:UIImagePickerControllerMediaType];
//媒体类型是视频时
if ([type isEqualToString:@"public.movie"])
{
NSLog(@"===video URL = %@===", [info objectForKey:UIImagePickerControllerMediaURL]);
//视频路径URL
NSURL *outputUrl = [info objectForKey:UIImagePickerControllerMediaURL];
//关闭相册界面
[picker dismissViewControllerAnimated:YES completion:^{
//执行视频压缩功能
[self compressVideoWithVideoUrl:outputUrl];
}];
}
}
//压缩视频
/*
* 自定义视频压缩
* videoUrl 原视频url路径 必传
* outputBiteRate 压缩视频至指定比特率(bps) 可传nil 默认1500kbps
* outputFrameRate 压缩视频至指定帧率 可传nil 默认30fps
* outputWidth 压缩视频至指定宽度 可传nil 默认960
* outputWidth 压缩视频至指定高度 可传nil 默认540
* compressComplete 压缩后的视频信息回调 (id responseObjc) 可自行打印查看
**/
- (void)compressVideoWithVideoUrl:(NSURL *)outputUrl{
[VideoCompress compressVideoWithVideoUrl:outputUrl withBiteRate:@(1500 * 1024) withFrameRate:@(30) withVideoWidth:@(960) withVideoHeight:@(540) compressComplete:^(id responseObjc) {
NSString *filePathStr = [responseObjc objectForKey:@"urlStr"];
AVURLAsset *asset = [AVURLAsset assetWithURL:[NSURL fileURLWithPath:filePathStr]];
AVAssetTrack *videoTrack = [asset tracksWithMediaType:AVMediaTypeVideo].firstObject;
//视频大小 MB
unsigned long long fileSize = [[NSFileManager defaultManager] attributesOfItemAtPath:filePathStr error:nil].fileSize;
float fileSizeMB = fileSize / (1024.0*1024.0);
//视频宽高
NSInteger videoWidth = videoTrack.naturalSize.width;
NSInteger videoHeight = videoTrack.naturalSize.height;
//比特率
NSInteger kbps = videoTrack.estimatedDataRate / 1024;
//帧率
NSInteger frameRate = [videoTrack nominalFrameRate];
NSLog(@"\nfileSize after compress = %.2f MB,\n videoWidth = %ld,\n videoHeight = %ld,\n video bitRate = %ld\n, video frameRate = %ld", fileSizeMB, videoWidth, videoHeight, kbps, frameRate);
// NSData *videoData = [NSData dataWithContentsOfFile:filePathStr];
// NSData *videoData = [NSData dataWithContentsOfURL:asset.URL];
//在这里上传或者保存已经处理好的视频文件
//保存视频至相册
UISaveVideoAtPathToSavedPhotosAlbum(filePathStr, self, @selector(videoSavedToPhotosAlbum:didFinishSavingWithError:contextInfo:), nil);
}];
}
取消事件的一些弹框
- (void)showAlertViewWithTitle:(NSString*)title message:(NSString*)msg withCancelButtonTitle:(NSString *)cancelButtonTitle{
UIAlertController* alertController = [UIAlertController alertControllerWithTitle:title message:@"" preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:cancelButtonTitle style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
}]];
[self presentViewController:alertController animated:YES completion:nil];
}
//视频保存成功提示框
#pragma mark 保存视频后的回调
- (void)videoSavedToPhotosAlbum:(NSString *)videoUrlStr didFinishSavingWithError:(NSError*)error contextInfo:(id)contextInfo{
NSString*message =@"提示";
if(!error) {
message = @"视频成功保存到相册";
}else{
message = [error description];
}
[self showAlertViewWithTitle:@"提示" message:message withCancelButtonTitle:@"确定"];
}
VideoCompress.m
/*
* 自定义视频压缩
* videoUrl 原视频url路径 必传
* outputBiteRate 压缩视频至指定比特率(bps) 可传nil 默认1500kbps
* outputFrameRate 压缩视频至指定帧率 可传nil 默认30fps
* outputWidth 压缩视频至指定宽度 可传nil 默认960
* outputWidth 压缩视频至指定高度 可传nil 默认540
* compressComplete 压缩后的视频信息回调 (id responseObjc) 可自行打印查看
**/
+ (void)compressVideoWithVideoUrl:(NSURL *)videoUrl withBiteRate:(NSNumber * _Nullable)outputBiteRate withFrameRate:(NSNumber * _Nullable)outputFrameRate withVideoWidth:(NSNumber * _Nullable)outputWidth withVideoHeight:(NSNumber * _Nullable)outputHeight compressComplete:(void(^)(id responseObjc))compressComplete{
if (!videoUrl) {
[SVProgressHUD showErrorWithStatus:@"视频路径不能为空"];
return;
}
NSLog(@"===videoUrl.abs = %@, videoUrl.path = %@", videoUrl.absoluteString, videoUrl.path);
NSInteger compressBiteRate = outputBiteRate ? [outputBiteRate integerValue] : 1500 * 1024;
NSInteger compressFrameRate = outputFrameRate ? [outputFrameRate integerValue] : 30;
NSInteger compressWidth = outputWidth ? [outputWidth integerValue] : 960;
NSInteger compressHeight = outputHeight ? [outputHeight integerValue] : 540;
//取出原视频详细资料
AVURLAsset *asset = [AVURLAsset assetWithURL:videoUrl];
//视频时长 S
CMTime time = [asset duration];
NSInteger seconds = ceil(time.value/time.timescale);
if (seconds < 3) {
[SVProgressHUD showErrorWithStatus:@"请上传3秒以上的视频"];
return;
}
//压缩前原视频大小MB
unsigned long long fileSize = [[NSFileManager defaultManager] attributesOfItemAtPath:videoUrl.path error:nil].fileSize;
float fileSizeMB = fileSize / (1024.0*1024.0);
//取出asset中的视频文件
AVAssetTrack *videoTrack = [asset tracksWithMediaType:AVMediaTypeVideo].firstObject;
//压缩前原视频宽高
NSInteger videoWidth = videoTrack.naturalSize.width;
NSInteger videoHeight = videoTrack.naturalSize.height;
//压缩前原视频比特率
NSInteger kbps = videoTrack.estimatedDataRate / 1024;
//压缩前原视频帧率
NSInteger frameRate = [videoTrack nominalFrameRate];
NSLog(@"\noriginalVideo\nfileSize = %.2f MB,\n videoWidth = %ld,\n videoHeight = %ld,\n video bitRate = %ld\n, video frameRate = %ld", fileSizeMB, videoWidth, videoHeight, kbps, frameRate);
NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithDictionary:@{@"urlStr" : videoUrl.path}];
//原视频比特率小于指定比特率 不压缩 返回原视频
if (kbps <= (compressBiteRate / 1024)) {
compressComplete(dic);
return;
}
//指定压缩视频沙盒根目录
NSString *cachesDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
//添加文件完整路径
NSString *outputUrlStr = [[cachesDir stringByAppendingPathComponent:@"videoTest"] stringByAppendingPathExtension:@"mp4"];
NSLog(@"===压缩视频存放的指定路径%@===", outputUrlStr);
//如果指定路径下已存在其他文件 先移除指定文件
if ([[NSFileManager defaultManager] fileExistsAtPath:outputUrlStr]) {
BOOL removeSuccess = [[NSFileManager defaultManager] removeItemAtPath:outputUrlStr error:nil];
if (!removeSuccess) {
[SVProgressHUD showErrorWithStatus:@"旧文件移除失败"];
return;
}
}
//创建视频文件读取者
AVAssetReader *reader = [AVAssetReader assetReaderWithAsset:asset error:nil];
//从指定文件读取视频
AVAssetReaderTrackOutput *videoOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:videoTrack outputSettings:[VideoCompress configVideoOutput]];
//取出原视频中音频详细资料
AVAssetTrack *audioTrack = [asset tracksWithMediaType:AVMediaTypeAudio].firstObject;
//从音频资料中读取音频
AVAssetReaderTrackOutput *audioOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:audioTrack outputSettings:[VideoCompress configAudioOutput]];
//将读取到的视频信息添加到读者队列中
if ([reader canAddOutput:videoOutput]) {
[reader addOutput:videoOutput];
}
//将读取到的音频信息添加到读者队列中
if ([reader canAddOutput:audioOutput]) {
[reader addOutput:audioOutput];
}
//视频文件写入者
AVAssetWriter *writer = [AVAssetWriter assetWriterWithURL:[NSURL fileURLWithPath:outputUrlStr] fileType:AVFileTypeMPEG4 error:nil];
//根据指定配置创建写入的视频文件
AVAssetWriterInput *videoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:[VideoCompress videoCompressSettingsWithBitRate:compressBiteRate withFrameRate:compressFrameRate withWidth:compressWidth WithHeight:compressHeight withOriginalWidth:videoWidth withOriginalHeight:videoHeight]];
//根据指定配置创建写入的音频文件
AVAssetWriterInput *audioInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:[VideoCompress audioCompressSettings]];
if ([writer canAddInput:videoInput]) {
[writer addInput:videoInput];
NSLog(@"videoInput==========videoInput");
}
if ([writer canAddInput:audioInput]) {
[writer addInput:audioInput];
NSLog(@"audioInput==========audioInput");
}
[SVProgressHUD showWithStatus:@"视频压缩中..."];
[reader startReading];
[writer startWriting];
[writer startSessionAtSourceTime:kCMTimeZero];
//创建视频写入队列
dispatch_queue_t videoQueue = dispatch_queue_create("Video Queue", DISPATCH_QUEUE_SERIAL);
//创建音频写入队列
dispatch_queue_t audioQueue = dispatch_queue_create("Audio Queue", DISPATCH_QUEUE_SERIAL);
//创建一个线程组
dispatch_group_t group = dispatch_group_create();
//进入线程组
dispatch_group_enter(group);
//队列准备好后 usingBlock
[videoInput requestMediaDataWhenReadyOnQueue:videoQueue usingBlock:^{
BOOL completedOrFailed = NO;
while ([videoInput isReadyForMoreMediaData] && !completedOrFailed) {
CMSampleBufferRef sampleBuffer = [videoOutput copyNextSampleBuffer];
if (sampleBuffer != NULL) {
[videoInput appendSampleBuffer:sampleBuffer];
NSLog(@"===%@===", sampleBuffer);
CFRelease(sampleBuffer);
} else {
completedOrFailed = YES;
[videoInput markAsFinished];
dispatch_group_leave(group);
}
}
}];
dispatch_group_enter(group);
//队列准备好后 usingBlock
[audioInput requestMediaDataWhenReadyOnQueue:audioQueue usingBlock:^{
BOOL completedOrFailed = NO;
while ([audioInput isReadyForMoreMediaData] && !completedOrFailed) {
CMSampleBufferRef sampleBuffer = [audioOutput copyNextSampleBuffer];
if (sampleBuffer != NULL) {
BOOL success = [audioInput appendSampleBuffer:sampleBuffer];
NSLog(@"===%@===", sampleBuffer);
CFRelease(sampleBuffer);
completedOrFailed = !success;
} else {
completedOrFailed = YES;
}
}
if (completedOrFailed) {
[audioInput markAsFinished];
dispatch_group_leave(group);
}
}];
//完成压缩
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
if ([reader status] == AVAssetReaderStatusReading) {
[reader cancelReading];
}
switch (writer.status) {
case AVAssetWriterStatusWriting:
{
[SVProgressHUD showSuccessWithStatus:@"视频压缩完成"];
[writer finishWritingWithCompletionHandler:^{
[dic setObject:outputUrlStr forKey:@"urlStr"];
compressComplete(dic);
}];
}
break;
case AVAssetWriterStatusCancelled:
[SVProgressHUD showInfoWithStatus:@"取消压缩"];
break;
case AVAssetWriterStatusFailed:
NSLog(@"===error:%@===", writer.error);
[SVProgressHUD showErrorWithStatus:[NSString stringWithFormat:@"%@",writer.error]];
break;
case AVAssetWriterStatusCompleted:
{
[SVProgressHUD showSuccessWithStatus:@"视频压缩完成"];
[writer finishWritingWithCompletionHandler:^{
[dic setObject:outputUrlStr forKey:@"urlStr"];
compressComplete(dic);
}];
}
break;
default:
break;
}
});
}
视频压缩的参数配置
+ (NSDictionary *)videoCompressSettingsWithBitRate:(NSInteger)biteRate withFrameRate:(NSInteger)frameRate withWidth:(NSInteger)width WithHeight:(NSInteger)height withOriginalWidth:(NSInteger)originalWidth withOriginalHeight:(NSInteger)originalHeight{
/*
* AVVideoAverageBitRateKey: 比特率(码率)每秒传输的文件大小 kbps
* AVVideoExpectedSourceFrameRateKey:帧率 每秒播放的帧数
* AVVideoProfileLevelKey:画质水平
BP-Baseline Profile:基本画质。支持I/P 帧,只支持无交错(Progressive)和CAVLC;
EP-Extended profile:进阶画质。支持I/P/B/SP/SI 帧,只支持无交错(Progressive)和CAVLC;
MP-Main profile:主流画质。提供I/P/B 帧,支持无交错(Progressive)和交错(Interlaced),也支持CAVLC 和CABAC 的支持;
HP-High profile:高级画质。在main Profile 的基础上增加了8×8内部预测、自定义量化、 无损视频编码和更多的YUV 格式;
**/
NSInteger returnWidth = originalWidth > originalHeight ? width : height;
NSInteger returnHeight = originalWidth > originalHeight ? height : width;
NSDictionary *compressProperties = @{
AVVideoAverageBitRateKey : @(biteRate),
AVVideoExpectedSourceFrameRateKey : @(frameRate),
AVVideoProfileLevelKey : AVVideoProfileLevelH264HighAutoLevel
};
if (@available(iOS 11.0, *)) {
NSDictionary *compressSetting = @{
AVVideoCodecKey : AVVideoCodecTypeH264,
AVVideoWidthKey : @(returnWidth),
AVVideoHeightKey : @(returnHeight),
AVVideoCompressionPropertiesKey : compressProperties,
AVVideoScalingModeKey : AVVideoScalingModeResizeAspectFill
};
return compressSetting;
}else {
NSDictionary *compressSetting = @{
AVVideoCodecKey : AVVideoCodecTypeH264,
AVVideoWidthKey : @(returnWidth),
AVVideoHeightKey : @(returnHeight),
AVVideoCompressionPropertiesKey : compressProperties,
AVVideoScalingModeKey : AVVideoScalingModeResizeAspectFill
};
return compressSetting;
}
}
音频压缩的参数配置
//音频设置
+ (NSDictionary *)audioCompressSettings{
AudioChannelLayout stereoChannelLayout = {
.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo,
.mChannelBitmap = kAudioChannelBit_Left,
.mNumberChannelDescriptions = 0,
};
NSData *channelLayoutAsData = [NSData dataWithBytes:&stereoChannelLayout length:offsetof(AudioChannelLayout, mChannelDescriptions)];
NSDictionary *audioCompressSettings = @{
AVFormatIDKey : @(kAudioFormatMPEG4AAC),
AVEncoderBitRateKey : @(128000),
AVSampleRateKey : @(44100),
AVNumberOfChannelsKey : @(2),
AVChannelLayoutKey : channelLayoutAsData
};
return audioCompressSettings;
}
读取音频参数配置
/** 音频解码 */
+ (NSDictionary *)configAudioOutput
{
NSDictionary *audioOutputSetting = @{
AVFormatIDKey: @(kAudioFormatLinearPCM)
};
return audioOutputSetting;
}
读取视频参数配置
/** 视频解码 */
+ (NSDictionary *)configVideoOutput
{
NSDictionary *videoOutputSetting = @{
(__bridge NSString *)kCVPixelBufferPixelFormatTypeKey:[NSNumber numberWithUnsignedInt:kCVPixelFormatType_422YpCbCr8],
(__bridge NSString *)kCVPixelBufferIOSurfacePropertiesKey:[NSDictionary dictionary]
};
return videoOutputSetting;
}