×

iOS H264视频编码的注意点

96
AlexBao
2017.04.27 20:58* 字数 649

最近在学习IOS用videotoolbox框架来编码,遇到了一些问题,总结一下。

1.尝试编码纯NV12数据时遇到调用VTCompressionSessionEncodeFrame函数后,回调返回 kVTPixelTransferNotSupportedErr 错误,错误码是-12905.

首先说明一下NV12格式数据的来源是先通过摄像头采集数据,然后将YUV的数据保存到文件里,采集到的数据时NV12格式的,然后将数据直接用videotoolbox来直接编码成h264流。因为编码时接收的是CVPixelBufferRef 格式的数据,所以需要先将YUV数据封装到CVPixelBufferRef格式中,网上找了很多方法,可以通过调用CVPixelBufferCreateWithPlanarBytes来创建,但是编码的时候就是会报-12905这个错。后来在ffmpeg框架里面找到了videotooboxenc.c里面有将AVFrame里面的YUV数据转换成CVPixelBufferRef格式,然后用于编码。代码如下:

CVReturn status = 0;
    CVPixelBufferPoolRef pix_buf_pool =     VTCompressionSessionGetPixelBufferPool(_encodingSession);
    CVPixelBufferRef pix_buffer = nil;
    if (!pix_buf_pool) {
        NSLog(@"create pix_buf_pool error");
    }
    status = CVPixelBufferPoolCreatePixelBuffer(NULL,
                                                pix_buf_pool,
                                                &pix_buffer);

    if (status != kCVReturnSuccess) {
        NSLog(@"CVPixelBufferPoolCreatePixelBuffer error status:%d",status);
    }
    status = CVPixelBufferLockBaseAddress(pix_buffer, 0);
    if (status) {
        NSLog(@"CVPixelBufferLockBaseAddress error");
    }
    if (CVPixelBufferIsPlanar(pix_buffer)) {
        uint8_t *dst_addr = (uint8_t *)CVPixelBufferGetBaseAddressOfPlane(pix_buffer, 0);
        uint8_t *src_addr = data;
        size_t width = CVPixelBufferGetWidth(pix_buffer);
        size_t yBytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(pix_buffer, 0);
        size_t yPlaneHeight = CVPixelBufferGetHeightOfPlane(pix_buffer, 0);
        for (int i=0; i<yPlaneHeight; ++i) {
            memcpy(dst_addr, src_addr, width);
            src_addr += width;
            dst_addr += yBytesPerRow;
        }
        
        dst_addr = (uint8_t *)CVPixelBufferGetBaseAddressOfPlane(pix_buffer, 1);
        size_t uvBytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(pix_buffer, 1);
        size_t uvPlaneHeight = CVPixelBufferGetHeightOfPlane(pix_buffer, 1);
        for (int i=0; i<uvPlaneHeight; ++i) {
            memcpy(dst_addr, src_addr, width);
            dst_addr+= uvBytesPerRow;
            src_addr += width;
        }
    }
    status = CVPixelBufferUnlockBaseAddress(pix_buffer, 0);
    if (pix_buffer) {
        [self pushData:(__bridge NSData *)(pix_buffer) metadata:metadata];
    }
    CFRelease(pix_buffer);

这样就可以了,这里的pix_buffer是通过编码器创建的VTCompressionSessionRef类型的_encodingSession 获取CVPixelBufferPoolRef, 然后通过Pool创建出pix_buffer才能把数据给到编码器。但这样有个问题,就是不能随便变更编码的分辨率。只能以创建编码器时设定的分辨率进行编码,当前的解决方案是,通过ffmpeg将yuv的数据调整到目标分辨率后,进行编码。

2.baseline profile模式下的一些坑

当profile是AVVideoProfileLevelH264BaselineAutoLevel时,如果设置了kVTCompressionPropertyKey_DataRateLimits 并且设置了kVTH264EntropyMode_CABAC熵编码模式,那编出来的视频将非常模糊。可能本h264的baseline本身是不支持CABAC的编码模式的,搞不懂VideotoolBox这个框架这个参数为啥会有影响。

3.high profile模式下B帧的坑

当在high模式下,按API文档来说设置了kVTCompressionPropertyKey_AllowFrameReordering为true,就可以额编码出B帧了,但如果同时设置了kVTCompressionPropertyKey_DataRateLimits那么B帧这个设置就会失效。

4.kVTCompressionPropertyKey_DataRateLimits 这个参数的坑

最近发现kVTCompressionPropertyKey_DataRateLimits这个参数对机型和系统很敏感, 在iPhone6 plus机型+ iOS8.1和iOS8.2系统上如果是在初始化的时候设置,会导致VTCompressionSessionPrepareToEncodeFrames这个方法的调用的失败,导致编码出错。

并且这个参数比较坑,如果初始化的时候不设置这个参数,后面再设置会无效,初始化设置了,后面再修改这个参数,是有效的。

未完待续

个人学习笔记
Web note ad 1