VideoToolbox 硬编码 h.264

前言

VideoToolboxAppleiOS 8 以后推出的用于视频硬编码、解码的工具库。 平时所说的软编解码是指使用 ffmpeg 这个第三方库去作编码解码。bash

1. 原始裸流 CMSampleBuffer 获取

通常在作音视频应用开发的时候,咱们都是用 AVFoundation 去作原始数据采集的,使用前置摄像头或者后置摄像头采集视频数据,使用麦克风采集音频数据。app

AVCaptureVideoDataOutputSampleBufferDelegate这个代理的async

- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
复制代码

回调方法里面能够获取采集的视频裸流信息。注意AVCaptureAudioDataOutputSampleBufferDelegate音频输出的代理方法也是这个,那么咱们如何区分究竟是音频数据仍是视频数据呢?这里有两种方案:ide

  • 判断 outputAVCaptureAudioDataOutput(音频) 仍是 AVCaptureVideoDataOutput(视频)
  • 判断 connectionaudioConnection(音频) 仍是 videoConnection(视频), 我本身在代码里使用属性声明了 audioConnectionvideoConnection

2. H.264 硬编码

2.1 初始化编码会话

  • 建立编码会话函数

    建立编码会话的时候注意传入了咱们的编码回调函数 VideoEncodeCallback,这个函数会屡次调用。工具

    //建立编码会话
    OSStatus status = VTCompressionSessionCreate(kCFAllocatorDefault, (int32_t)_config.width, (int32_t)_config.height, kCMVideoCodecType_H264, NULL, NULL, NULL, VideoEncodeCallback, (__bridge void * _Nullable)(self), &_encodeSession);
    if (status != noErr) {
        NSLog(@"VTCompressionSession create failed. status=%d", (int)status);
        return self;
    } else {
        NSLog(@"VTCompressionSession create success");
    }
    复制代码
  • 设置编码会话属性学习

    //设置实时编码
    VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
    //指定编码比特流的配置文件和级别。直播通常使用baseline,可减小因为b帧带来的延时
    VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel); 
    
    //设置码率均值(比特率能够高于此。默认比特率为零,表示视频编码器。应该肯定压缩数据的大小。注意,比特率设置只在定时时有效)
    CFNumberRef bit = (__bridge CFNumberRef)@(_config.bitrate);
    VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_AverageBitRate, bit); 
    
    //设置码率上限
    CFArrayRef limits = (__bridge CFArrayRef)@[@(_config.bitrate / 4), @(_config.bitrate * 4)];
    VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_DataRateLimits,limits);
    
    //设置关键帧间隔(GOPSize)GOP太大图像会模糊,越小图像质量越高,固然数据量也会随之变大
    CFNumberRef maxKeyFrameInterval = (__bridge CFNumberRef)@(_config.fps * 2);
    VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, maxKeyFrameInterval); 
    
    //设置fps(预期)
    CFNumberRef expectedFrameRate = (__bridge CFNumberRef)@(_config.fps);
    VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_ExpectedFrameRate, expectedFrameRate); 
    复制代码
  • 准备编码ui

    OSStatus status = VTCompressionSessionPrepareToEncodeFrames(_encodeSession);
    复制代码

2.2 拿到 CMSampleBuffer 开始编码

  1. 先从原始裸流 CMSampleBuffer 获取原始图像信息 CVImageBuffer
  2. 生成 PTS
    • PTS:Presentation Time Stamp,PTS 主要用于度量解码后的视频帧何时被显示出来
    • DTS:Decode Time Stamp,DTS 主要是标识读入内存中的比特流在何时开始送入解码器中进行解码
  3. 设置持续时间 durationkCMTimeInvalid,表示会一直进行解码
  4. 声明 VTEncodeInfoFlags 来记录编码信息
  5. 调用 VTCompressionSessionEncodeFrame() 开始解码
- (void)encodeVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer {
    CFRetain(sampleBuffer);
    dispatch_async(_encodeQueue, ^{
        // 帧数据
        CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
        // 该帧的显示时间戳 (PTS: 用于视频显示的时间戳)
        CMTime presentationTimeStamp = CMTimeMake(frameID++, 1000);
        //持续时间
        CMTime duration = kCMTimeInvalid;
        //编码
        VTEncodeInfoFlags flags;
        OSStatus status = VTCompressionSessionEncodeFrame(self.encodeSesion, imageBuffer, presentationTimeStamp, duration, NULL, NULL, &flags);
        if (status != noErr) {
            NSLog(@"VTCompression: encode failed: status=%d",(int)status);
        }
        CFRelease(sampleBuffer);
    });
}
复制代码

这里的 frameID 是咱们声明的图像帧的递增序标识,每次编码自增就能够了。编码

