VideoToolBox之视频编码

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了VideoToolBox之视频编码相关的知识,希望对你有一定的参考价值。

参考技术A 因为未经过编码(压缩)的视频,具有极大的数据量,不利于存储,传输,实时应用.

在保证视觉效果的前提下尽可能减少视频的数据量.

ITU-T与ISO/IEC是制定视频编码标准的两大组织
ITU-T的标准包括:H.261,H.263,H.264 主要应用于实时视频通信领域,如会议电视.
ISO/IEC:MPEG系列标准,主要应用于视频存储(DVD),广播电视,因特网或无线网上的流媒体等
而两大组织也共同制定了一些标准,H.262标准等同于MPEG-2的视频编码标准,H.264标准则被纳入MPEG-4的第10部分.

H264是新一代的编码标准,以高压缩高质量和支持多种网络的流媒体传输著称.
编码理论依据是:参照一段时间内图像的统计结果表明,在相邻几幅图像画面中,一般有差别的像素只有10%以内的点,亮度差值变化不超过2%,而色度差值的变化只有1%以内。所以对于一段变化不大图像画面,我们可以先编码出一个完整的图像帧X,随后的Y帧就不编码全部图像,只写入与X帧的差别,这样Y帧的大小就只有完整帧的1/10或更小!Y帧之后的Z帧如果变化不大,我们可以继续以参考Y的方式编码Z帧,这样循环下去。这段图像我们称为一个GOP图像组(GOP图像组就是有相同特点的一段数据),也就是对这个图像生成一个完整帧X1,随后的图像就参考X1生成,只写入与X1的差别内容。当某个图像与之前的图像变化很大,无法参考前面的帧来生成,那我们就结束上一个GOP图像组,开始下一段GOP图像组。
在H264协议里定义了三种帧,完整编码的帧叫I帧,参考之前的I帧生成的只包含差异部分编码的帧叫P帧,还有一种参考前后的帧编码的帧叫B帧。

H264采用的核心算法是帧内压缩和帧间压缩,帧内压缩是生成I帧的算法,帧间压缩是生成B帧和P帧的算法。

