VideoToolbox视频编码——在macOS上对获取到的视频进行编码的问题记录 及YUV422转YUV420

想在macOS平台上写一个将Mac摄像头上获取到的视频数据使用VideoToolBox编码后socket发送出去的服务器,但是遇到了好多问题

先是在接受数据的客户端最终渲染出来的视频中发现绿屏现象,如下图所示:



并且报-12911的错误信息,网上查了一下说是视频数据不完整的原因,

经过验证,初步排除了socket收发数据有误的可能,所以暂时将焦点放在了macOS端的工程上,

将macOS上采集到的视频进行VideoToolBox编码,编码后写入文件,存储成h.264文件,用VLC进行播放时,发现视频整体速度偏快

经过好一番折腾,终于发现有蹊跷的地方:AVCapture输出流代理

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection;
一般情况下,当在iOS环境下,默认情况下,为30 fps,意味着该函数每秒调用30次

但是在macOS端的工程上,每秒的调用并没有达到30次,有时候只有15次,有时候又是20次…

猜想正是因为这样,而编码器以为每秒还是有30帧,所以VLC进行播放时,走了30帧,以为是1秒,但是实际上不止有1秒,因此播放时会有快进的感觉。



在代理中简单打印摄像头输出数据的信息:

    CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    CMVideoFormatDescriptionRef desc = NULL;
    CMVideoFormatDescriptionCreateForImageBuffer(NULL, pixelBuffer, &desc);
    CFDictionaryRef extensions = CMFormatDescriptionGetExtensions(desc);
    NSLog(@"extensions = %@", extensions);
输出:

     extensions = {
     CVBytesPerRow = 1924;
     CVImageBufferColorPrimaries = "ITU_R_709_2";
     CVImageBufferTransferFunction = "ITU_R_709_2";
     CVImageBufferYCbCrMatrix = "ITU_R_709_2";      // ITU_R_709_2是HD视频的方案,一般用于YUV422,YUV至RGB的转换矩阵和SD视频(一般是ITU_R_601_4)并不相同。
     Version = 2;
     }
即便是在把VideoToolBox 设置成kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,输出也同上,

而在iOS真机上输出的是:

    extensions = {
    CVBytesPerRow = 724;
    CVImageBufferChromaLocationTopField = Center;
    CVImageBufferColorPrimaries = "ITU_R_709_2";
    CVImageBufferTransferFunction = "ITU_R_709_2";
    CVImageBufferYCbCrMatrix = "ITU_R_601_4";
    Version = 2;
}

这里暂时只比较CVImageBufferYCbCrMatrix不同,根据查得的资料(链接),可知,ITU_R_709_2是HD视频的方案,一般用于YUV422,说明macOS摄像头摄像头采集到的是YUV422即YUYV格式的视频,

又将AVCaptureDevice的formats打印出来:

        // 获取当前设备支持的像素格式
        NSLog(@"-- videoDevice.formats = %@", videoDevice.formats);
输出

