• iOS 视频压缩 mov转mp4 码率


    最近还是因为IM模块的功能,IOS录制MOV视频发送后,安卓端无法播放,迫不得已兼容将MOV视频转为MP4发送。

    其中mov视频包括4K/24FPS、4K/30FPS、4K/60FPS、720p HD/30FPS、1080p HD/30FPS、1080p HD/60FPS!

    使用AVAssetExportSession作为导出工具,指定压缩质量AVAssetExportPresetMediumQuality,这样能有效的减少视频体积,但是视频画面清晰度比较差,举个例子:一个25秒的1080p视频,经过压缩后从1080p变为320p,大小从34m变成2.6m。

    1. AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:avAsset presetName:AVAssetExportPresetMediumQuality];
    2. exportSession.outputURL= url;
    3. exportSession.shouldOptimizeForNetworkUse = YES;
    4. exportSession.outputFileType = AVFileTypeMPEG4;
    5. [exportSessionexportAsynchronouslyWithCompletionHandler:^{
    6. switch([exportSessionstatus]) {
    7. case AVAssetExportSessionStatusFailed:
    8. NSLog(@"Export canceled");
    9. break;
    10. case AVAssetExportSessionStatusCancelled:
    11. NSLog(@"Export canceled");
    12. break;
    13. case AVAssetExportSessionStatusCompleted:{
    14. NSLog(@"Successful!");
    15. break;
    16. }
    17. default:break;
    18. }

    重新梳理下我们的需求,我们的场景对视频质量要求稍高,对视频的大小容忍比较高,所以将最大分辨率设为720p。

    所以我们的压缩设置改为AVAssetExportPreset1280x720,压缩后大小几乎没变,从34m变成32.5m。我们可以用mideaInfo来查看下两个视频文件到底有什么区别,上图为1080p,下图为720p:

    由上图可以看到,两个分辨率差别巨大的视频,大小居然差不多,要分析其中的原因首先要了解H264编码。

    3.H264编码

    关于H264编码的原理可以参考(这篇文章),本文不详细展开,只说明几个参数。

    Bit Rate:

    比特率是指每秒传送的比特(bit)数。单位bps(Bit Per Second),比特率越高,每秒传送数据就越多,画质就越清晰。声音中的比特率是指将模拟声音信号转换成数字声音信号后,单位时间内的二进制数据量,是间接衡量音频质量的一个指标。 视频中的比特率(码率)原理与声音中的相同,都是指由模拟信号转换为数字信号后,单位时间内的二进制数据量。

    所以选择适合的比特率是压缩视频大小的关键,比特率设置太小的话,视频会变得模糊,失真。比特率太高的话,视频数据太大,又达不到我们压缩的要求。

    Format profile:

    作为行业标准,H.264编码体系定义了4种不同的Profile(类):Baseline(基线类),Main(主要类), Extended(扩展类)和High Profile(高端类)(它们各自下分成许多个层):
    Baseline Profile 提供I/P帧,仅支持progressive(逐行扫描)和CAVLC;
    Extended Profile 提供I/P/B/SP/SI帧,仅支持progressive(逐行扫描)和CAVLC;
    Main Profile 提供I/P/B帧,支持progressive(逐行扫描)和interlaced(隔行扫描),提供CAVLC或CABAC;
    High Profile (也就是FRExt)在Main Profile基础上新增:8x8 intra prediction(8x8 帧内预测), custom quant(自定义量化), lossless video coding(无损视频编码), 更多的yuv格式(4:4:4...);

    从压缩比例来说 从压缩比例来说,baseline< main < high,由于上图中720p是Main@L3.1,1080p是High@L4,这就是明明分辨率不一样,但是压缩后的大小却差不多的原因。

    关于iPhone设备对的支持

    • iPhone 3GS 和更早的设备支持 Baseline Profile level 3.0 及更低的级别

    • iPhone 4S 支持 High Profile level 4.1 及更低的级别

    • iPhone 5C 支持 High Profile level 4.1 及更低的级别

    • iPhone 5S 支持 High Profile level 4.1 及更低的级别

    • iPad 1 支持 Main Profile level 3.1 及更低的级别

    • iPad 2 支持 Main Profile level 3.1 及更低的级别

    • iPad with Retina display 支持 High Profile level 4.1 及更低的级别

    • iPad mini 支持 High Profile level 4.1 及更低的级别

    GOP

    GOP 指的就是两个I帧之间的间隔。
    在视频编码序列中,主要有三种编码帧:I帧、P帧、B帧。

    1. I帧即Intra-coded picture(帧内编码图像帧),不参考其他图像帧,只利用本帧的信息进行编码
    2. P帧即Predictive-codedPicture(预测编码图像帧),利用之前的I帧或P帧,采用运动预测的方式进行帧间预测编码
    3. B帧即Bidirectionallypredicted picture(双向预测编码图像帧),提供最高的压缩比,它既需要之前的图
      像帧(I帧或P帧),也需要后来的图像帧(P帧),采用运动预测的方式进行帧间双向预测编码
        在视频编码序列中,GOP即Group of picture(图像组),指两个I帧之间的距离,Reference(参考周期)指两个P帧之间的距离。一个I帧所占用的字节数大于一个P帧,一个P帧所占用的字节数大于一个B帧。

    所以在码率不变的前提下,GOP值越大,P、B帧的数量会越多,平均每个I、P、B帧所占用的字节数就越多,也就更容易获取较好的图像质量;Reference越大,B帧的数量越多,同理也更容易获得较好的图像质量。
      需要说明的是,通过提高GOP值来提高图像质量是有限度的,在遇到场景切换的情况时,H.264编码器会自动强制插入一个I帧,此时实际的GOP值被缩短了。另一方面,在一个GOP中,P、B帧是由I帧预测得到的,当I帧的图像质量比较差时,会影响到一个GOP中后续P、B帧的图像质量,直到下一个GOP开始才有可能得以恢复,所以GOP值也不宜设置过大。
      同时,由于P、B帧的复杂度大于I帧,所以过多的P、B帧会影响编码效率,使编码效率降低。另外,过长的GOP还会影响Seek操作的响应速度,由于P、B帧是由前面的I或P帧预测得到的,所以Seek操作需要直接定位,解码某一个P或B帧时,需要先解码得到本GOP内的I帧及之前的N个预测帧才可以,GOP值越长,需要解码的预测帧就越多,seek响应的时间也越长。
    M 和 N :M值表示I帧或者P帧之间的帧数目,N值表示GOP的长度。N的至越大,代表压缩率越大。因为图2中N=15远小于图一中N=30。这也是720p尺寸压缩不理想的原因。

    4.解决思路

    由上可知压缩视频主要可以采用以下几种手段:

    • 降低分辨率
    • 降低码率
    • 指定高的 Format profile

    由于业务指定分辨率为720p,所以我们只能尝试另外两种方法。

    降低码率

    根据这篇文章Video Encoding Settings for H.264 Excellence,推荐了适合720p的推荐码率为2400~3700之间。之前压缩的文件码率为9979,所以码率还是有很大的优化空间的。

    宽屏

    非宽屏

    指定高的 Format profile

    由于现在大部分的设备都支持High Profile level,所以我们可以把Format profileMain Profile level改为High Profile level

    现在我们已经知道要做什么了,那么怎么做呢?

    5.解决方法

    由于之前的AVAssetExportSession不能指定码率和Format profile,我们这里需要使用AVAssetReaderAVAssetWriter

    AVAssetReader负责将数据从asset里拿出来,AVAssetWriter负责将得到的数据存成文件。
    核心代码如下:

    1. //生成reader 和 writer
    2. self.reader = [AVAssetReader.alloc initWithAsset:self.asset error:&readerError];
    3. self.writer = [AVAssetWriter assetWriterWithURL:self.outputURL fileType:self.outputFileType error:&writerError];
    4. //视频
    5. if (videoTracks.count > 0) {
    6. self.videoOutput = [AVAssetReaderVideoCompositionOutput assetReaderVideoCompositionOutputWithVideoTracks:videoTracks videoSettings:self.videoInputSettings];
    7. self.videoOutput.alwaysCopiesSampleData = NO;
    8. if (self.videoComposition)
    9. {
    10. self.videoOutput.videoComposition = self.videoComposition;
    11. }
    12. else
    13. {
    14. self.videoOutput.videoComposition = [self buildDefaultVideoComposition];
    15. }
    16. if ([self.reader canAddOutput:self.videoOutput])
    17. {
    18. [self.reader addOutput:self.videoOutput];
    19. }
    20. //
    21. // Video input
    22. //
    23. self.videoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:self.videoSettings];
    24. self.videoInput.expectsMediaDataInRealTime = NO;
    25. if ([self.writer canAddInput:self.videoInput])
    26. {
    27. [self.writer addInput:self.videoInput];
    28. }
    29. NSDictionary *pixelBufferAttributes = @
    30. {
    31. (id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA),
    32. (id)kCVPixelBufferWidthKey: @(self.videoOutput.videoComposition.renderSize.width),
    33. (id)kCVPixelBufferHeightKey: @(self.videoOutput.videoComposition.renderSize.height),
    34. @"IOSurfaceOpenGLESTextureCompatibility": @YES,
    35. @"IOSurfaceOpenGLESFBOCompatibility": @YES,
    36. };
    37. self.videoPixelBufferAdaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:self.videoInput sourcePixelBufferAttributes:pixelBufferAttributes];
    38. }
    39. //音频
    40. NSArray *audioTracks = [self.asset tracksWithMediaType:AVMediaTypeAudio];
    41. if (audioTracks.count > 0) {
    42. self.audioOutput = [AVAssetReaderAudioMixOutput assetReaderAudioMixOutputWithAudioTracks:audioTracks audioSettings:nil];
    43. self.audioOutput.alwaysCopiesSampleData = NO;
    44. self.audioOutput.audioMix = self.audioMix;
    45. if ([self.reader canAddOutput:self.audioOutput])
    46. {
    47. [self.reader addOutput:self.audioOutput];
    48. }
    49. } else {
    50. // Just in case this gets reused
    51. self.audioOutput = nil;
    52. }
    53. //
    54. // Audio input
    55. //
    56. if (self.audioOutput) {
    57. self.audioInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:self.audioSettings];
    58. self.audioInput.expectsMediaDataInRealTime = NO;
    59. if ([self.writer canAddInput:self.audioInput])
    60. {
    61. [self.writer addInput:self.audioInput];
    62. }
    63. }
    64. //开始读写
    65. [self.writer startWriting];
    66. [self.reader startReading];
    67. [self.writer startSessionAtSourceTime:self.timeRange.start];
    68. //压缩完成的回调
    69. __block BOOL videoCompleted = NO;
    70. __block BOOL audioCompleted = NO;
    71. __weak typeof(self) wself = self;
    72. self.inputQueue = dispatch_queue_create("VideoEncoderInputQueue", DISPATCH_QUEUE_SERIAL);
    73. if (videoTracks.count > 0) {
    74. [self.videoInput requestMediaDataWhenReadyOnQueue:self.inputQueue usingBlock:^
    75. {
    76. if (![wself encodeReadySamplesFromOutput:wself.videoOutput toInput:wself.videoInput])
    77. {
    78. @synchronized(wself)
    79. {
    80. videoCompleted = YES;
    81. if (audioCompleted)
    82. {
    83. [wself finish];
    84. }
    85. }
    86. }
    87. }];
    88. }
    89. else {
    90. videoCompleted = YES;
    91. }
    92. if (!self.audioOutput) {
    93. audioCompleted = YES;
    94. } else {
    95. [self.audioInput requestMediaDataWhenReadyOnQueue:self.inputQueue usingBlock:^
    96. {
    97. if (![wself encodeReadySamplesFromOutput:wself.audioOutput toInput:wself.audioInput])
    98. {
    99. @synchronized(wself)
    100. {
    101. audioCompleted = YES;
    102. if (videoCompleted)
    103. {
    104. [wself finish];
    105. }
    106. }
    107. }
    108. }];
    109. }

    其中self.videoInput里的self.videoSettings我们需要对视频压缩参数做设置

    1. self.videoSettings = @
    2. {
    3. AVVideoCodecKey: AVVideoCodecH264,
    4. AVVideoWidthKey: @1280,
    5. AVVideoHeightKey: @720,
    6. AVVideoCompressionPropertiesKey: @
    7. {
    8. AVVideoAverageBitRateKey: @3000000,
    9. AVVideoProfileLevelKey: AVVideoProfileLevelH264High40,
    10. },
    11. };

    封装好的控件可以参考https://blog.csdn.net/yunhuaikong/article/details/133300420?spm=1001.2014.3001.5502

    6.最终效果

    通过下图我们可以看到,视频已经成功被压缩成10m左右。结合视频效果,这个压缩成果我们还是很满意的。

    压缩后的文件

    下面是视频部分画面截图的效果:

                                                                    原视频,34m

                                                    原来的MediumQuality压缩效果,2.6m

                                                             原来的720p压缩效果,32.5m

                                                            优化后的720p的压缩效果,10m

    7.视频转码时遇到的坑

    使用 SDAVAssetExportSession 时遇到一个坑,大部分视频转码没问题,部分视频转码会有黑屏问题,最后定位出现问题的代码如下:

    1. - (AVMutableVideoComposition *)buildDefaultVideoComposition
    2. {
    3. AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoComposition];
    4. AVAssetTrack *videoTrack = [[self.asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
    5. // get the frame rate from videoSettings, if not set then try to get it from the video track,
    6. // if not set (mainly when asset is AVComposition) then use the default frame rate of 30
    7. float trackFrameRate = 0;
    8. if (self.videoSettings)
    9. {
    10. NSDictionary *videoCompressionProperties = [self.videoSettings objectForKey:AVVideoCompressionPropertiesKey];
    11. if (videoCompressionProperties)
    12. {
    13. NSNumber *frameRate = [videoCompressionProperties objectForKey:AVVideoAverageNonDroppableFrameRateKey];
    14. if (frameRate)
    15. {
    16. trackFrameRate = frameRate.floatValue;
    17. }
    18. }
    19. }
    20. else
    21. {
    22. trackFrameRate = [videoTrack nominalFrameRate];
    23. }
    24. if (trackFrameRate == 0)
    25. {
    26. trackFrameRate = 30;
    27. }
    28. videoComposition.frameDuration = CMTimeMake(1, trackFrameRate);
    29. CGSize targetSize = CGSizeMake([self.videoSettings[AVVideoWidthKey] floatValue], [self.videoSettings[AVVideoHeightKey] floatValue]);
    30. CGSize naturalSize = [videoTrack naturalSize];
    31. CGAffineTransform transform = videoTrack.preferredTransform;
    32. // Workaround radar 31928389, see https://github.com/rs/SDAVAssetExportSession/pull/70 for more info
    33. if (transform.ty == -560) {
    34. transform.ty = 0;
    35. }
    36. if (transform.tx == -560) {
    37. transform.tx = 0;
    38. }
    39. CGFloat videoAngleInDegree = atan2(transform.b, transform.a) * 180 / M_PI;
    40. if (videoAngleInDegree == 90 || videoAngleInDegree == -90) {
    41. CGFloat width = naturalSize.width;
    42. naturalSize.width = naturalSize.height;
    43. naturalSize.height = width;
    44. }
    45. videoComposition.renderSize = naturalSize;
    46. // center inside
    47. {
    48. float ratio;
    49. float xratio = targetSize.width / naturalSize.width;
    50. float yratio = targetSize.height / naturalSize.height;
    51. ratio = MIN(xratio, yratio);
    52. float postWidth = naturalSize.width * ratio;
    53. float postHeight = naturalSize.height * ratio;
    54. float transx = (targetSize.width - postWidth) / 2;
    55. float transy = (targetSize.height - postHeight) / 2;
    56. CGAffineTransform matrix = CGAffineTransformMakeTranslation(transx / xratio, transy / yratio);
    57. matrix = CGAffineTransformScale(matrix, ratio / xratio, ratio / yratio);
    58. transform = CGAffineTransformConcat(transform, matrix);
    59. }
    60. // Make a "pass through video track" video composition.
    61. AVMutableVideoCompositionInstruction *passThroughInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
    62. passThroughInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, self.asset.duration);
    63. AVMutableVideoCompositionLayerInstruction *passThroughLayer = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:videoTrack];
    64. [passThroughLayer setTransform:transform atTime:kCMTimeZero];
    65. passThroughInstruction.layerInstructions = @[passThroughLayer];
    66. videoComposition.instructions = @[passThroughInstruction];
    67. return videoComposition;
    68. }
    1. transform 不正确引起的黑屏
    CGAffineTransform transform = videoTrack.preferredTransform;
    

    1.参考评论区 @baopanpan同学的说法,TZImagePickerController可以解决,找到代码试了一下,确实不会出现黑屏的问题了,代码如下

    1. /// 获取优化后的视频转向信息
    2. - (AVMutableVideoComposition *)fixedCompositionWithAsset:(AVAsset *)videoAsset {
    3. AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoComposition];
    4. // 视频转向
    5. int degrees = [self degressFromVideoFileWithAsset:videoAsset];
    6. if (degrees != 0) {
    7. CGAffineTransform translateToCenter;
    8. CGAffineTransform mixedTransform;
    9. videoComposition.frameDuration = CMTimeMake(1, 30);
    10. NSArray *tracks = [videoAsset tracksWithMediaType:AVMediaTypeVideo];
    11. AVAssetTrack *videoTrack = [tracks objectAtIndex:0];
    12. AVMutableVideoCompositionInstruction *roateInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
    13. roateInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, [videoAsset duration]);
    14. AVMutableVideoCompositionLayerInstruction *roateLayerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:videoTrack];
    15. if (degrees == 90) {
    16. // 顺时针旋转90°
    17. translateToCenter = CGAffineTransformMakeTranslation(videoTrack.naturalSize.height, 0.0);
    18. mixedTransform = CGAffineTransformRotate(translateToCenter,M_PI_2);
    19. videoComposition.renderSize = CGSizeMake(videoTrack.naturalSize.height,videoTrack.naturalSize.width);
    20. [roateLayerInstruction setTransform:mixedTransform atTime:kCMTimeZero];
    21. } else if(degrees == 180){
    22. // 顺时针旋转180°
    23. translateToCenter = CGAffineTransformMakeTranslation(videoTrack.naturalSize.width, videoTrack.naturalSize.height);
    24. mixedTransform = CGAffineTransformRotate(translateToCenter,M_PI);
    25. videoComposition.renderSize = CGSizeMake(videoTrack.naturalSize.width,videoTrack.naturalSize.height);
    26. [roateLayerInstruction setTransform:mixedTransform atTime:kCMTimeZero];
    27. } else if(degrees == 270){
    28. // 顺时针旋转270°
    29. translateToCenter = CGAffineTransformMakeTranslation(0.0, videoTrack.naturalSize.width);
    30. mixedTransform = CGAffineTransformRotate(translateToCenter,M_PI_2*3.0);
    31. videoComposition.renderSize = CGSizeMake(videoTrack.naturalSize.height,videoTrack.naturalSize.width);
    32. [roateLayerInstruction setTransform:mixedTransform atTime:kCMTimeZero];
    33. }else {//增加异常处理
    34. videoComposition.renderSize = CGSizeMake(videoTrack.naturalSize.width,videoTrack.naturalSize.height);
    35. }
    36. roateInstruction.layerInstructions = @[roateLayerInstruction];
    37. // 加入视频方向信息
    38. videoComposition.instructions = @[roateInstruction];
    39. }
    40. return videoComposition;
    41. }
    42. /// 获取视频角度
    43. - (int)degressFromVideoFileWithAsset:(AVAsset *)asset {
    44. int degress = 0;
    45. NSArray *tracks = [asset tracksWithMediaType:AVMediaTypeVideo];
    46. if([tracks count] > 0) {
    47. AVAssetTrack *videoTrack = [tracks objectAtIndex:0];
    48. CGAffineTransform t = videoTrack.preferredTransform;
    49. if(t.a == 0 && t.b == 1.0 && t.c == -1.0 && t.d == 0){
    50. // Portrait
    51. degress = 90;
    52. } else if(t.a == 0 && t.b == -1.0 && t.c == 1.0 && t.d == 0){
    53. // PortraitUpsideDown
    54. degress = 270;
    55. } else if(t.a == 1.0 && t.b == 0 && t.c == 0 && t.d == 1.0){
    56. // LandscapeRight
    57. degress = 0;
    58. } else if(t.a == -1.0 && t.b == 0 && t.c == 0 && t.d == -1.0){
    59. // LandscapeLeft
    60. degress = 180;
    61. }
    62. }
    63. return degress;
    64. }
    2. naturalSize 不正确的坑

    之前用模拟器录屏得到一个视频,调用[videoTrack naturalSize]的时候,得到的size为 (CGSize) naturalSize = (width = 828, height = 0.02734375),明显是不正确的,这个暂时没有找到解决办法,知道的同学可以在下面评论一下。

    3. 视频黑边问题

    视频黑边应该是视频源尺寸和目标尺寸比例不一致造成的,需要根据原尺寸的比例算出目标尺寸

    1. CGSize targetSize = CGSizeMake(videoAsset.pixelWidth, videoAsset.pixelHeight);
    2. //尺寸过大才压缩,否则不更改targetSize
    3. if (targetSize.width * targetSize.height > 1280 * 720) {
    4. int width = 0,height = 0;
    5. if (targetSize.width > targetSize.height) {
    6. width = 1280;
    7. height = 1280 * targetSize.height/targetSize.width;
    8. }else {
    9. width = 720;
    10. height = 720 * targetSize.height/targetSize.width;
    11. }
    12. targetSize = CGSizeMake(width, height);
    13. }else if (targetSize.width == 0 || targetSize.height == 0) {//异常情况处理
    14. targetSize = CGSizeMake(720, 1280);
    15. }
  • 相关阅读:
    GBase 8c 分析表
    idea设置格式化竖线
    error: no member named ‘int8_t‘ in the global namespace
    MFC为“对话框中的控件添加变量”,QT中使用“ui.对象名称”来调用控件
    Android判断应用是否在前台运行
    我的创作纪念日
    解决Spring Boot启动异常:未配置数据源的问题
    Vulhub 靶场使用
    【机器学习】支持向量机分类
    go 语言爬虫库goquery介绍
  • 原文地址:https://blog.csdn.net/yunhuaikong/article/details/133344926