GOP(Group of Pictures):(画面组,一个GOP就是一组连续的画面,每个画面都是一帧,一个GOP就是很多帧的集合,两个I帧之间的间隔,在H264中图像以GOP图像组为单位进行组织,一个GOP图像组是一段图像编码后的数据流,以I帧开始,到下一个I帧结束。

一个序列的第一个图像叫做 IDR 图像(立即刷新图像),IDR 图像都是 I 帧图像。H.264 引入 IDR 图像是为了解码的重同步,当解码器解码到 IDR 图像时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。这样,如果前一个序列出现重大错误,在这里可以获得重新同步的机会。IDR图像之后的图像永远不会使用IDR之前的图像的数据来解码。

一个序列就是一段内容差异不太大的图像编码后生成的一串数据流。当运动变化比较少时,一个序列可以很长,因为运动变化少就代表图像画面的内容变动很小,所以就可以编一个I帧,然后一直P帧、B帧了。当运动变化多时,可能一个序列就比较短了,比如就包含一个I帧和3、4个P帧。

I帧:

帧内编码帧,I帧表示关键帧,你可以理解为这一帧画面的完整保留;解码时只需要本帧数据就可以完成(因为包含完整画面),一个GOP中,I帧是编解码的起点,有效防止帧间预测误差累计扩散。
P帧:(差别帧)保留这一帧跟之前帧的差别,解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面。(P帧没有完整画面数据,只有与前一帧的画面差别的数据)
B帧:(双向差别帧)保留的是本帧与前后帧的差别,解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面的与本帧数据的叠加取得最终的画面。B帧压缩率高,但是解码时CPU会比较累.

I帧特点:

1.它是一个全帧压缩编码帧。它将全帧图像信息进行H.26L压缩编码及传输(效果全面超越JPEG,逼近甚至超过JPEG2000);

2.解码时仅用I帧的数据就可重构完整图像;

3.I帧描述了图像背景和运动主体的详情;

4.I帧不需要参考其他画面而生成;

5.I帧是P帧和B帧的参考帧(其质量直接影响到同组中以后各帧的质量);

6.I帧是帧组GOP的基础帧(第一帧),在一组中只有一个I帧;

7.I帧不需要考虑运动矢量;

8.I帧所占数据的信息量比较大

前向预测编码帧,使用视频序列一个时间方向上的相关性进行压缩。P帧表示的是这一帧跟之前的帧的差别,P帧可以作为后续图像编码时的参考帧。解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面。(也就是差别帧,P帧没有完整画面数据,只有与前一帧的画面差别的数据,因此解码要使用参考图像的像素值)。

P帧是以I帧为参考帧,在I帧中找出P帧“某点”的预测值和运动矢量,取预测差值和运动矢量一起传送。在接收端根据运动矢量从I帧中找出P帧“某点”的预测值并与差值相加以得到P帧“某点”样值,从而可得到完整的P帧。

P帧特点:

1.P帧是I帧后面相隔1~2帧的编码帧;

2.P帧采用运动补偿的方法传送它与前面的I或P帧的差值及运动矢量(预测误差);

3.解码时必须将I帧中的预测值与预测误差求和后才能重构完整的P帧图像;

4.P帧属于前向预测的帧间编码。它只参考前面最靠近它的I帧或P帧;

5.P帧可以是其后面P帧的参考帧,也可以是其前后的B帧的参考帧;

6.由于P帧是参考帧,它可能造成解码错误的扩散;

7.由于是差值传送,P帧的压缩比较高。

双向预测内插编码帧,利用视频序列两个时间方向上的相关性进行压缩,由于B帧的编解码顺序打乱了视频图像的自然顺序,因此B帧不用作参考帧。B帧是双向差别帧,也就是B帧记录的是本帧与前后帧的差别,换言之,要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面的与本帧数据的叠加取得最终的画面。B帧压缩率高,但是解码时CPU会比较累。

B帧的预测与重构

B帧以前面的I或P帧和后面的P帧为参考帧,“找出”B帧“某点”的预测值和两个运动矢量,并取预测差值和运动矢量传送。接收端根据运动矢量在两个参考帧中“找出(算出)”预测值并与差值求和,得到B帧“某点”样值,从而可得到完整的B帧。

B帧特点

1.B帧是由前面的I或P帧和后面的P帧来进行预测的;

2.B帧传送的是它与前面的I或P帧和后面的P帧之间的预测误差及运动矢量;

3.B帧是双向预测编码帧;

4.B帧压缩比最高,因为它只反映丙参考帧间运动主体的变化情况,预测比较准确;

5.B帧不是参考帧,不会造成解码错误的扩散。

SPS:序列参数集 作用于一系列连续的编码图像;
SPS 对于H264而言,就是编码后的第一帧,如果是读取的H264文件,就是第一个帧界定符和第二个帧界定符之间的数据的长度是4
PPS:图像参数集 作用于编码视频序列中一个或多个独立的图像;

PPS 就是编码后的第二帧,如果是读取的H264文件,就是第二帧界定符和第三帧界定符中间的数据长度不固定。

VideoToolbox是ios8以后开放的硬编码与硬解码的API,一组用C语言写的函数.VTCCompressionSessionRef是针对一连串的视频帧进行压缩,CF层的一个引用计数对象.
步骤:

关于视频捕捉主要使用AVFoundatio框架中的AVCaptureSession.不再叙述

实时编码输出:kVTCompressionPropertyKey_RealTime
码率:图片进行压缩后每秒显示的数据量。
帧率:每秒显示的图片数。影响画面流畅度,与画面流畅度成正比:帧率越大,画面越流畅;帧率越小,画面越有跳动感。
由于人类眼睛的特殊生理结构,如果所看画面之帧率高于16的时候,就会认为是连贯的,此现象称之为视觉暂留。并且当帧速达到一定数值后,再增长的话,人眼也不容易察觉到有明显的流畅度提升了。
分辨率:(矩形)图片的长度和宽度,即图片的尺寸
压缩前的每秒数据量:帧率X分辨率(单位应该是若干个字节)
压缩比:压缩前的每秒数据量/码率 (对于同一个视频源并采用同一种视频编码算法,则:压缩比越高,画面质量越差。)

准备编码

使用AVFoundation执行输出的代理方法,得到数据,一般是CMSampleBufferRef类型,编码前后的数据分别为:

而编码完成会执行创建的时候传入的回调函数.

写入SPS,PPS

写入主数据

调用编码完成函数,将编码会话销毁,释放资源

参考: http://www.voidcn.com/blog/onion2007/article/p-4428513.html

iOS-VideoToolbox硬编码H264

前言

VideoToolBox是iOS8之后,苹果开发的用于硬解码编码H264/H265(iOS11以后支持)的API。

对于H264还不了解的童鞋一定要先看下这边的H264的简介。

编码流程

我们实现一个简单的Demo,从摄像头获取到视频数据,然后再编码成H264裸数据保存在沙盒中。

1. 创建初始化VideoToolBox

 

技术图片

 

 

 

核心代码如下

- (void)initVideoToolBox 
    dispatch_sync(encodeQueue  , ^
        frameNO = 0;
        int width = 480, height = 640;
        OSStatus status = VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, didCompressH264, (__bridge void *)(self),  &encodingSession);
        NSLog(@"H264: VTCompressionSessionCreate %d", (int)status);
        if (status != 0)
        
            NSLog(@"H264: Unable to create a H264 session");
            return ;
        
        
        // 设置实时编码输出(避免延迟)
        VTSessionSetProperty(encodingSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
        VTSessionSetProperty(encodingSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
        
        // 设置关键帧(GOPsize)间隔
        int frameInterval = 24;
        CFNumberRef  frameIntervalRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &frameInterval);
        VTSessionSetProperty(encodingSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, frameIntervalRef);
        
        //设置期望帧率
        int fps = 24;
        CFNumberRef  fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
        VTSessionSetProperty(encodingSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);
        
        
        //设置码率,均值,单位是byte
        int bitRate = width * height * 3 * 4 * 8;
        CFNumberRef bitRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRate);
        VTSessionSetProperty(encodingSession, kVTCompressionPropertyKey_AverageBitRate, bitRateRef);
        
        //设置码率,上限,单位是bps
        int bitRateLimit = width * height * 3 * 4;
        CFNumberRef bitRateLimitRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRateLimit);
        VTSessionSetProperty(encodingSession, kVTCompressionPropertyKey_DataRateLimits, bitRateLimitRef);
        
        //开始编码
        VTCompressionSessionPrepareToEncodeFrames(encodingSession);
    );


