AVFounction学习笔记之–VideoToolbox视频硬编码

  • 视频编码相关知识概念

帧:每帧代表一张静态的图像
GOP:GOP就是一组连续额画面,每个画面都是一帧,一个GOP就是很多帧的集合,GOP cache长度越长,画面质量越好
码率:画面进行压缩后每秒显示的数据量
帧率:每秒显示图片的数(人眼所看画面在16帧以上,就会认为是连贯的)
分辨率:图片的长度 * 宽度,图片的尺寸
压缩前每秒数据:帧率 * 分辨率
压缩比:压缩前的每秒钟数据 / 码率(压缩比越高,画面质量越差)
视频封装格式:一种存储视频信息的容器(流式封装:TS\FLV;索引封装:MP4\MOV\AVI)
主要作用:一个视频文件往往会包含图像和音频,还有一些配置信息,这些内容需要按照一定规则组织封装起来
注意:封装格式和文件格式一样,因为一般视频文件格式的后缀即采用相应的视频封装格式的名称,所以视频文件格式就是视频封装格式

I帧(关键帧):帧内编码帧,包含一帧画面的完整帧,是P帧和B帧的参考帧,占用数据的信息量比较大,是GOP基础帧的第一帧,一组GOP中只有一个I帧
P帧(差别帧):保留帧与前帧的区别(以I帧为参考帧),解码需要缓存画面叠加本帧定义的差别,生成最终画面,P帧只存储差别数据,并不是完整帧,压缩比比较高
B帧(双向差别帧):记录的是本帧与前后帧的差别。通过前面的I帧或P帧和后面的P帧来进行预测的

帧内压缩(空间压缩):只考虑本帧数据,不考虑相邻帧之间的冗余信息。当压缩⼀帧图像时,仅考虑本帧的数据⽽不考虑相邻帧之间的冗余信息,这实际上与静态图像压缩类似。帧内⼀般采⽤用有损压缩算法,由于帧内压缩是编码一个完整的图像,所以可以独立的解码、显示。帧内压缩一般达不不到很⾼高的压缩,跟编码jpeg差不多

帧间压缩:通过比较时间轴上不同帧之间的数据进行压缩。帧间压缩一般是无损的。

码率计算公式

名称 公式 192*144 320*240 480*360 640*480 1280*720 1920*1080
极低码率 宽* 高 * 3/4 30kb/s 60kb/s 120kb/s 250kb/s 500kb/s 1mbps
低码率 宽* 高 * 3/2 60kb/s 120kb/s 250kb/s 500kb/s 1mbps 2mbps
中码率 宽* 高 * 3 120kb/s 250kb/s 500kb/s 1mbps 2mbps 4mbps
高码率 宽* 高 * 3 * 2 250kb/s 500kb/s 1mbps 2mbps 4mbps 8mbps
极高码率 宽* 高 * 3 * 4 500kb/s 1mbps 2mbps 4mbps 8mbps 16mbps
  • H264视频编码

H264视频编码压缩方法:
1、分组:把几帧图像分为一组(GOP),为防止运动变化,帧数不宜去多
2、定义帧:将每组内各帧图像定义为三种类型:I帧、B帧、P帧
3、预测帧:以I帧作为基础,以I帧预测P帧,再由I帧预测B帧
4、数据传输:最后将I帧数据与预测的差值信息进行存储和传输

H264 NAL头解析
如果NALU对相应的Slice为一帧的开始,则用4字节表示,即0x00000001;否则用3字节表示,0x000001、
NAL Header: forbidden_bit, nal_reference_bit(优先级)2bit,nal_unit_type(类型)5bit。标识NAL单位称为VCL的NAL单元,其他类型的NAL单元为非VCL的NAL单元
0:未规定
1:非IDR图像中不采用数据划分的片段
2:非IDR图像中A类数据划分片段
3:非IDR图像中B类数据划分片段
4:非IDR图像中C类数据划分片段
5:IDR图像的片段
6:补充增强信息(SEI)
7:序列参数集(SPS)
8:图像参数集(PPS)
9:分割符
10:序列结束符
11:流结束符
12:填充数据
13:序列参数集扩展
14:带前缀的NAL单元
15:子序列参数集
16-18:保留
19:不采用数据划分的辅助编码图像片段
20:编码片段扩展
21-23:保留
14-31:未规定
H.264的SPS和PPS串,包含了初始化H.264解析器所需要的信息参数,包括编码所用的profile,level,图像的宽高,deblock滤波器等

  • 编码数据格式

编码前或者解码后的数据格式 = CMSampleBuffer = CMTime + CMVideoFormatDesc + CVPixelBuffer
编码后的数据格式 = CMSampleBuffer = CMTime + CMVideoFormat(图像存储格式) + CMBlockBuffer

FFmpeg或者硬编码编码完成的H264数据格式
AVFounction学习笔记之--VideoToolbox视频硬编码

  • VideoToolbox编码流程

