使用美乐威MWCapture SDK,利用Mac电脑GPU完成一路4K@60Hz信号的采集、渲染及编码

随着基础设施的更新和扩容,4K信号在各个垂直领域的应用日渐增多和普及。由于4K视频的数据量是高清(1920x1080)信号的4倍,软件开发人员在处理视频的渲染和编码时面临新的挑战。近年来显卡技术的快速发展,使之成为开发人员实现视频编码的首选工具。如果4K@60Hz信号能被主机的GPU处理,这将充分释放CPU的运算负担,提高产品的稳定性,并节省整体成本。本文将重点介绍使用美乐威MWCapture SDK,利用Mac电脑GPU完成一路4K@60Hz信号的采集、渲染及编码。


什么是MWCapture SDK?

MWCapture SDK是美乐威针对I/O系列提供的采集、编码、渲染等功能相关的开发接口和例程,能够帮助开发人员快速优化应用程序和算法。其软件开发库和例程包括用于改进应用程序开发和性能优化的工具,提供了美乐威自定义接口,使您能够将采集设备的特定功能(例如音视频采集、获取输入信号信息和设置信号源等)整合到软件中。您可以直接使用这些应用程序,也可以使用随附的例程来构建自己的应用程序。采集卡的型号、硬件配置、固件版本和驱动程序决定了可用的功能。

根据您所使用的采集设备的兼容性,库函数分为以下几类:


准备软硬件及开发环境

硬件配置:

  1. 采集卡: Pro Capture HDMI 4K Plus(LT)或Pro Capture SDI 4K Plus (如果您的硬件配置足够高,您也可以选择Pro Capture Dual HDMI 4K Plus LT或者Pro Capture Dual SDI 4K Plus实现两路4K的采集。)
  2. Mac电脑:经测试如下配置的电脑可以实现4K@60Hz信号的采集、渲染和编码。

压缩格式为H.264时推荐:

Mac mini(2018)                                          iMac(2019)

CPU:Quad-Core Intel Core i5                      CPU:6-Core Intel Core i3

Memory:2GB                                               Memory:8GB

GPU:Intel UHD Graphics 630 1536MB        GPU:AMD Radeon Pro 570X

压缩格式是H.265时推荐:

iMac Pro(2019)

CPU:Intel Xeon 8 Core Processor

Memory:32GB

GPU:AMD Radeon Vega 56

 

软件配置:

我们建议使用美乐威SDK中的AVCapture例程进行测试。此例程经过美乐威仔细调试,可以充分发挥显卡的性能,实现目标功能。

开发环境:

MacOS:10.11及以以上版本

Xcode:与安装macOS系统相匹配

 

AVCapture 程序结构图


AVCapture例程使用步骤

视频采集:

视频采集部分通过调用美乐威私有采集接口实现,主要步骤如下:

  1. 创建定时器。
  2. 设置定时器触发时间,并等待定时器触发。注意这里的时间,不是主机系统时间,而是采集卡的板载时间,可以通过函数 MWGetDivceTime 函数获取。
  3. 定时器触发后,调用函数 MWCaptureVideoFrameToVirtualAddressEx 采集一帧视频数据。注意,美乐威私有视频采集接口没有采用回调的方式采集视频数据,而是主动调用函数的方式获取,结合使用定时器,可以实现安任意帧率采集的功能。
  4. 函数MWCaptureVideoFrameToVirtualAddressEx 是非阻塞的,调用后立刻返回,之后需调用 MWWaitEvent 函数,等待采集完成,并释放相关资源。

注意:美乐威SDK中提供的采集接口在不同平台上(Windows、Linux、macOS)上是一致的,因此在AVCapture例程中关于视频采集部分的代码可以移植搭配其他平台使用;但本文中的提到的视频渲染和编码接口使用了macOS平台相关接口,无法兼容Windows和Linux平台。

采集一帧视频数据。主要代码片段如下:

