H264编码详解中(H264编码详解中)

前言

上一篇:视频H264编码详解(上)中,我们把视频捕捉和H264编码这2大功能,都写在ViewController类中,这样代码会很多很杂,分工也不明确,因此,我们需要进行功能模块划分 封装,这样封装后的类功能既明确,也方便使用。

所以,本篇文章主要有2部分内容

  1. 对视频捕捉和H264编码这两部分功能进行封装。
  2. 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

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]; }

C 音视频开发学习资料:点击领取→音视频开发(资料文档 视频教程 面试题)(FFmpeg WebRTC RTMP RTSP HLS RTP)

最后属性的懒加载部分

- (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出去。

C 音视频开发学习资料:点击领取→音视频开发(资料文档 视频教程 面试题)(FFmpeg WebRTC RTMP RTSP HLS RTP)

二、H264硬编码工具类封装

以上是视频捕捉管理类的封装,接下来就是H264硬编码工具类封装,类命名为CCVideoEncoder,编码涉及的流程包括

准备编码 编码 回调结果

2.1 delegate

和捕捉管理类一样,硬编码也需要将编码后的数据输出,所以定义delegate,根据上篇文章,我们知道,视频的编码就是生成H264文件,它的格式就是SPS PPS NALU,所以,我们的代理就定义2个方法

  1. 方法1:SPS&PPS数据编码回调
  2. 方法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生成如下图的这种格式

H264编码详解中(H264编码详解中)(1)

⚠️ 编码工作当然是在编码队列中处理!

代码

- (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点

  1. 编码回调中,处理关键帧SPS``PPS的时候,只处理了一次
  2. 编码中,帧的标识符的处理

这些其实都是通过成员变量缓存来实现

@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文件这个后面再说)

解码的功能大致包括

  1. 解析数据(NALU Unit), 处理I/P/B 帧等
  2. 初始化解码器
  3. 将解析后的H264 NALU Unit 传递给 解码器
  4. 解码器解码,将解码数据delegate出去
  5. delegate对象接收到解码数据后,显示解码数据(需使用OpenGL ES)
3.1 解码的三个核心函数

再来介绍下VideoToolBox框架中,解码相关的几个核心C函数

  1. 创建session VTDecompressionSessionCreate

H264编码详解中(H264编码详解中)(2)

/*! @function VTDecompressionSessionCreate @abstract 创建用于解压缩视频帧的会话。 @discussion 解压后的帧将通过调用OutputCallback发出 @param allocator 内存的会话。通过使用默认的kCFAllocatorDefault的分配器。 @param videoFormatDescription 描述源视频帧 @param videoDecoderSpecification 指定必须使用的特定视频解码器.NULL @param destinationImageBufferAttributes 描述源像素缓冲区的要求 NULL @param outputCallback 使用已解压缩的帧调用的回调 @param decompressionSessionOut 指向一个变量以接收新的解压会话 */

解码一个frame VTDecompressionSessionDecodeFrame

H264编码详解中(H264编码详解中)(3)

/* @param session 解码session @param sampleBuffer 源数据 包含一个或多个视频帧的CMsampleBuffer @param decodeFlags 解码标志 @param sourceFrameRefCon 解码后数据outputPixelBuffer @param infoFlagsOut 同步/异步解码标识 */

销毁解码session VTDecompressionSessionInvalidate

H264编码详解中(H264编码详解中)(4)

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

后面的实现部分,请听下回分解!

总结

H264编码详解中(H264编码详解中)(5)

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页