初始化这里设置了编码类型kCMVideoCodecType_H264,
分辨率640 * 480,fps,GOP,码率。

2. 从摄像头获取视频数据丢给VideoToolBox编码成H264

 


 

技术图片

 

 

 

初始化视频采集端核心代码如下

//初始化摄像头采集端
- (void)initCapture
    
    self.captureSession = [[AVCaptureSession alloc]init];
    
    //设置录制640 * 480
    self.captureSession.sessionPreset = AVCaptureSessionPreset640x480;
    
    AVCaptureDevice *inputCamera = [self cameraWithPostion:AVCaptureDevicePositionBack];
  
    self.captureDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:inputCamera error:nil];
    
    if ([self.captureSession canAddInput:self.captureDeviceInput]) 
        [self.captureSession addInput:self.captureDeviceInput];
    
    
    self.captureDeviceOutput = [[AVCaptureVideoDataOutput alloc] init];
    [self.captureDeviceOutput setAlwaysDiscardsLateVideoFrames:NO];
    
    //设置YUV420p输出
    [self.captureDeviceOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange] forKey:(id)kCVPixelBufferPixelFormatTypeKey]];
    
    [self.captureDeviceOutput setSampleBufferDelegate:self queue:captureQueue];
    
    if ([self.captureSession canAddOutput:self.captureDeviceOutput]) 
        [self.captureSession addOutput:self.captureDeviceOutput];
    
    
    //建立连接
    AVCaptureConnection *connection = [self.captureDeviceOutput connectionWithMediaType:AVMediaTypeVideo];
    [connection setVideoOrientation:AVCaptureVideoOrientationPortrait];