AVFounction学习笔记之--VideoToolbox视频硬编码

  • VideoToolbox示例代码
#import "H264Encoder.h"
#import <VideoToolbox/VideoToolbox.h>

@interface H264Encoder()
@property(nonatomic, assign)int frameID;
@property(nonatomic, assign)VTCompressionSessionRef cEncodeingSession;
@property (nonatomic, strong) NSFileHandle * videoFileHandle;
@property (nonatomic, strong) dispatch_queue_t encodeQueue;
@end

@implementation H264Encoder

- (instancetype)init
{
    self = [super init];
    if (self) {
        dispatch_sync(self.encodeQueue, ^{
            [self initVideoToolbox];
        });
    }
    return self;
}

- (void)stopEncode
{
    VTCompressionSessionCompleteFrames(self.cEncodeingSession, kCMTimeInvalid);
    VTCompressionSessionInvalidate(self.cEncodeingSession);
    CFRelease(self.cEncodeingSession);
    self.cEncodeingSession = NULL;
    [self.videoFileHandle closeFile];
    self.videoFileHandle = NULL;
}


- (void)encodeH264:(CMSampleBufferRef)sampleBuffer {
    dispatch_sync(self.encodeQueue, ^{
        NSLog(@"H264编码中...");
        // 拿到每一帧的未编码的数据
        CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
        // 根据当前的帧数创建帧时间
        CMTime ptime = CMTimeMake(self.frameID ++, 1000);
        // 编码准备
        VTEncodeInfoFlags flags; // 0 同步编码 1表示异步编码
        OSStatus status = VTCompressionSessionEncodeFrame(self.cEncodeingSession, imageBuffer, ptime, kCMTimeInvalid, NULL, NULL, &flags);
        if (status != noErr) {
            VTCompressionSessionInvalidate(self.cEncodeingSession);
            CFRelease(self.cEncodeingSession);
            self.cEncodeingSession = NULL;
            return;
        } else {
            NSLog(@"encode error status = %d", (int)status);
        }
    });
}

- (void)initVideoToolbox {
    // 用于记录是第几帧数据
    self.frameID = 0;
    // 捕捉视频的宽高
    int width = [UIScreen mainScreen].bounds.size.width;
    int height = [UIScreen mainScreen].bounds.size.height;
    // 创建一个编码器 didCompressH264编码回调函数
    OSStatus status = VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264,
                                                 NULL, NULL, NULL,
                                                 didCompressH264,
                                                 (__bridge void*)self, &_cEncodeingSession);
    
    if (status != 0) {
        NSLog(@"创建编码器失败 status = %d", (int)status);
        return ;
    }
    
    // 设置实施编码输出
    VTSessionSetProperty(self.cEncodeingSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
    VTSessionSetProperty(self.cEncodeingSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
    
    // 设置关键帧(GOPsize)间隔
    int frameInterval = 30;
    CFNumberRef frameIntervalRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &frameInterval);
    VTSessionSetProperty(self.cEncodeingSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, frameIntervalRef);
    
    // 设置期望帧率,不是实际帧率
    int fps = 30;
    CFNumberRef fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
    VTSessionSetProperty(self.cEncodeingSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);
    
    // 设置码率,单位是byte (编码效率, 码率越高,则画面越清晰, 如果码率较低会引起马赛克 --> 码率高有利于还原原始画面,但是也不利于传输)
    int bigRate = width * height * 3 * 4 * 8;
    CFNumberRef bigRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bigRate);
    VTSessionSetProperty(self.cEncodeingSession, kVTCompressionPropertyKey_AverageBitRate, bigRateRef);
    
    int bigRateLimit = width * height * 3 * 4;
    CFNumberRef bigRateLimitRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bigRateLimit);
    VTSessionSetProperty(self.cEncodeingSession, kVTCompressionPropertyKey_DataRateLimits, bigRateLimitRef);
    
    // 开始准备编码
    VTCompressionSessionPrepareToEncodeFrames(self.cEncodeingSession);
}

