H264编码详解中(H264编码详解中)
上一篇:视频H264编码详解(上)中,我们把视频捕捉和H264编码这2大功能,都写在ViewController类中,这样代码会很多很杂,分工也不明确,因此,我们需要进行功能模块划分 封装,这样封装后的类功能既明确,也方便使用。
所以,本篇文章主要有2部分内容
- 对视频捕捉和H264编码这两部分功能进行封装。
- H264解码(第一部分)相关:主要包括解码的原理 和 解码实施的思路。
首先,我们看看针对视频捕捉管理类的封装,类暂且命名为CCSystemCapture,它应该包含以下功能:
- 开始/结束捕捉
- 预览层layer相关
- 摄像头切换
- 最重要的前提 授权检测
- 输出结果的回调
真实场景下,除了视频捕捉外,我们肯定还需考虑音频的捕捉,所以我们封装捕捉类的时候,可以提供一个枚举 区分音频和视频,例如
//捕获类型
typedef NS_ENUM(int,CCSystemCaptureType){
CCSystemCaptureTypeVideo = 0, //视频
CCSystemCaptureTypeAudio,//音频
CCSystemCaptureTypeAll //音视频都需要
};
C 音视频开发学习资料:点击领取→音视频开发(资料文档 视频教程 面试题)(FFmpeg WebRTC RTMP RTSP HLS RTP)
1.1 准备工作1.1.1 delegate现在我们知道,视频的捕捉最终拿到的是未编码前的流数据,其数据类型是CMSampleBufferRef,因此我们可以定义一个deleagte,将流数据输出出去
@protocol CCSystemCaptureDelegate <NSObject>
@optional
- (void)captureSampleBuffer:(CMSampleBufferRef)sampleBuffer type: (CCSystemCaptureType)type;
@end
之前捕捉时,有涉及预览层,视频分辨率的宽和高,这些都是需要定义的
/**预览层*/
@property (nonatomic, strong) UIView *preview;
/**捕获视频的宽*/
@property (nonatomic, assign, readonly) NSUInteger witdh;
/**捕获视频的高*/
@property (nonatomic, assign, readonly) NSUInteger height;
当然还有delegate
@property (nonatomic, weak) id<CCSystemCaptureDelegate> delegate;
复制代码
对外公开的方法包括
- 预览相关
/** 准备工作(只捕获音频时调用)*/
- (void)prepare;
//捕获内容包括视频时调用(预览层大小,添加到view上用来显示)
- (void)prepareWithPreviewSize:(CGSize)size;
- 捕捉相关
/**开始*/
- (void)start;
/**结束*/
- (void)stop;
/**切换摄像头*/
- (void)changeCamera;
- 权限相关
(int)checkMicrophoneAuthor;//麦克风
(int)checkCameraAuthor;//摄像头
完整版
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
//捕获类型
typedef NS_ENUM(int,CCSystemCaptureType){
CCSystemCaptureTypeVideo = 0,
CCSystemCaptureTypeAudio,
CCSystemCaptureTypeAll
};
@protocol CCSystemCaptureDelegate <NSObject>
@optional
- (void)captureSampleBuffer:(CMSampleBufferRef)sampleBuffer type: (CCSystemCaptureType)type;
@end
/**捕获音视频*/
@interface CCSystemCapture : NSObject
/**预览层*/
@property (nonatomic, strong) UIView *preview;
@property (nonatomic, weak) id<CCSystemCaptureDelegate> delegate;
/**捕获视频的宽*/
@property (nonatomic, assign, readonly) NSUInteger witdh;
/**捕获视频的高*/
@property (nonatomic, assign, readonly) NSUInteger height;
- (instancetype)initWithType:(CCSystemCaptureType)type;
- (instancetype)init UNAVAILABLE_ATTRIBUTE;
/** 准备工作(只捕获音频时调用)*/
- (void)prepare;
//捕获内容包括视频时调用(预览层大小,添加到view上用来显示)
- (void)prepareWithPreviewSize:(CGSize)size;
/**开始*/
- (void)start;
/**结束*/
- (void)stop;
/**切换摄像头*/
- (void)changeCamera;
//授权检测
(int)checkMicrophoneAuthor;//麦克风
(int)checkCameraAuthor;//摄像头
@end
以上是public的,AVFoudation捕捉相关的属性应该都写在扩展类中
@interface CCSystemCapture ()<AVCaptureAudioDataOutputSampleBufferDelegate,AVCaptureVideoDataOutputSampleBufferDelegate>
/********************控制相关**********/
//是否进行
@property (nonatomic, assign) BOOL isRunning;
/********************公共*************/
//会话
@property (nonatomic, strong) AVCaptureSession *captureSession;
//代理队列
@property (nonatomic, strong) dispatch_queue_t captureQueue;
/********************音频相关**********/
//音频设备
@property (nonatomic, strong) AVCaptureDeviceInput *audioInputDevice;
//输出数据接收
@property (nonatomic, strong) AVCaptureAudioDataOutput *audioDataOutput;
@property (nonatomic, strong) AVCaptureConnection *audioConnection;
/********************视频相关**********/
//当前使用的视频设备
@property (nonatomic, weak) AVCaptureDeviceInput *videoInputDevice;
//前后摄像头
@property (nonatomic, strong) AVCaptureDeviceInput *frontCamera;
@property (nonatomic, strong) AVCaptureDeviceInput *backCamera;
//输出数据接收
@property (nonatomic, strong) AVCaptureVideoDataOutput *videoDataOutput;
@property (nonatomic, strong) AVCaptureConnection *videoConnection;
//预览层
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *preLayer;
@property (nonatomic, assign) CGSize prelayerSize;
@end
还有,捕捉类型枚举作为成员变量
@implementation CCSystemCapture{
//捕捉类型
CCSystemCaptureType capture;
}
- (instancetype)initWithType:(CCSystemCaptureType)type {
self = [super init];
if (self) {
capture = type;
}
return self;
}
还包括对外公开的prepare方法
//准备捕获
- (void)prepare {
[self prepareWithPreviewSize:CGSizeZero];
}
//准备捕获(视频/音频)
- (void)prepareWithPreviewSize:(CGSize)size {
_prelayerSize = size;
if (capture == CCSystemCaptureTypeAudio) {
[self setupAudio];
}else if (capture == CCSystemCaptureTypeVideo) {
[self setupVideo];
}else if (capture == CCSystemCaptureTypeAll) {
[self setupAudio];
[self setupVideo];
}
}
接着就是音频和视频的配置相关代码
- 设置音频
- (void)setupAudio {
//麦克风设备
AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
//将audioDevice ->AVCaptureDeviceInput 对象
self.audioInputDevice = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:nil];
//音频输出
self.audioDataOutput = [[AVCaptureAudioDataOutput alloc] init];
[self.audioDataOutput setSampleBufferDelegate:self queue:self.captureQueue];
//配置
[self.captureSession beginConfiguration];
if ([self.captureSession canAddInput:self.audioInputDevice]) {
[self.captureSession addInput:self.audioInputDevice];
}
if([self.captureSession canAddOutput:self.audioDataOutput]){
[self.captureSession addOutput:self.audioDataOutput];
}
[self.captureSession commitConfiguration];
self.audioConnection = [self.audioDataOutput connectionWithMediaType:AVMediaTypeAudio];
}
设置视频
- (void)setupVideo {
//所有video设备
NSArray *videoDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
//前置摄像头
self.frontCamera = [AVCaptureDeviceInput deviceInputWithDevice:videoDevices.lastObject error:nil];
//后置摄像头
self.backCamera = [AVCaptureDeviceInput deviceInputWithDevice:videoDevices.firstObject error:nil];
//设置当前设备为后置
self.videoInputDevice = self.backCamera;
//视频输出
self.videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
[self.videoDataOutput setSampleBufferDelegate:self queue:self.captureQueue];
[self.videoDataOutput setAlwaysDiscardsLateVideoFrames:YES];
//kCVPixelBufferPixelFormatTypeKey它指定像素的输出格式,这个参数直接影响到生成图像的成功与否
// kCVPixelFormatType_420YpCbCr8BiPlanarFullRange YUV420格式.
[self.videoDataOutput setVideoSettings:@{
(__bridge NSString *)kCVPixelBufferPixelFormatTypeKey:@(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)
}];
//配置
[self.captureSession beginConfiguration];
if ([self.captureSession canAddInput:self.videoInputDevice]) {
[self.captureSession addInput:self.videoInputDevice];
}
if([self.captureSession canAddOutput:self.videoDataOutput]){
[self.captureSession addOutput:self.videoDataOutput];
}
//分辨率
[self setVideoPreset];
[self.captureSession commitConfiguration];
//commit后下面的代码才会有效
self.videoConnection = [self.videoDataOutput connectionWithMediaType:AVMediaTypeVideo];
//设置视频输出方向
self.videoConnection.videoOrientation = AVCaptureVideoOrientationPortrait;
//FPS
[self updatefps:25];
//设置预览
[self setupPreviewLayer];
}
这里解释一下fps
FPS是图像领域中的定义,是指画面每秒传输帧数,通俗来讲就是指动画或视频的画面数。FPS是测量用于保存、显示动态视频的信息数量。每秒钟帧数愈多,所显示的动作就会越流畅。通常,要避免动作不流畅的最低是30。某些计算机视频格式,每秒只能提供15帧。
接着就是分辨率的配置
- (void)setVideoPreset{ if ([self.captureSession canSetSessionPreset:AVCaptureSessionPreset1920x1080]) { self.captureSession.sessionPreset = AVCaptureSessionPreset1920x1080; _witdh = 1080; _height = 1920; }else if ([self.captureSession canSetSessionPreset:AVCaptureSessionPreset1280x720]) { self.captureSession.sessionPreset = AVCaptureSessionPreset1280x720; _witdh = 720; _height = 1280; }else{ self.captureSession.sessionPreset = AVCaptureSessionPreset640x480; _witdh = 480; _height = 640; } }
fps的更新(这个也可以写死)
-(void)updateFps:(NSInteger)fps { //获取当前capture设备 NSArray *videoDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; //遍历所有设备(前后摄像头) for (AVCaptureDevice *vDevice in videoDevices) { //获取当前支持的最大fps float maxRate = [(AVFrameRateRange *)[vDevice.activeFormat.videoSupportedFrameRateRanges objectAtIndex:0] maxFrameRate]; //如果想要设置的fps小于或等于最大fps,就进行修改 if (maxRate >= fps) { //实际修改fps的代码 if ([vDevice lockForConfiguration:NULL]) { vDevice.activeVideoMinFrameDuration = CMTimeMake(10, (int)(fps * 10)); vDevice.activeVideoMaxFrameDuration = vDevice.activeVideoMinFrameDuration; [vDevice unlockForConfiguration]; } } } }
预览层的设置
- (void)setupPreviewLayer{ self.preLayer = [AVCaptureVideoPreviewLayer layerWithSession:self.captureSession]; self.preLayer.frame = CGRectMake(0, 0, self.prelayerSize.width, self.prelayerSize.height); //设置满屏 self.preLayer.videoGravity = AVLayerVideoGravityResizeAspectFill; [self.preview.layer addSublayer:self.preLayer]; }
C 音视频开发学习资料:点击领取→音视频开发(资料文档 视频教程 面试题)(FFmpeg WebRTC RTMP RTSP HLS RTP)
最后属性的懒加载部分
1.2 开始/结束 捕捉
- (AVCaptureSession *)captureSession{ if (!_captureSession) { _captureSession = [[AVCaptureSession alloc] init]; } return _captureSession; } - (dispatch_queue_t)captureQueue{ if (!_captureQueue) { _captureQueue = dispatch_queue_create("TMCapture Queue", NULL); } return _captureQueue; } - (UIView *)preview{ if (!_preview) { _preview = [[UIView alloc] init]; } return _preview; }
1.3 切换摄像头
- (void)start { if (!self.isRunning) { self.isRunning = YES; [self.captureSession startRunning]; } } - (void)stop { if (self.isRunning) { self.isRunning = NO; [self.captureSession stopRunning]; } }
1.4 授权相关
- (void)changeCamera{ [self switchCamera]; } -(void)switchCamera{ [self.captureSession beginConfiguration]; [self.captureSession removeInput:self.videoInputDevice]; if ([self.videoInputDevice isEqual: self.frontCamera]) { self.videoInputDevice = self.backCamera; }else{ self.videoInputDevice = self.frontCamera; } [self.captureSession addInput:self.videoInputDevice]; [self.captureSession commitConfiguration]; }
1.5 dealloc
/** * 麦克风授权 * 0 :未授权 1:已授权 -1:拒绝 */ (int)checkMicrophoneAuthor{ int result = 0; //麦克风 AVAudioSessionRecordPermission permissionStatus = [[AVAudioSession sharedInstance] recordPermission]; switch (permissionStatus) { case AVAudioSessionRecordPermissionUndetermined: //请求授权 [[AVAudioSession sharedInstance] requestRecordPermission:^(BOOL granted) { }]; result = 0; break; case AVAudioSessionRecordPermissionDenied://拒绝 result = -1; break; case AVAudioSessionRecordPermissionGranted://允许 result = 1; break; default: break; } return result; } /** * 摄像头授权 * 0 :未授权 1:已授权 -1:拒绝 */ (int)checkCameraAuthor { int result = 0; AVAuthorizationStatus videoStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; switch (videoStatus) { case AVAuthorizationStatusNotDetermined://第一次 //请求授权 [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) { }]; break; case AVAuthorizationStatusAuthorized://已授权 result = 1; break; default: result = -1; break; } return result; }
- (void)dealloc { NSLog(@"capture销毁。。。。"); [self destroyCaptureSession]; } #pragma mark - 销毁会话 - (void)destroyCaptureSession { if (self.captureSession) { if (capture == CCSystemCaptureTypeAudio) { [self.captureSession removeInput:self.audioInputDevice]; [self.captureSession removeOutput:self.audioDataOutput]; }else if (capture == CCSystemCaptureTypeVideo) { [self.captureSession removeInput:self.videoInputDevice]; [self.captureSession removeOutput:self.videoDataOutput]; }else if (capture == CCSystemCaptureTypeAll) { [self.captureSession removeInput:self.audioInputDevice]; [self.captureSession removeOutput:self.audioDataOutput]; [self.captureSession removeInput:self.videoInputDevice]; [self.captureSession removeOutput:self.videoDataOutput]; } } self.captureSession = nil; }
总的来说,视频捕捉管理类CCSystemCapture主要处理了
音频和视频的捕捉,并将最终的结果(即未编码数据)delegate出去。
C 音视频开发学习资料:点击领取→音视频开发(资料文档 视频教程 面试题)(FFmpeg WebRTC RTMP RTSP HLS RTP)
二、H264硬编码工具类封装以上是视频捕捉管理类的封装,接下来就是H264硬编码工具类封装,类命名为CCVideoEncoder,编码涉及的流程包括
准备编码 编码 回调结果
2.1 delegate和捕捉管理类一样,硬编码也需要将编码后的数据输出,所以定义delegate,根据上篇文章,我们知道,视频的编码就是生成H264文件,它的格式就是SPS PPS NALU,所以,我们的代理就定义2个方法
- 方法1:SPS&PPS数据编码回调
- 方法2:H264数据编码完成回调
代码如下
2.2 初始化相关
/**h264编码回调代理*/ @protocol CCVideoEncoderDelegate <NSObject> //Video-SPS&PPS数据编码回调 - (void)videoEncodeCallbacksps:(NSData *)sps pps:(NSData *)pps; //Video-H264数据编码完成回调 - (void)videoEncodeCallback:(NSData *)h264Data; @end
这次新定义一个配置类CCVideoConfig,专门用来做初始化的model CCVideoConfig.h
@interface CCVideoConfig : NSObject @property (nonatomic, assign) NSInteger width;//可选,系统支持的分辨率,采集分辨率的宽 @property (nonatomic, assign) NSInteger height;//可选,系统支持的分辨率,采集分辨率的高 @property (nonatomic, assign) NSInteger bitrate;//码率:自由设置 @property (nonatomic, assign) NSInteger fps;//每秒传输帧数:自由设置 25 (instancetype)defaultConifg; @end
CCVideoConfig.m
@implementation CCVideoConfig (instancetype)defaultConifg { return [[CCVideoConfig alloc] init]; } - (instancetype)init { self = [super init]; if (self) { self.width = 480; self.height = 640; self.bitrate = 640*1000; self.fps = 25; } return self; } @end
于是,CCVideoEncoder初始化的方法可以这么定义
- (instancetype)initWithConfig:(CCVideoConfig*)config;
编码工具类,当然还需定义编码方法,入参不用说,肯定是CMSampleBufferRef
-(void)encodeVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer;
综上,CCVideoEncoder.h的定义如下
2.3 其他私有属性
#import <Foundation/Foundation.h> #import <AVFoundation/AVFoundation.h> #import "CCAVConfig.h" /**h264编码回调代理*/ @protocol CCVideoEncoderDelegate <NSObject> //Video-SPS&PPS数据编码回调 - (void)videoEncodeCallbacksps:(NSData *)sps pps:(NSData *)pps; //Video-H264数据编码完成回调 - (void)videoEncodeCallback:(NSData *)h264Data; @end /**h264硬编码器 (编码和回调均在异步队列执行)*/ @interface CCVideoEncoder : NSObject @property (nonatomic, strong) CCVideoConfig *config; @property (nonatomic, weak) id<CCVideoEncoderDelegate> delegate; - (instancetype)initWithConfig:(CCVideoConfig*)config; /**编码*/ -(void)encodeVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer; @end
编码器工具类中,需要做的无非就是 编码 delegate结果,这2件事我们完全可以分别单独完成,所以需要定义2个异步队列,编码的话当然最需要的就是编码会话,所以,最终的私有属性是
2.4 实现部分2.4.1 编码session初始化 & 配置
@interface CCVideoEncoder () //编码队列 @property (nonatomic, strong) dispatch_queue_t encodeQueue; //回调队列 @property (nonatomic, strong) dispatch_queue_t callbackQueue; /**编码会话*/ @property (nonatomic) VTCompressionSessionRef encodeSesion; @end
2.4.2 编码session回调
- (instancetype)initWithConfig:(CCVideoConfig *)config { self = [super init]; if (self) { _config = config; _encodeQueue = dispatch_queue_create("h264 hard encode queue", DISPATCH_QUEUE_SERIAL); _callbackQueue = dispatch_queue_create("h264 hard encode callback queue", DISPATCH_QUEUE_SERIAL); /**编码设置*/ //创建编码会话 OSStatus status = VTCompressionSessionCreate(kCFAllocatorDefault, (int32_t)_config.width, (int32_t)_config.height, kCMVideoCodecType_H264, NULL, NULL, NULL, VideoEncodeCallback, (__bridge void * _Nullable)(self), &_encodeSesion); if (status != noErr) { NSLog(@"VTCompressionSession create failed. status=%d", (int)status); return self; } //设置编码器属性 //设置是否实时执行 status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue); NSLog(@"VTSessionSetProperty: set RealTime return: %d", (int)status); //指定编码比特流的配置文件和级别。直播一般使用baseline,可减少由于b帧带来的延时 status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel); NSLog(@"VTSessionSetProperty: set profile return: %d", (int)status); //设置码率均值(比特率可以高于此。默认比特率为零,表示视频编码器。应该确定压缩数据的大小。注意,比特率设置只在定时时有效) CFNumberRef bit = (__bridge CFNumberRef)@(_config.bitrate); status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_AverageBitRate, bit); NSLog(@"VTSessionSetProperty: set AverageBitRate return: %d", (int)status); //码率限制(只在定时时起作用)*待确认 CFArrayRef limits = (__bridge CFArrayRef)@[@(_config.bitrate / 4), @(_config.bitrate * 4)]; status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_DataRateLimits,limits); NSLog(@"VTSessionSetProperty: set DataRateLimits return: %d", (int)status); //设置关键帧间隔(GOPSize)GOP太大图像会模糊 CFNumberRef maxKeyFrameInterval = (__bridge CFNumberRef)@(_config.fps * 2); status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_MaxKeyFrameInterval, maxKeyFrameInterval); NSLog(@"VTSessionSetProperty: set MaxKeyFrameInterval return: %d", (int)status); //设置fps(预期) CFNumberRef expectedFrameRate = (__bridge CFNumberRef)@(_config.fps); status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_ExpectedFrameRate, expectedFrameRate); NSLog(@"VTSessionSetProperty: set ExpectedFrameRate return: %d", (int)status); //准备编码 status = VTCompressionSessionPrepareToEncodeFrames(_encodeSesion); NSLog(@"VTSessionSetProperty: set PrepareToEncodeFrames return: %d", (int)status); } return self; }
我们知道,编码session创建时,指定的回调函数中,是用来生成H264文件格式的,主要2部分流程
- 处理关键帧SPS PPS
- 循环遍历处理其他NALU流数据
⚠️ delegate输出数据时,应该在回调队列中完成。
所以,2个delegate的方法,均在此处抛出,代码如下
2.4.3 编码
// startCode 长度 4 const Byte startCode[] = "\x00\x00\x00\x01"; //编码成功回调 void VideoEncodeCallback(void * CM_NULLABLE outputCallbackRefCon, void * CM_NULLABLE sourceFrameRefCon,OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer ) { if (status != noErr) { NSLog(@"VideoEncodeCallback: encode error, status = %d", (int)status); return; } if (!CMSampleBufferDataIsReady(sampleBuffer)) { NSLog(@"VideoEncodeCallback: data is not ready"); return; } CCVideoEncoder *encoder = (__bridge CCVideoEncoder *)(outputCallbackRefCon); //判断是否为关键帧 BOOL keyFrame = NO; CFArrayRef attachArray = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true); keyFrame = !CFDictionaryContainsKey(CFArrayGetValueAtIndex(attachArray, 0), kCMSampleAttachmentKey_NotSync);//(注意取反符号) //获取sps & pps 数据 ,只需获取一次,保存在h264文件开头即可 if (keyFrame && !encoder->hasSpsPps) { size_t spsSize, spsCount; size_t ppsSize, ppsCount; const uint8_t *spsData, *ppsData; //获取图像源格式 CMFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer); OSStatus status1 = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDesc, 0, &spsData, &spsSize, &spsCount, 0); OSStatus status2 = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDesc, 1, &ppsData, &ppsSize, &ppsCount, 0); //判断sps/pps获取成功 if (status1 == noErr & status2 == noErr) { NSLog(@"VideoEncodeCallback: get sps, pps success"); encoder->hasSpsPps = true;//标识true,下次不会再获取sps pps //sps data NSMutableData *sps = [NSMutableData dataWithCapacity:4 spsSize]; [sps appendBytes:startCode length:4]; [sps appendBytes:spsData length:spsSize]; //pps data NSMutableData *pps = [NSMutableData dataWithCapacity:4 ppsSize]; [pps appendBytes:startCode length:4]; [pps appendBytes:ppsData length:ppsSize]; dispatch_async(encoder.callbackQueue, ^{ //回调方法传递sps/pps [encoder.delegate videoEncodeCallbacksps:sps pps:pps]; }); } else { NSLog(@"VideoEncodeCallback: get sps/pps failed spsStatus=%d, ppsStatus=%d", (int)status1, (int)status2); } } //获取NALU数据 size_t lengthAtOffset, totalLength; char *dataPoint; //将数据复制到dataPoint CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer); OSStatus error = CMBlockBufferGetDataPointer(blockBuffer, 0, &lengthAtOffset, &totalLength, &dataPoint); if (error != kCMBlockBufferNoErr) { NSLog(@"VideoEncodeCallback: get datapoint failed, status = %d", (int)error); return; } //循环获取nalu数据 size_t offet = 0; //返回的nalu数据前四个字节不是0001的startcode(不是系统端的0001),而是大端模式的帧长度length const int lengthInfoSize = 4; while (offet < totalLength - lengthInfoSize) { uint32_t naluLength = 0; //获取nalu 数据长度 memcpy(&naluLength, dataPoint offet, lengthInfoSize); //大端转系统端 naluLength = CFSwapInt32BigToHost(naluLength); //获取到编码好的视频数据 NSMutableData *data = [NSMutableData dataWithCapacity:4 naluLength]; [data appendBytes:startCode length:4]; [data appendBytes:dataPoint offet lengthInfoSize length:naluLength]; //将NALU数据回调到代理中 dispatch_async(encoder.callbackQueue, ^{ [encoder.delegate videoEncodeCallback:data]; }); //移动下标,继续读取下一个数据 offet = lengthInfoSize naluLength; } }
编码相对于编码回调函数,工作流程其实很简单,就是利用核心函数VTCompressionSessionEncodeFrame,将CMSampleBuffer生成如下图的这种格式
⚠️ 编码工作当然是在编码队列中处理!
代码
- (void)encodeVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer { CFRetain(sampleBuffer); dispatch_async(_encodeQueue, ^{ //帧数据(未编码) CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer); //该帧的时间戳 frameID ; CMTime timeStamp = CMTimeMake(frameID, 1000); //持续时间 CMTime duration = kCMTimeInvalid; //编码 VTEncodeInfoFlags flags; OSStatus status = VTCompressionSessionEncodeFrame(self.encodeSesion, imageBuffer, timeStamp, duration, NULL, NULL, &flags); if (status != noErr) { NSLog(@"VTCompression: encode failed: status=%d",(int)status); } CFRelease(sampleBuffer); }); }
C 音视频开发学习资料:点击领取→音视频开发(资料文档 视频教程 面试题)(FFmpeg WebRTC RTMP RTSP HLS RTP)
细节补充在上面的编码回调和编码的过程中,细心的你们有没发现2点
- 编码回调中,处理关键帧SPS``PPS的时候,只处理了一次
- 编码中,帧的标识符的处理
这些其实都是通过成员变量缓存来实现
2.4.4 dealloc
@implementation CCVideoEncoder{ long frameID; //帧的递增序标识 BOOL hasSpsPps;//判断是否已经获取到pps和sps }
dealloc中就是处理session的释放
三、视频H264硬解码原理详解
- (void)dealloc { if (_encodeSesion) { VTCompressionSessionCompleteFrames(_encodeSesion, kCMTimeInvalid); VTCompressionSessionInvalidate(_encodeSesion); CFRelease(_encodeSesion); _encodeSesion = NULL; } }
通常,我们小视频开发的完整流程大致是这样
小视频:H264文件/AAC文件 --> MP4文件(FFMpeg打包) --> 上传到服务器(断点续传)
现在,我们封装了捕捉和硬编码,此时拿到了H264文件格式的流数据,接下来当然是硬解码流程了。(音频的AAC文件这个后面再说)
解码的功能大致包括
3.1 解码的三个核心函数
- 解析数据(NALU Unit), 处理I/P/B 帧等
- 初始化解码器
- 将解析后的H264 NALU Unit 传递给 解码器
- 解码器解码,将解码数据delegate出去
- delegate对象接收到解码数据后,显示解码数据(需使用OpenGL ES)
再来介绍下VideoToolBox框架中,解码相关的几个核心C函数
- 创建session VTDecompressionSessionCreate
/*! @function VTDecompressionSessionCreate @abstract 创建用于解压缩视频帧的会话。 @discussion 解压后的帧将通过调用OutputCallback发出 @param allocator 内存的会话。通过使用默认的kCFAllocatorDefault的分配器。 @param videoFormatDescription 描述源视频帧 @param videoDecoderSpecification 指定必须使用的特定视频解码器.NULL @param destinationImageBufferAttributes 描述源像素缓冲区的要求 NULL @param outputCallback 使用已解压缩的帧调用的回调 @param decompressionSessionOut 指向一个变量以接收新的解压会话 */
解码一个frame VTDecompressionSessionDecodeFrame
/* @param session 解码session @param sampleBuffer 源数据 包含一个或多个视频帧的CMsampleBuffer @param decodeFlags 解码标志 @param sourceFrameRefCon 解码后数据outputPixelBuffer @param infoFlagsOut 同步/异步解码标识 */
销毁解码session VTDecompressionSessionInvalidate
3.2 解码的对象
解码的对象 H264原始码流 --> NALU,其中包括关键的帧
- I帧:保留了一张完整的视频帧,这是解码的关键!
- P帧:向前参考帧,里面保存了差异数据,解码它需要依赖I帧。
- B帧:双向参考帧,解码时既需要I帧,也需要P帧。
如果H264码流中I帧错误/丢失,就会导致错误传递,只有P/B帧,是完成不了解码工作的!此时就会出现花屏的现象。
我们在VideoToolBox硬编码H264流数据时,针对的是I帧,在里面手动加入了SPS/PPS。所以解码时,就需要使用SPS/PPS数据,来对解码器进行初始化!
3.3 解码思路详解3.3.1 解析数据
- 因为是流数据(一个接一个),需实时解码
- 首先,分析NALU数据,前面4个字节是起始位,标识一个NALU的开始,第5位才是NALU数据类型type
- 获取到第5位数据,转化成十进制,然后根据表格,判断它的数据类型
- 判断好类型后,才能将NALU送入解码器
- CVPixelBufferRef 保存的是 解码后的数据 或者 未编码前的数据
⚠️ SPS/PPS只要获取就行,是不需要解码的!
3.3.2 VideoToolBox的基本概念上篇文章对VideoToolBox框架只是简单的介绍了2句,这次再补充一下
四、H264硬解码工具类(1)
- VideoToolBox是基于coreMedia, coreVideo, coreFundation框架的C语言API.
- 包含三种类型session:编码、解码和像素移动
- 从coreMedia, coreVideo框架衍生出时间或帧管理数据类型(CMTime、CVPixelBuffer)
- 视频描述格式 CMVideoFormatDescriptionRef
和编码器的封装一样,就是初始化 解码 delegate输出结果,解码器的类命名为CCVideoDecoder
4.1 解码的delegate和编码的delegate数据不同,解码输出的是CVPixelBuffer
4.2 初始化 & 解码 方法
/**h264解码回调代理*/ @protocol CCVideoDecoderDelegate <NSObject> //解码后H264数据回调 - (void)videoDecodeCallback:(CVPixelBufferRef)imageBuffer; @end
初始化也是根据配置类CCVideoConfig
/**初始化解码器**/ - (instancetype)initWithConfig:(CCVideoConfig*)config;
上面我们说过,解码是实时进行的,所以处理的NSData
/**解码h264数据*/ - (void)decodeNaluData:(NSData *)frame;
综上,解码器CCVideoDecoder.h
#import <Foundation/Foundation.h> #import <AVFoundation/AVFoundation.h> #import "CCAVConfig.h" /**h264解码回调代理*/ @protocol CCVideoDecoderDelegate <NSObject> //解码后H264数据回调 - (void)videoDecodeCallback:(CVPixelBufferRef)imageBuffer; @end @interface CCVideoDecoder : NSObject @property (nonatomic, strong) CCVideoConfig *config; @property (nonatomic, weak) id<CCVideoDecoderDelegate> delegate; /**初始化解码器**/ - (instancetype)initWithConfig:(CCVideoConfig*)config; /**解码h264数据*/ - (void)decodeNaluData:(NSData *)frame; @end
后面的实现部分,请听下回分解!
总结,
免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com