本文翻译自《MacOS, Media Capture using CoreMediaIO》
前言
这篇文章的目标读者是MacOS C++ / Obj-C开发者和设计师,假定本文读者熟悉面向对象编程和设计。
为了简洁明了起见, 本文省略了线程同步方面的内容, 并没有详细讨论。
介绍
使用OC编写的AVFoundation框架封装了媒体处理方法(捕获,编辑,...)。它功能很强大,有很好的文档支持,涵盖了大多数A/V用例,然而,一些偏门用例不被这个框架所支持。例如,当从设备发出的payload已经被调制或压缩时,能够直接访问从设备发出的缓冲区,就显得特别重要。在这种情况下,AVFoundation(具体说是AVCaptureSession)将在用户访问payload之前对其进行解调或解压。要直接访问从设备发送的缓冲区中的任何中间数据, 我们将不得不使用更底层的API, 即CoreMediaIO。
苹果的CoreMediaIO是一个低级C++框架, 用于对音频/视频设备 (如照相机、捕获卡甚至镜像iOS设备会话) 访问和交互。
CoreMediaIO的问题是缺少文档,而且,现有的示例代码是旧的,需要相当多的修改才能用最新的SDK编译它。
在这篇短文中,我将提供一个简单的示例代码,演示使用CoreMediaIO和AVFoundation的捕获和格式解析。
实现
CoreMediaIO API通过“CoreMediaIO.framework”提供,将框架导入工程中,并引入头文件“CoreMediaIO/CMIOHardware.h”。
为了可以开始捕获,我们要做的第一件事是寻找感兴趣的设备,如果我们对屏幕捕获感兴趣(例如,捕获附加的iOS设备的屏幕),我们需要启用CoreMediaIO ‘DAL’插件,以下是代码演示:
void EnableDALDevices()
{
CMIOObjectPropertyAddress prop = {
kCMIOHardwarePropertyAllowScreenCaptureDevices,
kCMIOObjectPropertyScopeGlobal,
kCMIOObjectPropertyElementMaster
};
UInt32 allow = 1;
CMIOObjectSetPropertyData(kCMIOObjectSystemObject,
&prop, 0, NULL,
sizeof(allow), &allow );
}
某些设备在运行时添加或删除, 为了得到设备在运行时添加或删除指示, 可以使用NSNotificationCenter来捕获A/V设备连接的通知, 当前添加/删除的AVCaptureDevice由包含在block中的参数note对象里的object指示。请注意, 除非执行运行循环, 否则不会收到任何通知。以下是代码演示:
NSNotificationCenter *notiCenter = [NSNotificationCenter defaultCenter];
id connObs =[notiCenter addObserverForName:AVCaptureDeviceWasConnectedNotification
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *note)
{
// Device addition logic
}];
id disconnObs =[notiCenter addObserverForName:AVCaptureDeviceWasDisconnectedNotification
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *note)
{
// Device removal logic
}];
[[NSRunLoop mainRunLoop] run];
[notiCenter removeObserver:connObs];
[notiCenter removeObserver:disconnObs];
寻找感兴趣的设备
下一步是枚举连接的捕获设备,可以通过使用AVFoundation中的AVCaptureDevice类或者直接使用CoreMediaIO C++ API,每个捕获设备提供了一个唯一的标识符,可以使用devicesWithMediaType过滤特定设备。
下面的代码演示使用AVFoundation的API寻找感兴趣的设备ID:
// Use the ‘devicesWithMediaType’ to filter devs by media type
// NSArray* devs = [AVCaptureDevice devicesWithMediaType: AVMediaTypeMuxed];
NSArray* devs = [AVCaptureDevice devices];
NSLog(@“devices: %d\n”, (int)[devs count]);
for(AVCaptureDevice* d in devs) {
NSLog(@“uniqueID: %@\n”, [d uniqueID]);
NSLog(@“modelID: %@\n”, [d modelID]);
NSLog(@“description: %@\n”, [d localizedName]);
}
下一步是找到我们要用于捕获的设备,CoreMediaIO的捕获设备由CMIODeviceID标识,下面的代码演示如何根据特定的ID来匹配设备CMIODeviceID,这个ID是由外部提供并且已知的。
OSStatus GetPropertyData(CMIOObjectID objID, int32_t sel, CMIOObjectPropertyScope scope,
UInt32 qualifierDataSize, const void* qualifierData, UInt32 dataSize,
UInt32& dataUsed, void* data) {
CMIOObjectPropertyAddress addr={ (CMIOObjectPropertySelector)sel, scope,
kCMIOObjectPropertyElementMaster };
return CMIOObjectGetPropertyData(objID, &addr, qualifierDataSize, qualifierData,
dataSize, &dataUsed, data);
}
OSStatus GetPropertyData(CMIOObjectID objID, int32_t selector, UInt32 qualifierDataSize,
const void* qualifierData, UInt32 dataSize, UInt32& dataUsed,
void* data) {
return GetPropertyData(objID, selector, 0, qualifierDataSize,
qualifierData, dataSize, dataUsed, data);
}
OSStatus GetPropertyDataSize(CMIOObjectID objID, int32_t sel,
CMIOObjectPropertyScope scope, uint32_t& size) {
CMIOObjectPropertyAddress addr={ (CMIOObjectPropertySelector)sel, scope,
kCMIOObjectPropertyElementMaster };
return CMIOObjectGetPropertyDataSize(objID, &addr, 0, 0, &size);
}
OSStatus GetPropertyDataSize(CMIOObjectID objID, int32_t selector, uint32_t& size) {
return GetPropertyDataSize(objID, selector, 0, size);
}
OSStatus GetNumberDevices(uint32_t& cnt) {
if(0 != GetPropertyDataSize(kCMIOObjectSystemObject, kCMIOHardwarePropertyDevices, cnt))
return -1;
cnt /= sizeof(CMIODeviceID);
return 0;
}
OSStatus GetDevices(uint32_t& cnt, CMIODeviceID* pDevs) {
OSStatus status;
uint32_t numberDevices = 0, used = 0;
if((status = GetNumberDevices(numberDevices)) < 0)
return status;
if(numberDevices > (cnt = numberDevices))
return -1;
uint32_t size = numberDevices * sizeof(CMIODeviceID);
return GetPropertyData(kCMIOObjectSystemObject, kCMIOHardwarePropertyDevices,
0, NULL, size, used, pDevs);
}
template< const int C_Size >
OSStatus GetDeviceStrProp(CMIOObjectID objID, CMIOObjectPropertySelector sel,
char (&pValue)[C_Size]) {
CFStringRef answer = NULL;
UInt32 dataUsed= 0;
OSStatus status = GetPropertyData(objID, sel, 0, NULL, sizeof(answer),
dataUsed, &answer);
if(0 == status)// SUCCESS
CFStringCopyUTF8String(answer, pValue);
return status;
}
template< const int C_Size >
Boolean CFStringCopyUTF8String(CFStringRef aString, char (&pText)[C_Size]) {
CFIndex length = CFStringGetLength(aString);
if(sizeof(pText) < (length + 1))
return false;
CFIndex maxSize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8);
return CFStringGetCString(aString, pText, maxSize, kCFStringEncodingUTF8);
}
实用方法
OSStatus FindDeviceByUniqueId(const char* pUID, CMIODeviceID& devId) {
OSStatus status = 0;
uint32_t numDev = 0;
if(((status = GetNumberDevices(numDev)) < 0) || (0 == numDev))
return status;
// Allocate memory on the stack
CMIODeviceID* pDevs = (CMIODeviceID*)alloca(numDev * sizeof(*pDevs));
if((status = GetDevices(numDev, pDevs)) < 0)
return status;
for(uint32_t i = 0; i < numDev; i++) {
char pUniqueID[64];
if((status = GetDeviceStrProp(pDevs[i], kCMIODevicePropertyDeviceUID, pUniqueID)) < 0)
break;
status = afpObjectNotFound;// Not Found…
if(0 != strcmp(pUID, pUniqueID))
continue;
devId = pDevs[i];
return 0;
}
return status;
}
利用UID进行设备解析
CoreMediaIO 捕获设备公开流, 每个此类流都是数据源, 并使用 CMIOStreamID 类型表示, 一个流可能提供视频payload, 另一个可以提供音频payload, 另一些可能提供多重复用payload, 当捕获时我们必须选择一个流并开始抽取数据, 下面的代码演示如何枚举给定设备的可用流 (由它的 CMIODeviceID 指示) 以及如何解析payload格式。
uint32_t GetNumberInputStreams(CMIODeviceID devID)
{
uint32 size = 0;
GetPropertyDataSize(devID, kCMIODevicePropertyStreams,
kCMIODevicePropertyScopeInput, size);
return size / sizeof(CMIOStreamID);
}
OSStatus GetInputStreams(CMIODeviceID devID, uint32_t&
ioNumberStreams, CMIOStreamID* streamList)
{
ioNumberStreams = std::min(GetNumberInputStreams(devID), ioNumberStreams);
uint32_t size = ioNumberStreams * sizeof(CMIOStreamID);
uint32_t dataUsed = 0;
OSStatus err = GetPropertyData(devID, kCMIODevicePropertyStreams,
kCMIODevicePropertyScopeInput, 0,
NULL, size, dataUsed, streamList);
if(0 != err)
return err;
ioNumberStreams = size / sizeof(CMIOStreamID);
CMIOStreamID* firstItem = &(streamList[0]);
CMIOStreamID* lastItem = firstItem + ioNumberStreams;
std::sort(firstItem, lastItem);
return 0;
}
实用方法
CMIODeviceID devId;
FindDeviceByUniqueId(“4e58df701eb87”, devId);
uint32_t numStreams = GetNumberInputStreams(devId);
CMIOStreamID* pStreams = (CMIOStreamID*)alloca(numStreams * sizeof(CMIOStreamID));
GetInputStreams(devId, numStreams, pStreams);
for(uint32_t i = 0; i < numStreams; i++) {
CMFormatDescriptionRef fmt = 0;
uint32_t used;
GetPropertyData(pStreams[i], kCMIOStreamPropertyFormatDescription,
0, NULL, sizeof(fmt), used, &fmt);
CMMediaType mt = CMFormatDescriptionGetMediaType(fmt);
uint8_t null1 = 0;// ‘mt’ is a 4 char string, we use ‘null1’ so
// it could be printed.
FourCharCode fourcc= CMFormatDescriptionGetMediaSubType(fmt);
uint8_t null2 = 0;// ‘fourcc’ is a 4 char string, we use ‘null1’
// so it could be printed.
printf(“media type: %s\nmedia sub type: %s\n”, (char*)&mt, (char*)&fourcc);
}
流格式解析
下一个也是最后一个阶段是开始从流中抽出数据,这是通过注册一个回调来完成的,CoreMediaIO会在得到payload时调用这个回调,下面的代码片段演示了如何访问原始的payload字节。
CMSimpleQueueRef queueRef = 0;// The queue that will be used to
// process the incoming data
CMIOStreamCopyBufferQueue(strmID, [](CMIOStreamID streamID, void*, void* refCon) {
// The callback ( lambda in out case ) being called by CoreMediaIO
CMSimpleQueueRef queueRef = *(CMSimpleQueueRef*)refCon;
CMSampleBufferRef sb = 0;
while(0 != (sb = (CMSampleBufferRef)CMSimpleQueueDequeue(queueRef))) {
size_t len = 0;// The ‘len’ of our payload
size_t lenTotal = 0;
char* pPayload = 0;// This is where the RAW media
// data will be stored
const CMTime ts = CMSampleBufferGetOutputPresentationTimeStamp(sb);
const double dSecTime = (double)ts.value / (double)ts.timescale;
CMBlockBufferRef bufRef = CMSampleBufferGetDataBuffer(sb);
CMBlockBufferGetDataPointer(bufRef, 0, &len, &lenTotal, &pPayload);
assert(len == lenTotal);
// TBD: Process ‘len’ bytes of ‘pPayload’
}
}, &queueRef, &queueRef);
最后一件要注意的是,在更少数的情况下,直到第一个样本被发送,实际的捕获格式才是可用的,在这种情况下,它应该在第一个样本接收时被解析,下面的代码片段演示如何使用CMSAMPuffBuffRef来解析音频采样格式,同样的,视频和其他媒体类型也可以按照这种方式解析。
bool PrintAudioFormat(CMSampleBufferRef sb)
{
CMFormatDescriptionRef fmt = CMSampleBufferGetFormatDescription(sb);
CMMediaType mt = CMFormatDescriptionGetMediaType(fmt);
if(kCMMediaType_Audio != mt) {
printf(“Not an audio sample\n”);
return false;
}
CMAudioFormatDescriptionRef afmt = (CMAudioFormatDescriptionRef)fmt;
const auto pAud = CMAudioFormatDescriptionGetStreamBasicDescription(afmt);
if(0 == pAud)
return false;
// We are expecting PCM Audio
if(‘lpcm’ != pAud->mFormatID)// ‘pAud->mFormatID’ == fourCC
return false;// Not a supported format
printf(“mChannelsPerFrame: %d\nmSampleRate: %.1f\n”\
“mBytesPerFrame: %d\nmBitsPerChannel: %d\n”,
pAud->mChannelsPerFrame, pAud->mSampleRate,
pAud->mBytesPerFrame, pAud->mBitsPerChannel);
return true;
}
结束语
本文中所提供的只是一个大概的关于CoreMediaIO的可行的用法,更多信息可以参考CoreMediaIO示例。
引用
CoreMediaIO,
AVFoundation,
AVCaptureSession, NSNotificationCenter,
Run Loop,
AVCaptureDevice