前言
上一篇004-视频H264编码详解(上)中,我们把视频捕捉
和H264编码
这2大功能,都写在ViewController
类中,这样代码会很多很杂,分工也不明确,因此,我们需要进行功能模块划分
+封装
,这样封装后的类功能既明确,也方便使用。
所以,本篇文章主要有2部分内容??
- 对
视频捕捉
和H264编码
这两部分功能进行封装
。 - H264解码(第一部分)相关:主要包括
解码的原理
和解码实施的思路
。
一、视频捕捉管理类封装
首先,我们看看针对视频捕捉管理类
的封装,类暂且命名为CCSystemCapture
,它应该包含以下功能:
开始/结束
捕捉预览层layer
相关摄像头切换
- 最重要的前提 ??
授权检测
输出结果
的回调
真实场景下,除了视频捕捉
外,我们肯定还需考虑音频的捕捉
,所以我们封装捕捉类的时候,可以提供一个枚举
?? 区分音频
和视频
,例如??
//捕获类型
typedef NS_ENUM(int,CCSystemCaptureType){
CCSystemCaptureTypeVideo = 0, //视频
CCSystemCaptureTypeAudio,//音频
CCSystemCaptureTypeAll //音视频都需要
};
复制代码
1.1 准备工作
1.1.1 delegate
现在我们知道,视频的捕捉最终拿到的是未编码前
的流数据,其数据类型是CMSampleBufferRef
,因此我们可以定义一个deleagte,将流数据
输出出去??
@protocol CCSystemCaptureDelegate <NSObject>
@optional
- (void)captureSampleBuffer:(CMSampleBufferRef)sampleBuffer type: (CCSystemCaptureType)type;
@end
复制代码
1.1.2 public 属性和方法
之前捕捉时,有涉及预览层
,视频分辨率的宽和高
,这些都是需要定义的??
/**预览层*/
@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;
}
复制代码
1.1.3 初始化相关
- (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];
}
复制代码
最后属性的懒加载
部分??
- (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.2 开始/结束 捕捉
- (void)start {
if (!self.isRunning) {
self.isRunning = YES;
[self.captureSession startRunning];
}
}
- (void)stop {
if (self.isRunning) {
self.isRunning = NO;
[self.captureSession stopRunning];
}
}
复制代码
1.3 切换摄像头
- (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.4 授权相关
/**
* 麦克风授权
* 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;
}
复制代码
1.5 dealloc
- (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出去。
二、H264硬编码工具类封装
以上是视频捕捉管理类
的封装,接下来就是H264硬编码工具类封装
,类命名为CCVideoEncoder
,编码涉及的流程包括 ??
准备编码 + 编码 + 回调结果
2.1 delegate
和捕捉管理类一样,硬编码也需要将编码后的数据输出,所以定义delegate,根据上篇文章,我们知道,视频的编码就是生成H264文件
,它的格式就是SPS + PPS + NALU
,所以,我们的代理就定义2个方法??
- 方法1:SPS&PPS数据编码回调
- 方法2:H264数据编码完成回调
代码如下??
/**h264编码回调代理*/
@protocol CCVideoEncoderDelegate <NSObject>
//Video-SPS&PPS数据编码回调
- (void)videoEncodeCallbacksps:(NSData *)sps pps:(NSData *)pps;
//Video-H264数据编码完成回调
- (void)videoEncodeCallback:(NSData *)h264Data;
@end
复制代码
2.2 初始化相关
这次新定义一个配置类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
的定义如下 ??
#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
复制代码
2.3 其他私有属性
编码器工具类中,需要做的无非就是 ?? 编码
+ delegate结果
,这2件事我们完全可以分别单独完成,所以需要定义2个异步队列
,编码的话当然最需要的就是编码会话
,所以,最终的私有属性是 ?
@interface CCVideoEncoder ()
//编码队列
@property (nonatomic, strong) dispatch_queue_t encodeQueue;
//回调队列
@property (nonatomic, strong) dispatch_queue_t callbackQueue;
/**编码会话*/
@property (nonatomic) VTCompressionSessionRef encodeSesion;
@end
复制代码
2.4 实现部分
2.4.1 编码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;
}
复制代码
2.4.2 编码session回调
我们知道,编码session创建时,指定的回调函数中,是用来生成H264文件
格式的,主要2部分流程??
- 处理关键帧
SPS + PPS
- 循环遍历处理
其他NALU流数据
⚠️ delegate输出数据时,应该在
回调队列
中完成。
所以,2个delegate的方法,均在此处抛出,代码如下 ??
// 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;
}
}
复制代码
2.4.3 编码
编码相对于编码回调函数,工作流程其实很简单,就是利用核心函数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);
});
}
复制代码
细节补充
在上面的编码回调
和编码
的过程中,细心的你们有没发现2点??
编码回调
中,处理关键帧SPS``PPS
的时候,只处理了一次编码
中,帧的标识符
的处理
这些其实都是通过成员变量缓存
来实现??
@implementation CCVideoEncoder{
long frameID; //帧的递增序标识
BOOL hasSpsPps;//判断是否已经获取到pps和sps
}
复制代码
2.4.4 dealloc
dealloc中就是处理session的释放??
- (void)dealloc {
if (_encodeSesion) {
VTCompressionSessionCompleteFrames(_encodeSesion, kCMTimeInvalid);
VTCompressionSessionInvalidate(_encodeSesion);
CFRelease(_encodeSesion);
_encodeSesion = NULL;
}
}
复制代码
三、视频H264硬解码原理详解
通常,我们小视频开发的完整流程大致是这样??
小视频:H264文件/AAC文件 –> MP4文件(FFMpeg打包) –> 上传到服务器(断点续传)
现在,我们封装了捕捉
和硬编码
,此时拿到了H264文件
格式的流数据,接下来当然是硬解码
流程了。(音频的AAC文件
这个后面再说)
解码的功能大致包括
- 解析数据
(NALU Unit)
, 处理I/P/B 帧
等 - 初始化
解码器
- 将解析后的H264
NALU Unit
传递给解码器
- 解码器解码,将解码数据
delegate
出去 - delegate对象接收到解码数据后,
显示
解码数据(需使用OpenGL ES
)
3.1 解码的三个核心函数
再来介绍下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句,这次再补充一下 ?
VideoToolBox
是基于coreMedia
,coreVideo
,coreFundation
框架的C语言
API.- 包含三种类型session:
编码
、解码
和像素移动
- 从
coreMedia
,coreVideo
框架衍生出时间
或帧管理数据类型
(CMTime
、CVPixelBuffer
) - 视频描述格式 ??
CMVideoFormatDescriptionRef
四、H264硬解码工具类(1)
和编码器
的封装一样,就是初始化
+ 解码
+ delegate输出结果
,解码器的类命名为CCVideoDecoder
??
4.1 解码的delegate
和编码的delegate数据不同,解码输出的是CVPixelBuffer
??
/**h264解码回调代理*/
@protocol CCVideoDecoderDelegate <NSObject>
//解码后H264数据回调
- (void)videoDecodeCallback:(CVPixelBufferRef)imageBuffer;
@end
复制代码
4.2 初始化 & 解码 方法
初始化也是根据配置类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
复制代码
后面的实现部分,请听下回分解!
总结
- 视频捕捉管理类封装
- 基于
AVFoudation
- 分别捕捉
音频
和视频
- 预览层
- 分辨率宽 * 高
- 开始/停止 捕捉
- 摄像头切换
- 权限检测
- delegate输出
未编码
流数据CMSampleBufferRef
dealloc
销毁 捕捉session
- 基于
- H264硬编码工具类封装
- 基于
VideoToolBox
- 2个异步队列
- 编码队列
- 回调队列
- 初始化
- 编码session的创建 ??
VTCompressionSessionCreate
- 设置编码器属性 ??
VTSessionSetProperty
- 编码session的创建 ??
- 编码回调函数
- 处理关键帧
SPS + PPS
- 添加
起始码
- 只需处理一次
- 添加
- 循环遍历处理
其他NALU流数据
- 下标
步长偏移
大端模式
转换成小端模式
- 添加
起始码
- 下标
回调队列
中输出结果
- 处理关键帧
- 编码
编码队列
中异步处理- 获取
未编码
帧数据 ??CMSampleBufferGetImageBuffer
- 编码函数 ??
VTCompressionSessionEncodeFrame
- 帧ID(帧的标识符) + 时间戳
- delegate输出
- 关键帧
SPS
和PPS
- H264数据流
- 关键帧
dealloc
处理 编码session的释放
- 基于
- 视频H264硬解码原理
- 解码大致过程
- 解析数据
(NALU Unit)
, 处理I/P/B 帧
等 - 初始化
解码器
- 将解析后的H264
NALU Unit
传递给解码器
- 解码器解码,将解码数据
delegate
出去 - delegate对象接收到解码数据后,
显示
解码数据(需使用OpenGL ES
)
- 解析数据
- 三个核心函数
- 创建session ??
VTDecompressionSessionCreate
- 解码一个frame ??
VTDecompressionSessionDecodeFrame
- 销毁解码session ??
VTDecompressionSessionInvalidate
- 创建session ??
- 解码的对象
- H264原始码流 ??
NALU
,包括I P B
帧 - 花屏现象 ??
I帧
错误/丢失 - 使用
SPS/PPS
数据,来初始化
解码器
- H264原始码流 ??
- 解码的关键
- 实时解码
- NALU前4位是起始位,第5位是type
帧类型
- 将
帧类型
type转换十进制
,对应表格判断其类型 - 针对不同类型处理帧,其中
SPS/PPS
只要获取就行,是不需要解码 CVPixelBufferRef
保存的是解码后的数据
或者未编码前的数据
- 解码大致过程
- H264硬解码工具类封装
- 头文件
- 初始化解码
- 解码方法
- delegate输出
CVPixelBufferRef
数据
- 头文件