这里需要注意设置的视频分辨率和编码器一致640 * 480. AVCaptureVideoDataOutput类型选用YUV420p。

摄像头数据回调部分

- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
    dispatch_sync(encodeQueue, ^
        [self encode:sampleBuffer];
    );


//编码sampleBuffer
- (void) encode:(CMSampleBufferRef )sampleBuffer

    CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
    // 帧时间,如果不设置会导致时间轴过长。
    CMTime presentationTimeStamp = CMTimeMake(frameNO++, 1000);
    VTEncodeInfoFlags flags;
    OSStatus statusCode = VTCompressionSessionEncodeFrame(encodingSession,
                                                          imageBuffer,
                                                          presentationTimeStamp,
                                                          kCMTimeInvalid,
                                                          NULL, NULL, &flags);
    if (statusCode != noErr) 
        NSLog(@"H264: VTCompressionSessionEncodeFrame failed with %d", (int)statusCode);
        
        VTCompressionSessionInvalidate(encodingSession);
        CFRelease(encodingSession);
        encodingSession = NULL;
        return;
    
    NSLog(@"H264: VTCompressionSessionEncodeFrame Success");

3.框架中出现的数据结构
CMSampleBufferRef
存放一个或者多个压缩或未压缩的媒体数据;
下图列举了两种CMSampleBuffer。

技术图片

 

 

 

CMTime
64位的value,32位的scale,media的时间格式;

 

 

技术图片

 

 

 

CMBlockBuffer
这里可以叫裸数据;

CVPixelBuffer
包含未压缩的像素数据,图像宽度、高度等;

pixelBufferAttributes
CFDictionary包括宽高、像素格式(RGBA、YUV)、使用场景(OpenGL ES、Core Animation)

 

 

技术图片

 

CVPixelBufferPool
CVPixelBuffer的缓冲池,因为CVPixelBuffer的创建和销毁开销很大

 

 

技术图片

 

 

CMVideoFormatDescription
video格式,包括宽高、颜色空间、编码格式信息等;对于H264,还包含sps和pps数据;

 

 

技术图片

 

 

4. 编码完成后的数据写入H264

 

 

技术图片

 

 

这里编码完成我们先判断的是否为I帧,如果是需要读取sps和pps参数集,为什么要这样呢?

我们先看一下一个裸数据H264(Elementary Stream)的NALU构成

 

 

 

技术图片

 

 

H.264裸流中,不存在单独的SPS、PPS包或帧,而是附加在I帧前面,存储的一般形式为

00 00 00 01 SPS 00 00 00 01 PPS 00 00 00 01 I帧

前面的这些00 00数据称为起始码(Start Code),它们不属于SPS、PPS的内容。

SPS(Sequence Parameter Sets)和PPS(Picture Parameter Set):H.264的SPS和PPS包含了初始化H.264解码器所需要的信息参数,包括编码所用的profile,level,图像的宽和高,deblock滤波器等。

上面介绍了sps和pps是封装在CMFormatDescriptionRef中,所以我们得先CMFormatDescriptionRef中取出sps和pps写入h264裸流中。

这就不难理解写入H264的流程了。

代码如下