#pragma mark - 编码回调
// 编码完成回调
void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer)
{
    // CMSampleBufferRef  包括  CMTime(时间戳) + CMVideoGormatDesc(图像存储方式) + CMBlockBuffer(编码后的数据)
    // 获取h264编码的数据 sampleBuffer
    
    NSLog(@"didCompressH264: status = %d  infoFlags = %u", (int)status, (unsigned int)infoFlags);
    // 状态错误
    if (status != 0) {
        return;
    }
    
    // 没准备好
    if (!CMSampleBufferDataIsReady(sampleBuffer)) {
        NSLog(@"didCompressH264 data is not ready");
        return;
    }
    
    // 需要调用oc的方法
    H264Encoder * self = (__bridge H264Encoder*)outputCallbackRefCon;
    
    // 判断当前帧是否为关键帧
    bool keyFrame = !CFDictionaryContainsKey(CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0), kCMSampleAttachmentKey_NotSync);
    if (keyFrame) {
        // sps 序列参数集  pps 图像参数集    h264
        // 获取图像编码后的存储信息
        CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
        // 获取 sps 内容、大小、长度
        size_t spsCount, spsLength;
        const uint8_t *spsSet;
        OSStatus spsStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format,
                                                                                0,
                                                                                &spsSet,
                                                                                &spsLength,
                                                                                &spsCount,
                                                                                0);
        if (spsStatus == noErr) {
            // 获取pps信息
            size_t ppsCount, ppsLength;
            const uint8_t *ppsSet;
            OSStatus ppsStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format,
                                                                                    1,
                                                                                    &ppsSet,
                                                                                    &ppsLength,
                                                                                    &ppsCount,
                                                                                    0);
            if (ppsStatus == noErr) {
                
                // 将sps pps转成 NSData 写入文件
                NSData * spsData = [NSData dataWithBytes:spsSet length:spsLength];
                NSData * ppsData = [NSData dataWithBytes:ppsSet length:ppsLength];
                
                if (self) {
                    [self gotSpsPps:spsData pps:ppsData];
                }
            }
        }
    }
    
    // 获取数据块
    CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    size_t length, totleLength;
    char *dataPointer;
    OSStatus blockStatus = CMBlockBufferGetDataPointer(dataBuffer,
                                                       0,
                                                       &length,
                                                       &totleLength,
                                                       &dataPointer);
    if (blockStatus == noErr) {
        size_t bufferOfSet = 0;
        // 返回的nalu数据前四个字节不是0001的startcode,而是大端模式的帧长度length
        static const int AVCCHeaderLength = 4;
        // 获取nalu数据
        while (bufferOfSet < totleLength - AVCCHeaderLength) {
            UInt32 NALUnitLength = 0;
            // Read the NAL unit length
            memcpy(&NALUnitLength, dataPointer + bufferOfSet, AVCCHeaderLength);
            
            // 大端模式 转换为 系统端模式
            NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
            
            // 获取nalu数据
            NSData * data = [[NSData alloc] initWithBytes:(dataPointer + AVCCHeaderLength + bufferOfSet) length:NALUnitLength];
            // 将 nalu数据 写入文件
            [self gotEncodedData:data isKeyFrame:keyFrame];
            
            // 移动偏移量
            bufferOfSet += AVCCHeaderLength + NALUnitLength;
        }
    }
}


- (void)gotSpsPps:(NSData*)sps pps:(NSData*)pps
{
    NSLog(@"gotSpsPps %lu - %lu", (unsigned long)sps.length, (unsigned long)pps.length);
    const char bytres[] = "\x00\x00\x00\x01";
    size_t length = (sizeof bytres) - 1;
    NSData * byteHeader = [NSData dataWithBytes:bytres length:length];
    
    [self.videoFileHandle writeData:byteHeader];
    [self.videoFileHandle writeData:sps];
    [self.videoFileHandle writeData:byteHeader];
    [self.videoFileHandle writeData:pps];
}

- (void)gotEncodedData:(NSData*)data isKeyFrame:(BOOL)isKeyFrame
{
    NSLog(@"gotEncodedData = %lu", (unsigned long)data.length);
    
    if (self.videoFileHandle != NULL) {
        const char bytres[] = "\x00\x00\x00\x01";
        size_t length = (sizeof bytres) - 1;
        NSData * byteHeader = [NSData dataWithBytes:bytres length:length];
        [self.videoFileHandle writeData:byteHeader];
        [self.videoFileHandle writeData:data];
    }
}

#pragma mark - get
- (dispatch_queue_t)encodeQueue {
    if (!_encodeQueue) {
        _encodeQueue = dispatch_queue_create("encode_video_queue", DISPATCH_QUEUE_SERIAL);
    }
    return _encodeQueue;
}

- (NSFileHandle *)videoFileHandle {
    if (!_videoFileHandle) {
        NSString * filePath = [NSHomeDirectory() stringByAppendingPathComponent:@"/Documents/demo.h264"];
        [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
        BOOL createFile = [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil];
        NSAssert(createFile, @"create video path error");
        _videoFileHandle = [NSFileHandle fileHandleForWritingAtPath:filePath];
    }
    return _videoFileHandle;
}
  • 编码数据查看

在Info.plist中添加Application supports iTunes file sharing = YES,然后再Mac上面通过ITunes获取对应应用下面编码的视频文件即可。
AVFounction学习笔记之--VideoToolbox视频硬编码

相关文章:

  • 2021-06-08
  • 2022-03-02
  • 2022-12-23
  • 2021-07-09
  • 2022-01-22
  • 2022-12-23
  • 2021-12-02
猜你喜欢
  • 2021-07-29
  • 2022-12-23
  • 2022-12-23
  • 2021-08-07
  • 2021-08-18
  • 2021-07-08
  • 2021-10-28
相关资源
相似解决方案