Cocoa - detect event when camera started recording

2019-04-02 22:56发布

In my OSX application I'm using code below to show preview from camera.

  [[self session] beginConfiguration];

  NSError *error = nil;
  AVCaptureDeviceInput *newVideoDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:captureDevice error:&error];

  if (captureDevice != nil) {
    [[self session] removeInput: [self videoDeviceInput]];
    if([[self session] canAddInput: newVideoDeviceInput]) {
      [[self session] addInput:newVideoDeviceInput];
      [self setVideoDeviceInput:newVideoDeviceInput];
    } else {
      DLog(@"WTF?");
    }
  }

  [[self session] commitConfiguration];

Yet, I need to detect the exact time when the preview from the camera becomes available.

In other words I'm trying to detect the same moment like in Facetime under OSX, where animation starts once the camera provides the preview.

What is the best way to achieve this?

2条回答
干净又极端
2楼-- · 2019-04-02 23:34

I know this question is really old, but I stumbled upon it too when I was looking for this same question, and I have found answers so here goes.

For starters, AVFoundation is too high level, you'll need to drop down to a lower level, CoreMediaIO. There's not a lot of documentation on this, but basically you need to perform a couple queries.

To do this, we'll use a combination of calls. First, CMIOObjectGetPropertyDataSize lets us get the size of the data we'll query for next, which we can then use when we call CMIOObjectGetPropertyData. To set up the get property data size call, we need to start at the top, using this property address:

var opa = CMIOObjectPropertyAddress(
    mSelector: CMIOObjectPropertySelector(kCMIOHardwarePropertyDevices),
    mScope: CMIOObjectPropertyScope(kCMIOObjectPropertyScopeGlobal),
    mElement: CMIOObjectPropertyElement(kCMIOObjectPropertyElementMaster)
)

Next, we'll set up some variables to keep the data we'll need:

var (dataSize, dataUsed) = (UInt32(0), UInt32(0))
var result = CMIOObjectGetPropertyDataSize(CMIOObjectID(kCMIOObjectSystemObject), &opa, 0, nil, &dataSize)
var devices: UnsafeMutableRawPointer? = nil

From this point on, we'll need to wait until we get some data out, so let's busy loop:

repeat {
    if devices != nil {
        free(devices)
        devices = nil
    }
    devices = malloc(Int(dataSize))
    result = CMIOObjectGetPropertyData(CMIOObjectID(kCMIOObjectSystemObject), &opa, 0, nil, dataSize, &dataUsed, devices);
} while result == OSStatus(kCMIOHardwareBadPropertySizeError)

Once we get past this point in our execution, devices will point to potentially many devices. We need to loop through them, somewhat like this:

if let devices = devices {
    for offset in stride(from: 0, to: dataSize, by: MemoryLayout<CMIOObjectID>.size) {
        let current = devices.advanced(by: Int(offset)).assumingMemoryBound(to: CMIOObjectID.self)
        // current.pointee is your object ID you will want to keep track of somehow
    }
}

Finally, clean up devices

free(devices)

Now at this point, you'll want to use that object ID you saved above to make another query. We need a new property address:

var CMIOObjectPropertyAddress(
    mSelector: CMIOObjectPropertySelector(kCMIODevicePropertyDeviceIsRunningSomewhere),
    mScope: CMIOObjectPropertyScope(kCMIOObjectPropertyScopeWildcard),
    mElement: CMIOObjectPropertyElement(kCMIOObjectPropertyElementWildcard)
)

This tells CoreMediaIO that we want to know if the device is currently running somewhere (read: in any app), wildcarding the rest of the fields. Next we get to the meat of the query, camera below corresponds to the ID you saved before:

var (dataSize, dataUsed) = (UInt32(0), UInt32(0))
var result = CMIOObjectGetPropertyDataSize(camera, &opa, 0, nil, &dataSize)
if result == OSStatus(kCMIOHardwareNoError) {
    if let data = malloc(Int(dataSize)) {
        result = CMIOObjectGetPropertyData(camera, &opa, 0, nil, dataSize, &dataUsed, data)
        let on = data.assumingMemoryBound(to: UInt8.self)
        // on.pointee != 0 means that it's in use somewhere, 0 means not in use anywhere
    }
}

With the above code samples you should have enough to test whether or not the camera is in use. You only need to get the device once (the first part of the answer); the check for if it's in use however, you'll have to do at any time you want this information. As an extra exercise, consider playing with CMIOObjectAddPropertyListenerBlock to be notified on event changes for the in use property address we used above.

While this answer is nearly 3 years too late for the OP, I hope it helps someone in the future. Examples here are given with Swift 3.0.

查看更多
萌系小妹纸
3楼-- · 2019-04-02 23:38

The previous answer from the user jer is definitely the correct answer, but I just wanted to add one additional important information.

If a listener block is registered with CMIOObjectAddPropertyListenerBlock, the current run loop must be run, otherwise no event will be received and the listener block will never fire.

查看更多
登录 后发表回答