005 – 视频H264编码详解(中)

前言

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

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

  1. 视频捕捉H264编码这两部分功能进行封装
  2. 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. 方法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生成如下图的这种格式??

image.png

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

代码??

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

  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

image.png

/*!
     @function    VTDecompressionSessionCreate
     @abstract    创建用于解压缩视频帧的会话。
     @discussion  解压后的帧将通过调用OutputCallback发出

     @param    allocator  内存的会话。通过使用默认的kCFAllocatorDefault的分配器。
     @param    videoFormatDescription 描述源视频帧
     @param    videoDecoderSpecification 指定必须使用的特定视频解码器.NULL
     @param    destinationImageBufferAttributes 描述源像素缓冲区的要求 NULL
     @param    outputCallback 使用已解压缩的帧调用的回调
     @param    decompressionSessionOut 指向一个变量以接收新的解压会话
     */
复制代码
  1. 解码一个frame ?? VTDecompressionSessionDecodeFrame

image.png

/*
     @param    session 解码session
     @param    sampleBuffer 源数据 包含一个或多个视频帧的CMsampleBuffer
     @param    decodeFlags 解码标志
     @param    sourceFrameRefCon 解码后数据outputPixelBuffer
     @param    infoFlagsOut 同步/异步解码标识
 */
复制代码
  1. 销毁解码session ?? VTDecompressionSessionInvalidate

image.png

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框架衍生出时间帧管理数据类型(CMTimeCVPixelBuffer)
  • 视频描述格式 ?? 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
    • 编码回调函数
      • 处理关键帧SPS + PPS
        • 添加起始码
        • 只需处理一次
      • 循环遍历处理其他NALU流数据
        • 下标步长偏移
        • 大端模式 转换成 小端模式
        • 添加起始码
      • 回调队列中输出结果
    • 编码
      • 编码队列中异步处理
      • 获取未编码帧数据 ?? CMSampleBufferGetImageBuffer
      • 编码函数 ?? VTCompressionSessionEncodeFrame
      • 帧ID(帧的标识符) + 时间戳
    • delegate输出
      • 关键帧SPSPPS
      • H264数据流
    • dealloc 处理 编码session的释放
  • 视频H264硬解码原理
    • 解码大致过程
      1. 解析数据(NALU Unit), 处理I/P/B 帧
      2. 初始化解码器
      3. 将解析后的H264 NALU Unit 传递给 解码器
      4. 解码器解码,将解码数据delegate出去
      5. delegate对象接收到解码数据后,显示解码数据(需使用OpenGL ES
    • 三个核心函数
      • 创建session ?? VTDecompressionSessionCreate
      • 解码一个frame ?? VTDecompressionSessionDecodeFrame
      • 销毁解码session ?? VTDecompressionSessionInvalidate
    • 解码的对象
      • H264原始码流 ?? NALU,包括I P B
      • 花屏现象 ?? I帧错误/丢失
      • 使用SPS/PPS数据,来初始化解码器
    • 解码的关键
      • 实时解码
      • NALU前4位是起始位,第5位是type帧类型
      • 帧类型type转换十进制,对应表格判断其类型
      • 针对不同类型处理帧,其中SPS/PPS只要获取就行,是不需要解码
      • CVPixelBufferRef 保存的是 解码后的数据 或者 未编码前的数据
  • H264硬解码工具类封装
    • 头文件
      • 初始化解码
      • 解码方法
      • delegate输出CVPixelBufferRef数据
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享