音视频编辑

iOS视频 硬编码代码

2018-07-29  本文已影响333人  mark666

为什么视频可以压缩编码?

压缩编码的标准

ios8.0 之后 使用VideoToolBox框架
流程:

视频采集

视频硬件编码

- (void)prepareEncodeWithWidth:(int)width height:(int)height{
    //0 定义帧的下标值
    frameIndex = 0;
    //1.创建VTCompressionSessionRef 对象
    //1.创建VTCompressionSessionRef 对象
    // 参数一: CoreFoundation 创建对象的方式 ,NULL -> Default
    // 参数二:编码的视频宽度
    // 参数三: 编码的视频高度
    // 参数四: 编码的标准 H.264/ H.265
    // 参数五 ~ 参数七 NULL
    // 参数八: 编码成功一帧数据后的函数回调
    // 参数九: 回调函数的第一个参数
    //  VTCompressionSessionRef session;
    VTCompressionSessionCreate(kCFAllocatorDefault, 
      width, height, kCMVideoCodecType_H264, 
      NULL, NULL, NULL, 
      compressionCallback, (__bridge void * _Nullable)(self),
             &_session);

    //2.设置VTCompressionSessionRef 属性
   // 2.1 如果是直播,需要设置视频编码是实时输出
    VTSessionSetProperty(self.session, kVTCompressionPropertyKey_RealTime, (__bridge CFTypeRef _Nullable)(@YES));
    // 2.2 设置帧率 (16/24/30)
    // 帧/s
    VTSessionSetProperty(self.session, kVTCompressionPropertyKey_ExpectedFrameRate, (__bridge CFTypeRef _Nullable)(@30));
    //2.3 设置比特率 (码率) bit/s  单位时间的数据量
    VTSessionSetProperty(self.session, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFTypeRef _Nullable)(@(1500000))); // bit
    CFArrayRef dataLimits = (__bridge CFArrayRef)(@[@(1500000/8),@1]); //byte
    VTSessionSetProperty(self.session, kVTCompressionPropertyKey_DataRateLimits, dataLimits);
    // 2.4 设置GOP的大小
    VTSessionSetProperty(self.session, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef _Nullable)(@(20)));
    //3.准备开始编码
    VTCompressionSessionPrepareToEncodeFrames(self.session);
}

总结一下常用设置属性:

- (void)encodeFrame:(CMSampleBufferRef)sampleBuffer{
      //1.从CMSampleBufferRef 中获取 CVImageBufferRef
    CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    
    //利用 VTCompressionSessionRef 编码 CMSampleBufferRef
    //pts(presentationTimeStamp):展示时间戳,用来解码时,计算每一帧时间的
    //dts(DecodeTimeStamp): 解码时间戳,决定该帧在什么时间展示
    frameIndex ++;
    // 第几帧 帧率
    CMTime pts = CMTimeMake(frameIndex, 30);
    
    VTCompressionSessionEncodeFrame(self.session, 
     imageBuffer, pts, kCMTimeInvalid, NULL, NULL, NULL);
}
编码前后CMSampleBuffer区别

CMSampleBuffer = CMTime(时间戳) +CMVideoFormatDesc(图片存储方式) + CMBlockBuffer(编码后的数据)

void compressionCallback(void * CM_NULLABLE outputCallbackRefCon,
              void * CM_NULLABLE sourceFrameRefCon,
              OSStatus status,
              VTEncodeInfoFlags infoFlags,
              CM_NULLABLE CMSampleBufferRef sampleBuffer){
    // 0 获取到当前对象
    H264Encoder *encoder = (__bridge H264Encoder *)(outputCallbackRefCon);
    
    // 1.CMSampleBufferRef
    // 2.判断该帧是否是关键帧
    CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, YES);
    CFDictionaryRef dict = CFArrayGetValueAtIndex(attachments, 0);
    BOOL iskeyFrame = !CFDictionaryContainsKey(dict, kCMSampleAttachmentKey_NotSync);
    // 3. 如果是关键帧,那么将关键帧写入文件之前,先写入 PPS / SPS数据
    if (iskeyFrame) {
       //3.1 获取参数信息
      CMFormatDescriptionRef format =   CMSampleBufferGetFormatDescription(sampleBuffer);
      //3.2 从format 中获取sps信息
        //
        //参数二 : sps 0 pps 1
        //参数三
        const uint8_t *spsPointer;
        size_t spsSize,spsCount;
        
        CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &spsPointer, &spsSize, &spsCount, NULL);
      //3.3 从format 中获取pps信息
        const uint8_t *ppsPointer;
        size_t ppsSize,ppsCount;
        CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &ppsPointer, &ppsSize, &ppsCount, NULL);
       // 3.4 将sps/pps 写入 NAL单元
        NSData *spsData = [NSData dataWithBytes:spsPointer length:spsSize];
        NSData *ppsData = [NSData dataWithBytes:ppsPointer length:ppsSize];
        [encoder writeData:spsData];
        [encoder writeData:ppsData];
    }
    // 4.将编码后的数据写入文件
    // 4.1 获取CMSampleBufferRef
    CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    // 4.2 CMSampleBufferRef获取内存地址/长度
    size_t totalLength;
    char *dataPointer;
    CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &totalLength, &dataPointer);
    // 4.3 从dataPointer开始读取数据,并且写入NALU -> slice
    static const int h264HeaderLength = 4;
    size_t offsetLength = 0;
    // 4.4 通过循环,不断的读取slice的切片数据,并且封装成NALU 写入文件
    while (offsetLength < totalLength - h264HeaderLength) {
         // 4.5 读取slice的长度
        uint32_t naluLength;
        memcpy(&naluLength, dataPointer+offsetLength, h264HeaderLength);
        // 4.6 H264 大端字节序/ 小端字节序
        naluLength = CFSwapInt32BigToHost(naluLength);
        // 4.7 根据长度读取字节,并转成NSData
        NSData *data = [NSData dataWithBytes:dataPointer+offsetLength+h264HeaderLength length:naluLength];
        //4.8 写入文件
        [encoder writeData:data];
        //4.9 设置offsetLength
        offsetLength += naluLength + h264HeaderLength;
    }
}

需要注意的一点是,编码后的数据需要通过切片的方式读取数据,h264已经提供好了切片后的数据,并且默认使用4个字节提供每一个切片的数据的长度,在写入文件时候是不能包括这个4字节长度的。

- (void)writeData:(NSData *)data{
   // NALU 的形式写入
   // NALU 头  0x 表示 16进制的某个数字 x 表示16进制的某个字节
    const char bytes[] = "\x00\x00\x00\x01";
    int headerLength = sizeof(bytes) - 1;
    NSData *headerData = [NSData dataWithBytes:bytes length:headerLength];
    // NALU 体
    [self.fileHandle writeData:headerData];
    [self.fileHandle writeData:data];
}
- (void)endEncoding{
    VTCompressionSessionInvalidate(self.session);
    CFRelease(self.session);
}
上一篇 下一篇

猜你喜欢

热点阅读