// 编码完成回调
void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer) 
    NSLog(@"didCompressH264 called with status %d infoFlags %d", (int)status, (int)infoFlags);
    if (status != 0) 
        return;
    
    if (!CMSampleBufferDataIsReady(sampleBuffer)) 
        NSLog(@"didCompressH264 data is not ready ");
        return;
    
    ViewController* encoder = (__bridge ViewController*)outputCallbackRefCon;
    bool keyframe = !CFDictionaryContainsKey( (CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync);
    
    // 判断当前帧是否为关键帧
    // 获取sps & pps数据
    if (keyframe)
    
        CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
        size_t sparameterSetSize, sparameterSetCount;
        const uint8_t *sparameterSet;
        OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0 );
        if (statusCode == noErr)
        
            // 获得了sps,再获取pps
            size_t pparameterSetSize, pparameterSetCount;
            const uint8_t *pparameterSet;
            OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0 );
            if (statusCode == noErr)
            
                // 获取SPS和PPS data
                NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
                NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];
                if (encoder)
                
                    [encoder gotSpsPps:sps pps:pps];
                
            
        
    
    
    CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    size_t length, totalLength;
    char *dataPointer;
    
    //这里获取了数据指针,和NALU的帧总长度,前四个字节里面保存的
    OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, &length, &totalLength, &dataPointer);
    if (statusCodeRet == noErr) 
        size_t bufferOffset = 0;
        static const int AVCCHeaderLength = 4; // 返回的nalu数据前四个字节不是0001的startcode,而是大端模式的帧长度length
        
        // 循环获取nalu数据
        while (bufferOffset < totalLength - AVCCHeaderLength) 
            uint32_t NALUnitLength = 0;
            // 读取NALU长度的数据
            memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength);
            
            // 从大端转系统端
            NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
            
            NSData* data = [[NSData alloc] initWithBytes:(dataPointer + bufferOffset + AVCCHeaderLength) length:NALUnitLength];
            [encoder gotEncodedData:data];
            
            // 移动到下一个NALU单元
            bufferOffset += AVCCHeaderLength + NALUnitLength;
        
    
    


//填充SPS和PPS数据
- (void)gotSpsPps:(NSData*)sps pps:(NSData*)pps

    NSLog(@"gotSpsPps %d %d", (int)[sps length], (int)[pps length]);
    const char bytes[] = "\\x00\\x00\\x00\\x01";
    size_t length = (sizeof bytes) - 1; //string literals have implicit trailing ‘\\0‘
    NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
  //写入startcode
    [self.h264FileHandle writeData:ByteHeader];
    [self.h264FileHandle writeData:sps];
  //写入startcode
    [self.h264FileHandle writeData:ByteHeader];
    [self.h264FileHandle writeData:pps];
    


//填充NALU数据
- (void)gotEncodedData:(NSData*)data

    NSLog(@"gotEncodedData %d", (int)[data length]);
    if (self.h264FileHandle != NULL)
    
        const char bytes[] = "\\x00\\x00\\x00\\x01";
        size_t length = (sizeof bytes) - 1; //string literals have implicit trailing ‘\\0‘
        NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
        //写入startcode
        [self.h264FileHandle writeData:ByteHeader];
        //写入NALU数据
        [self.h264FileHandle writeData:data];
    


结束编码后销毁session

- (void)EndVideoToolBox

    VTCompressionSessionCompleteFrames(encodingSession, kCMTimeInvalid);
    VTCompressionSessionInvalidate(encodingSession);
    CFRelease(encodingSession);
    encodingSession = NULL;

这样就完成了使用VideoToolbox 的H264编码。编码好的H264文件可以从沙盒中取出。

总结

仅仅看流程不看代码肯定是学不会框架的,自己动手编码试试吧!
Demo下载地址:iOS-VideoToolBox-demo

 

 

以上是关于VideoToolBox之视频编码的主要内容,如果未能解决你的问题,请参考以下文章

iOS-VideoToolbox硬编码H264

视频的编解码-编码篇

EasyRTMP+EasyDSS实现一套完整的紧急视频回传直播与存储回放方案之EasyRTMP-iOS的AACEncoder.m文件实现音频的硬编码功能

关于iOS硬解码遇到多slice的视频码流

VideoToolbox框架详细解析(一) —— 基本概览

iOS8系统H264视频硬件编解码说明