while (self.running) {  
        llExpireTime = llExpireTime + dwFrameDuration;  
              
        LONGLONG llCurrentTime = 0LL;  
        xr = MWGetDeviceTime(self.hChannel, &llCurrentTime);  
        if (xr != MW_SUCCEEDED) {  
            llExpireTime = 0LL; usleep(10000);  
            continue;  
        }  
              
        if (llExpireTime < llCurrentTime) {  
            llExpireTime = llCurrentTime;  
        }  
        xr = MWScheduleTimer(self.hChannel, hTimerNotify, llExpireTime);  
        if (xr != MW_SUCCEEDED) {  
            llExpireTime = llCurrentTime;  
            continue;  
        }  
  
        DWORD dwRet = MWWaitEvent(hTimerEvent, 1000);  
        if (dwRet <= 0) {  
            continue;  
        }  
        ........  
  
        if (frame->pixelBuffer) {  
            ........
            xr = MWCaptureVideoFrameToVirtualAddressEx(self.hChannel,   
                                      MWCAP_VIDEO_FRAME_ID_NEWEST_BUFFERED,   
                                      byBuffer,  
                                      dwFrameSize,   
                                      cbStride,   
                                      FALSE,  
                                      (MWCAP_PTR64)pixelBuffer,  
                                      self.fourcc,   
                                      self.width,   
                                      self.height,  
                                      0,   
                                      0,   
                                      NULL,   
                                      NULL,   
                                      0,   
                                      100,   
                                      0,   
                                      100,   
                                      0,   
                                      MWCAP_VIDEO_DEINTERLACE_BLEND,   
                                      MWCAP_VIDEO_ASPECT_RATIO_IGNORE,   
                                      &rcSrc,   
                                      NULL,   
                                      0,   
                                      0,  
                                      MWCAP_VIDEO_COLOR_FORMAT_UNKNOWN,      
                                      MWCAP_VIDEO_QUANTIZATION_UNKNOWN,   
                                      MWCAP_VIDEO_SATURATION_UNKNOWN);  
            MWWaitEvent(hCaptureEvent, -1);  
            CVPixelBufferUnlockBaseAddress(frame->pixelBuffer, 0);  
                  
           ........  
        }  
    }       
   ........  
 } while (FALSE);

 

视频渲染

AVCapture 例程对视频渲染进行了优化。首先将视频数据的指针封装为CMSampleBufferRef类,然后将这个类直接交给渲染器进行渲染。CMSampleBufferRef类只是对原始视频数据指针的引用,因此这个过程中不会有任何的数据拷贝。

if (self.viewEnable) {  
 CMSampleTimingInfo timing = kCMTimeInvalid, kCMTimeInvalid, kCMTimeInvalid};  
 CMVideoFormatDescriptionRef videoInfo = NULL;  
 CVReturn result = CMVideoFormatDescriptionCreateForImageBuffer(NULL, frame->pixelBuffer, &videoInfo);  
  
 CMSampleBufferRef sampleBuffer = NULL;  
 result = CMSampleBufferCreateForImageBuffer(kCFAllocatorDefault, frame->pixelBuffer, true, NULL, NULL, videoInfo, &timing, &sampleBuffer);  
 CFRelease(videoInfo);  
  
 CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, YES);      
 CFMutableDictionaryRef dict = (CFMutableDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);  
 CFDictionarySetValue(dict, kCMSampleAttachmentKey_DisplayImmediately, kCFBooleanTrue);  
  
 if(self.videoLayer) {  
     [self.videoLayer enqueueSampleBuffer:(CMSampleBufferRef)sampleBuffer];  
    }  
          
 CFRelease(sampleBuffer);  
}

 

视频编码

AVCapture针对视频编码同样进行了优化。首先视频编码的运算量较大,重新开启了一个线程,专门用于视频编码。

if (self.audioCaptureThreadId == 0) {  
    pthread_t tid = 0;  
    if (0 == pthread_create(&tid, NULL, onVideoEncodeThreadProc,(__bridge void*)(self))) {  
       self.audioCaptureThreadId = tid;  
    }  
}  

其次,在采集了一帧数据后,会将数据指针放进一个队列。在编码线程中,会从队列中获取数据指针,然后将数据指针送到编码器进行编码。因为数据都是以指针的形式进行传输的,因此传输过程中,也是没有任何的数据拷贝的。

[self.vtEncLock lock];  
if (self.vtEnc) {  
    ((std::queue > *)self.encPixelFrameQueue)->push(frame);  
    while(((std::queue > *)self.encPixelFrameQueue)->size() > MAX_VIDEO_ENCODE_BUFFER_FRAMES) {  
        ((std::queue > *)self.encPixelFrameQueue)->pop();  
    }  
}  
[self.vtEncLock unlock];

 

编码线程核心代码

std::queue > *encQueue = (std::queue > *)self.encPixelFrameQueue;  
      
while (self.encoding) {  
    std::shared_ptr frame;  
          
    [self.vtEncLock lock];  
    if (!encQueue->empty()) {  
        frame = encQueue->front();  
    }  
    [self.vtEncLock unlock];  
          
    if (frame != NULL && frame->pixelBuffer) {  
        if (self.vtEnc) {  
            //printf("put video frame:%lld\n", frame->timestamp);  
            mw_venc_put_imagebuffer(self.vtEnc, frame->pixelBuffer, frame->timestamp);  
        }  
              
        [self.vtEncLock lock];  
        encQueue->pop();  
        [self.vtEncLock unlock];  
              
        ........  
    } else {  
        usleep(5000);  
    }  
} 

综上所述,要保证能够同时预览和录制4K@60Hz的视频,在处理过程中要尽量减少视频数据的拷贝,最好是零拷贝,这样才能将性能提升至最优。