-- videoDevice.formats = (
    "<AVCaptureDeviceFormat: 0x618000000c50> 'vide'/'yuvs' enc dims = 1280x720, pres dims = 1280x720 { 1.00-30.00 fps }",
    "<AVCaptureDeviceFormat: 0x618000000cf0> 'vide'/'2vuy' enc dims = 1280x720, pres dims = 1280x720 { 1.00-30.00 fps }",
    "<AVCaptureDeviceFormat: 0x618000000d20> 'vide'/'420v' enc dims = 1280x720, pres dims = 1280x720 { 1.00-30.00 fps }",
    "<AVCaptureDeviceFormat: 0x618000000d50> 'vide'/'yuvs' enc dims = 640x480, pres dims = 640x480 { 1.00-30.00 fps }",
    "<AVCaptureDeviceFormat: 0x618000000d80> 'vide'/'2vuy' enc dims = 640x480, pres dims = 640x480 { 1.00-30.00 fps }",
    "<AVCaptureDeviceFormat: 0x618000000db0> 'vide'/'420v' enc dims = 640x480, pres dims = 640x480 { 1.00-30.00 fps }",
    "<AVCaptureDeviceFormat: 0x618000000de0> 'vide'/'yuvs' enc dims = 320x240, pres dims = 320x240 { 1.00-30.00 fps }",
    "<AVCaptureDeviceFormat: 0x618000000e10> 'vide'/'2vuy' enc dims = 320x240, pres dims = 320x240 { 1.00-30.00 fps }",
    "<AVCaptureDeviceFormat: 0x618000000e40> 'vide'/'42
(可以看到打印的信息并不完整,不知道是为什么,当打印较长的其他数据如NSData时,也有打印不完整的现象)

由如上的输出信息可知此台Mac的摄像头支持的格式为:yuvs,2vuy,420v,
那么将VideoToolBox的编码类型改成:kCVPixelFormatType_422YpCbCr8_yuvs,或者kCVPixelFormatType_422YpCbCr8

但是当再次编译运行时,实际上AVCapture的输出流代理仍然没有达到每秒调用30次…


刚刚查到可以将YUV422转成YUV420,再进行编码,但是这样依然不能改变AVCapture输出流代理调用的次数啊…

这个方法待验证




========================== 8.12 更新 =============================

(呃今天把电脑换了个位置,Mac摄像头的帧率就达到30次了,我也是醉醉的,之前测试时的环境比较暗,难道是跟环境有关?)


MacBook摄像头参数为: 720p FaceTime HD摄像头



在关于在apple关于图像pixelformat以及YUV等相关知识可以去看看这篇文章:

颜色空间转换







========================== 9.19 更新 =============================

好久没有更新了,第一次在macOS上写程序 遇到了好多棘手的问题,自信都磨没了,最主要不在于有多难,而是资料太难找了有没有,想说巧妇难为无米之炊啊,甚至连FFmpeg编解码的资料都查了,也有想过干脆用Qt好了,网上资料一搜一大把。

后来想想还是不要太为难自己了,毕竟术业有专攻,别啥都看一下 最后啥也不行。

郁闷好久之后还是回到用iOS平台,写了个使用iPhone作为设备采集数据的工具,结果还算顺利。

iOS版的地址在这里:https://github.com/AmoAmoAmo/Smart_Device_Server



言归正传,macOS平台上的,就用了上面提到的,将YUV422转成YUV420,再进行编码。

做法如下:

1. CMSampleBufferRef 中提取yuv数据(Byte)
2. 处理yuv数据
3. yuv数据 转CVPixelBufferRef ,继续进行编码

代码如下:

// ========== 处理YUV422数据 ==========
/*
    1. CMSampleBufferRef 中提取yuv数据(Byte)
    2. 处理yuv数据
    3. yuv数据 转CVPixelBufferRef ,继续进行编码
 */
-(CVPixelBufferRef)processYUV422ToYUV420WithSampleBuffer:(CMSampleBufferRef)sampleBuffer
{
    // 1. 从CMSampleBufferRef中提取yuv数据
    // 获取yuv数据
    // 通过CMSampleBufferGetImageBuffer方法,获得CVImageBufferRef。
    // 这里面就包含了yuv420数据的指针
    CVImageBufferRef pixelBuffer_Before = CMSampleBufferGetImageBuffer(sampleBuffer);
    
    //表示开始操作数据
    CVPixelBufferLockBaseAddress(pixelBuffer_Before, 0);
    
    //图像宽度(像素)
    size_t pixelWidth = CVPixelBufferGetWidth(pixelBuffer_Before);
    //图像高度(像素)
    size_t pixelHeight = CVPixelBufferGetHeight(pixelBuffer_Before);
    //yuv中的y所占字节数
    size_t y_size = pixelWidth * pixelHeight;
    
    
    // 2. yuv中的u和v分别所占的字节数
    size_t uv_size = y_size / 4;
    
    uint8_t *yuv_frame = malloc(uv_size * 2 + y_size);
    
    //获取CVImageBufferRef中的y数据
    uint8_t *y_frame = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer_Before, 0);
    memcpy(yuv_frame, y_frame, y_size);
    
    //获取CMVImageBufferRef中的uv数据
    uint8_t *uv_frame = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer_Before, 1);
    memcpy(yuv_frame + y_size, uv_frame, uv_size * 2);
    
    CVPixelBufferUnlockBaseAddress(pixelBuffer_Before, 0);
    
    NSData *yuvData = [NSData dataWithBytesNoCopy:yuv_frame length:y_size + uv_size * 2];
    
    
    
    
    // 3. yuv 变成 转CVPixelBufferRef

    //现在要把NV12数据放入 CVPixelBufferRef中,因为 硬编码主要调用VTCompressionSessionEncodeFrame函数,此函数不接受yuv数据,但是接受CVPixelBufferRef类型。
    CVPixelBufferRef pixelBuf_After = NULL;
    //初始化pixelBuf,数据类型是kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,此类型数据格式同NV12格式相同。
    CVPixelBufferCreate(NULL,
                        pixelWidth, pixelHeight,
                        kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
                        NULL,
                        &pixelBuf_After);
    
    // Lock address,锁定数据,应该是多线程防止重入操作。
    if(CVPixelBufferLockBaseAddress(pixelBuf_After, 0) != kCVReturnSuccess){
        NSLog(@"encode video lock base address failed");
        return NULL;
    }
    
    //将yuv数据填充到CVPixelBufferRef中
    uint8_t *yuv_frame_2 = (uint8_t *)yuvData.bytes;
    
    //处理y frame
    uint8_t *y_frame_2 = CVPixelBufferGetBaseAddressOfPlane(pixelBuf_After, 0);
    memcpy(y_frame_2, yuv_frame_2, y_size);
    
    uint8_t *uv_frame_2 = CVPixelBufferGetBaseAddressOfPlane(pixelBuf_After, 1);
    memcpy(uv_frame_2, yuv_frame_2 + y_size, uv_size * 2);
    
    
    CVPixelBufferUnlockBaseAddress(pixelBuf_After, 0);
    
    return pixelBuf_After;
}

在代理didOutputSampleBuffer里,直接把获取到的原始数据sampleBuffer传给上面的函数,再把函数返回的CVPixelBufferRef传给VideoToolbox编码。


问题解决是解决了,但毕竟不是用更底层的代码转换的,效率低速度慢是慢了点,最终的效果还不错:




在client端收到视频时明显延时了好久,但是效果是达到了。万里长城也不是一天建成的,以后再慢慢积累吧。


以上源代码地址:https://github.com/AmoAmoAmo/Server_Mac


如果我的代码有帮助到你,请给我颗星星✨,比心。







猜你喜欢

转载自blog.csdn.net/a997013919/article/details/77094979