2.3 编码

  1. 容错处理,先判断当前状态是否正常,数据是否准备好
  2. 判断是否为关键帧 keyFrame
  3. 若是是关键帧,先获取图像源格式 CMFormatDescriptionRef, 处理 SPS 数据和 PPS 数据
    • 先处理 SPS 数据
    • 再处理 PPS 数据
    • 将获取的 SPS 数据和 PPS 数据写入 h.264 文件或者交给解码器去处理
  4. 若是不是关键帧,处理正常的 NALU 数据
    • 先从 CMSampleBuffer 获取 CMBlockBufferRef 数据
    • 读取数据内容,记录数据长度和总长度
    • 定义一个数据偏移量 bufferOffset, 而后 while 循环读取 NALU 数据,注意手动添加起始码 "\x00\x00\x00\x01"
    • 将获取的 NALU 数据写入 h.264 文件或者交给解码器去处理
// 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);
    
    //判断是否为关键帧
    CFArrayRef attachArray = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true);
    BOOL keyFrame = !CFDictionaryContainsKey(CFArrayGetValueAtIndex(attachArray, 0), kCMSampleAttachmentKey_NotSync);//(注意取反符号)
    
    //获取 sps & pps 数据 ,只需获取一次,保存在h264文件开头便可
    if (keyFrame) {
        //获取图像源格式
        CMFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer);
        // 声明 sps 数据大小, 个数 以及 数据内容, 先获取 sps 数据
        size_t spsSize, spsCount;
        const uint8_t *spsData;
        OSStatus spsStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDesc, 0, &spsData, &spsSize, &spsCount, 0);
        if (spsStatus == noErr) {
            // 声明 pps 数据大小, 个数 以及 数据内容, 后获取 pps 数据
            size_t ppsSize, ppsCount;
            const uint8_t *ppsData;
            OSStatus ppsStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDesc, 1, &ppsData, &ppsSize, &ppsCount, 0);
            if (ppsStatus == noErr) {
                NSLog(@"VideoEncodeCallback:got both sps and pps successfully");
                encoder->hasSpsPps = true;
                //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 pps failed ppsStatus=%d", (int)ppsStatus);
            }
        } else {
            NSLog(@"VideoEncodeCallback: get sps failed spsStatus=%d", (int)spsStatus);
        }
    }
    
    //获取NALU数据
    size_t lengthAtOffset, totalLength;
    char *dataPoint;
    
    // 从 CMSampleBuffer 获取 DataBuffer, 将数据复制到 dataPoint
    CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    OSStatus dataBufferStatus = CMBlockBufferGetDataPointer(blockBuffer, 0, &lengthAtOffset, &totalLength, &dataPoint);
    if (dataBufferStatus == noErr) {
        // 循环获取 nalu 数据
        size_t bufferOffset = 0;
        // 这个常量 4 是大端模式的帧长度length, 而不是 nalu 数据前四个字节(0001 的 startcode)
        static const int headerLength = 4;
        
        while (bufferOffset < totalLength - headerLength) {
            uint32_t nalUnitLength = 0;
            // 读取 nalu 长度的数据
            memcpy(&nalUnitLength, dataPoint + bufferOffset, headerLength);
            // 大端转系统端(iOS 的系统端是小端模式)
            nalUnitLength = CFSwapInt32BigToHost(nalUnitLength);
            // 获取到编码好的视频数据
            NSMutableData *data = [NSMutableData dataWithCapacity:4 + nalUnitLength];
            // 先添加 startcode
            [data appendBytes:startCode length:4];
            // 再拼接 nalu 数据
            [data appendBytes:dataPoint + bufferOffset + headerLength length:nalUnitLength];
            
            //将NALU数据回调到代理中
            dispatch_async(encoder.callbackQueue, ^{
                [encoder.delegate videoEncodeCallback:data];
            });
            
            // 移动偏移量,继续读取下一个 NALU 数据
            bufferOffset += headerLength + nalUnitLength;
        }
    } else {
        NSLog(@"VideoEncodeCallback: get datapoint failed, status = %d", (int)dataBufferStatus);
    }
}
复制代码

2.4 释放资源

编码结束了之后,在合理的地方要释放资源spa

- (void)releaseEncodeSession
{
    if (_encodeSession) {
        VTCompressionSessionCompleteFrames(_encodeSession, kCMTimeInvalid);
        VTCompressionSessionInvalidate(_encodeSession);
        
        CFRelease(_encodeSession);
        _encodeSession = NULL;
    }
}
复制代码

备注

此文章是本身学习音视频的笔记记录,也参考了网上不少的资料和文章,在这里推荐一下:

落影loyinglin

CC老师_HelloCoder

小东邪

相关文章
相关标签/搜索