Skip to content

Commit e770673

Browse files
committed
Implement AVFoundation Mode support and hot-plug detection
Implements Device::getModes() to enumerate available capture formats with resolution, frame rate, and pixel format information. Adds mode-based initialization through initWithDevice:mode: for explicit format control. Adds session error notifications and device disconnection detection. The isCapturing() method now checks session state to detect unplugged devices, preventing crashes from attempting to access disconnected hardware.
1 parent f41bc41 commit e770673

File tree

2 files changed

+243
-33
lines changed

2 files changed

+243
-33
lines changed

include/cinder/CaptureImplAvFoundation.h

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
#include "cinder/Capture.h"
2727
#import <AVFoundation/AVFoundation.h>
2828
#include <vector>
29+
#include <memory>
30+
#include <set>
2931

3032
namespace cinder {
3133

@@ -34,11 +36,12 @@ class CaptureImplAvFoundationDevice : public Capture::Device {
3436
CaptureImplAvFoundationDevice( AVCaptureDevice *device );
3537
~CaptureImplAvFoundationDevice();
3638

37-
bool checkAvailable() const;
38-
bool isConnected() const;
39-
Capture::DeviceIdentifier getUniqueId() const { return mUniqueId; }
39+
bool checkAvailable() const override;
40+
bool isConnected() const override;
41+
Capture::DeviceIdentifier getUniqueId() const override { return mUniqueId; }
4042
bool isFrontFacing() const { return mFrontFacing; }
41-
void* getNative() const { return mNativeDevice; }
43+
void* getNative() const override { return mNativeDevice; }
44+
std::vector<Capture::Mode> getModes() const override;
4245
private:
4346
Capture::DeviceIdentifier mUniqueId;
4447
AVCaptureDevice *mNativeDevice;
@@ -61,11 +64,13 @@ class CaptureImplAvFoundationDevice : public Capture::Device {
6164
int32_t mExposedFrameBytesPerRow;
6265
int32_t mExposedFrameHeight;
6366
int32_t mExposedFrameWidth;
67+
std::unique_ptr<cinder::Capture::Mode> mSelectedMode;
6468
}
6569

6670
+ (const std::vector<cinder::Capture::DeviceRef>&)getDevices:(BOOL)forceRefresh;
6771

6872
- (id)initWithDevice:(const cinder::Capture::DeviceRef)device width:(int)width height:(int)height;
73+
- (id)initWithDevice:(const cinder::Capture::DeviceRef)device mode:(const cinder::Capture::Mode&)mode;
6974
- (bool)prepareStartCapture;
7075
- (void)startCapture;
7176
- (void)stopCapture;
@@ -79,4 +84,4 @@ class CaptureImplAvFoundationDevice : public Capture::Device {
7984
- (int32_t)getCurrentFrameWidth;
8085
- (int32_t)getCurrentFrameHeight;
8186

82-
@end
87+
@end

src/cinder/CaptureImplAvFoundation.mm

Lines changed: 233 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@
2424
#import "cinder/CaptureImplAvFoundation.h"
2525
#include "cinder/cocoa/CinderCocoa.h"
2626
#include "cinder/Vector.h"
27+
#include "cinder/Utilities.h"
28+
#include "cinder/Log.h"
2729
#import <AVFoundation/AVFoundation.h>
30+
#include <set>
31+
#include <sstream>
2832

2933
namespace cinder {
3034

@@ -52,6 +56,86 @@
5256
return mNativeDevice.connected;
5357
}
5458

59+
std::vector<Capture::Mode> CaptureImplAvFoundationDevice::getModes() const
60+
{
61+
std::vector<Capture::Mode> modes;
62+
63+
if( !mNativeDevice )
64+
return modes;
65+
66+
// Define practical frame rates we want to expose
67+
std::vector<int> practicalFrameRates = { 120, 60, 30, 24, 15, 10, 5 };
68+
69+
std::set<std::string> seenModes; // To avoid duplicates
70+
71+
for( AVCaptureDeviceFormat *format in [mNativeDevice formats] ) {
72+
CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions( format.formatDescription );
73+
FourCharCode mediaSubType = CMFormatDescriptionGetMediaSubType( format.formatDescription );
74+
75+
// Map FourCharCode to our PixelFormat enum
76+
Capture::Mode::PixelFormat pixelFormat = Capture::Mode::PixelFormat::Unknown;
77+
switch( mediaSubType ) {
78+
case 'BGRA': pixelFormat = Capture::Mode::PixelFormat::BGRA32; break;
79+
case '420v': pixelFormat = Capture::Mode::PixelFormat::NV12; break;
80+
case '420f': pixelFormat = Capture::Mode::PixelFormat::I420; break;
81+
case 'yuvs': pixelFormat = Capture::Mode::PixelFormat::YUY2; break;
82+
case '2vuy': pixelFormat = Capture::Mode::PixelFormat::UYVY; break;
83+
case 'y420': pixelFormat = Capture::Mode::PixelFormat::YUV420P; break;
84+
default:
85+
// Skip unknown formats
86+
continue;
87+
}
88+
89+
// Get the supported frame rate range for this format
90+
AVFrameRateRange *frameRateRange = format.videoSupportedFrameRateRanges.firstObject;
91+
if( !frameRateRange ) continue;
92+
93+
int minFps = (int)round( frameRateRange.minFrameRate );
94+
int maxFps = (int)round( frameRateRange.maxFrameRate );
95+
96+
std::vector<int> framesToCreate;
97+
if( minFps == maxFps ) { // Device has fixed frame rate - just use that
98+
framesToCreate.push_back( minFps );
99+
} else { // Device supports variable frame rates - try practical rates first
100+
for( int targetFps : practicalFrameRates )
101+
if( targetFps >= minFps && targetFps <= maxFps )
102+
framesToCreate.push_back( targetFps );
103+
104+
// If no practical rates match, fall back to max frame rate
105+
if( framesToCreate.empty() )
106+
framesToCreate.push_back( maxFps );
107+
}
108+
109+
// Create modes for each frame rate
110+
for( int targetFps : framesToCreate ) {
111+
MediaTime frameRate( 1, targetFps ); // Frame duration: 1/fps seconds
112+
113+
std::string modeKey = std::to_string( dimensions.width ) + "x" + std::to_string( dimensions.height ) +
114+
"@" + std::to_string( targetFps ) + "fps_" + std::to_string( (int)pixelFormat );
115+
116+
// Skip if we've already seen this exact mode
117+
if( seenModes.find( modeKey ) != seenModes.end() )
118+
continue;
119+
seenModes.insert( modeKey );
120+
121+
// Create description - only show range if min != max
122+
std::string description = std::to_string( dimensions.width ) + "x" + std::to_string( dimensions.height ) +
123+
" @ " + std::to_string( targetFps ) + "fps";
124+
125+
if( minFps != maxFps ) {
126+
// Device supports variable frame rates - store the range
127+
description += " (range: " + std::to_string( minFps ) + "-" + std::to_string( maxFps ) + "fps)";
128+
modes.emplace_back( Capture::Mode( dimensions.width, dimensions.height, frameRate, Capture::Mode::Codec::Uncompressed, pixelFormat, description ) );
129+
} else {
130+
// Device has fixed frame rate - use simple constructor without range
131+
modes.emplace_back( Capture::Mode( dimensions.width, dimensions.height, frameRate, Capture::Mode::Codec::Uncompressed, pixelFormat, description ) );
132+
}
133+
}
134+
}
135+
136+
return modes;
137+
}
138+
55139
} //namespace
56140

57141
void frameDeallocator( void *refcon )
@@ -108,6 +192,33 @@ - (id)initWithDevice:(const cinder::Capture::DeviceRef)device width:(int)width h
108192
return self;
109193
}
110194

195+
- (id)initWithDevice:(const cinder::Capture::DeviceRef)device mode:(const cinder::Capture::Mode&)mode
196+
{
197+
if( ( self = [super init] ) ) {
198+
mDevice = device;
199+
if( ! mDevice ) {
200+
if( [CaptureImplAvFoundation getDevices:NO].empty() )
201+
throw cinder::CaptureExcInitFail();
202+
mDevice = [CaptureImplAvFoundation getDevices:NO][0];
203+
}
204+
205+
mDeviceUniqueId = [NSString stringWithUTF8String:mDevice->getUniqueId().c_str()];
206+
[mDeviceUniqueId retain];
207+
208+
mIsCapturing = false;
209+
mWidth = mode.getWidth();
210+
mHeight = mode.getHeight();
211+
mHasNewFrame = false;
212+
mExposedFrameBytesPerRow = 0;
213+
mExposedFrameWidth = 0;
214+
mExposedFrameHeight = 0;
215+
216+
// Store the mode for use during capture setup
217+
mSelectedMode = std::make_unique<cinder::Capture::Mode>(mode);
218+
}
219+
return self;
220+
}
221+
111222
- (void)dealloc
112223
{
113224
if( mIsCapturing ) {
@@ -119,7 +230,7 @@ - (void)dealloc
119230
[super dealloc];
120231
}
121232

122-
- (bool)prepareStartCapture
233+
- (bool)prepareStartCapture
123234
{
124235
NSError *error = nil;
125236

@@ -133,64 +244,115 @@ - (bool)prepareStartCapture
133244
else {
134245
device = [AVCaptureDevice deviceWithUniqueID:mDeviceUniqueId];
135246
}
136-
247+
137248
if( ! device ) {
138249
throw cinder::CaptureExcInitFail();
139250
}
140251

252+
[mSession beginConfiguration];
253+
254+
// Configure device format and frame rate if we have a specific mode
255+
if( mSelectedMode ) {
256+
// Find the best matching format for the selected mode
257+
AVCaptureDeviceFormat *bestFormat = nil;
258+
double targetFrameRate = 1.0 / mSelectedMode->getFrameRate().getSeconds(); // Convert duration to rate
259+
260+
// Look for exact or best matching format
261+
for( AVCaptureDeviceFormat *format in [device formats] ) {
262+
CMFormatDescriptionRef formatDesc = [format formatDescription];
263+
CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions( formatDesc );
264+
265+
// Check if resolution matches
266+
if( dimensions.width == mSelectedMode->getWidth() &&
267+
dimensions.height == mSelectedMode->getHeight() ) {
268+
269+
// Check if frame rate is supported
270+
for( AVFrameRateRange *range in [format videoSupportedFrameRateRanges] ) {
271+
if( targetFrameRate >= range.minFrameRate && targetFrameRate <= range.maxFrameRate ) {
272+
bestFormat = format;
273+
break;
274+
}
275+
}
276+
if( bestFormat )
277+
break;
278+
}
279+
}
280+
281+
// Configure device with the selected format
282+
if( bestFormat && [device lockForConfiguration:&error] ) {
283+
device.activeFormat = bestFormat;
284+
285+
// Set frame rate
286+
CMTime frameDuration = CMTimeMake( 1, (int32_t)targetFrameRate );
287+
device.activeVideoMinFrameDuration = frameDuration;
288+
device.activeVideoMaxFrameDuration = frameDuration;
289+
290+
[device unlockForConfiguration];
291+
}
292+
else {
293+
// Use session preset as fallback
294+
if( cinder::ivec2( mWidth, mHeight ) == cinder::ivec2( 640, 480 ) )
295+
mSession.sessionPreset = AVCaptureSessionPreset640x480;
296+
else if( cinder::ivec2( mWidth, mHeight ) == cinder::ivec2( 1280, 720 ) )
297+
mSession.sessionPreset = AVCaptureSessionPreset1280x720;
298+
else if( cinder::ivec2( mWidth, mHeight ) == cinder::ivec2( 1920, 1080 ) )
299+
mSession.sessionPreset = AVCaptureSessionPreset1920x1080;
300+
else
301+
mSession.sessionPreset = AVCaptureSessionPresetMedium;
302+
}
303+
}
304+
else {
305+
// Legacy mode - use session presets
306+
if( cinder::ivec2( mWidth, mHeight ) == cinder::ivec2( 640, 480 ) )
307+
mSession.sessionPreset = AVCaptureSessionPreset640x480;
308+
else if( cinder::ivec2( mWidth, mHeight ) == cinder::ivec2( 1280, 720 ) )
309+
mSession.sessionPreset = AVCaptureSessionPreset1280x720;
310+
else if( cinder::ivec2( mWidth, mHeight ) == cinder::ivec2( 1920, 1080 ) )
311+
mSession.sessionPreset = AVCaptureSessionPreset1920x1080;
312+
else
313+
mSession.sessionPreset = AVCaptureSessionPresetMedium;
314+
}
315+
141316
// Create a device input with the device and add it to the session.
142317
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];
143318
if( ! input ) {
319+
[mSession commitConfiguration];
144320
throw cinder::CaptureExcInitFail();
145321
}
146322
[mSession addInput:input];
147323

148324
// Create a VideoDataOutput and add it to the session
149325
AVCaptureVideoDataOutput *output = [[[AVCaptureVideoDataOutput alloc] init] autorelease];
150-
151326
[mSession addOutput:output];
152327

153-
[mSession beginConfiguration];
154-
if( cinder::ivec2( mWidth, mHeight ) == cinder::ivec2( 640, 480 ) )
155-
mSession.sessionPreset = AVCaptureSessionPreset640x480;
156-
else if( cinder::ivec2( mWidth, mHeight ) == cinder::ivec2( 1280, 720 ) )
157-
mSession.sessionPreset = AVCaptureSessionPreset1280x720;
158-
else
159-
mSession.sessionPreset = AVCaptureSessionPresetMedium;
160328
[mSession commitConfiguration];
161-
162-
//adjust connection settings
163-
/*
164-
//Testing indicates that at least the 3GS doesn't support video orientation changes
165-
NSArray * connections = output.connections;
166-
for( AVCaptureConnection *connection in connections ) {
167-
AVCaptureConnection * connection = [connections objectAtIndex:i];
168-
169-
if( connection.supportsVideoOrientation ) {
170-
connection.videoOrientation = AVCaptureVideoOrientationPortrait;
171-
}
172-
}*/
173329

174330
// Configure your output.
175331
dispatch_queue_t queue = dispatch_queue_create("myQueue", NULL);
176332
[output setSampleBufferDelegate:self queue:queue];
177333
dispatch_release(queue);
178334

335+
// Always request BGRA format to match legacy behavior and avoid YUV conversion issues
336+
// TODO: Add proper YUV support in the future
337+
OSType pixelFormatType = kCVPixelFormatType_32BGRA;
338+
179339
// Specify the pixel format
180340
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
181341
#if ! defined( CINDER_COCOA_TOUCH )
182342
[NSNumber numberWithDouble:mWidth], (id)kCVPixelBufferWidthKey,
183343
[NSNumber numberWithDouble:mHeight], (id)kCVPixelBufferHeightKey,
184344
#endif
185-
[NSNumber numberWithUnsignedInt:kCVPixelFormatType_32BGRA], (id)kCVPixelBufferPixelFormatTypeKey,
345+
[NSNumber numberWithUnsignedInt:pixelFormatType], (id)kCVPixelBufferPixelFormatTypeKey,
186346
nil];
187347
output.videoSettings = options;
188348

189349
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(avCaptureInputPortFormatDescriptionDidChange:) name:AVCaptureInputPortFormatDescriptionDidChangeNotification object:nil];
190350

191-
// If you wish to cap the frame rate to a known value, such as 15 fps, set
192-
// minFrameDuration.
193-
// output.minFrameDuration = CMTimeMake(1, 15);
351+
// Add session runtime error notifications to detect device disconnections
352+
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sessionRuntimeError:) name:AVCaptureSessionRuntimeErrorNotification object:mSession];
353+
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sessionWasInterrupted:) name:AVCaptureSessionWasInterruptedNotification object:mSession];
354+
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sessionInterruptionEnded:) name:AVCaptureSessionInterruptionEndedNotification object:mSession];
355+
194356
return true;
195357
}
196358

@@ -235,7 +397,21 @@ - (void)stopCapture
235397

236398
- (bool)isCapturing
237399
{
238-
return mIsCapturing;
400+
// Check not only our flag, but also the actual session state
401+
// This will detect device disconnections that don't trigger stopCapture
402+
bool sessionRunning = mSession && mSession.isRunning;
403+
bool hasInputs = mSession && mSession.inputs.count > 0;
404+
bool result = mIsCapturing && sessionRunning && hasInputs;
405+
406+
// Log when state doesn't match expectations
407+
if( mIsCapturing && (!sessionRunning || !hasInputs) ) {
408+
CI_LOG_W( "Camera disconnection detected: mIsCapturing=" << mIsCapturing
409+
<< " sessionRunning=" << sessionRunning
410+
<< " hasInputs=" << hasInputs
411+
<< " inputCount=" << (mSession ? mSession.inputs.count : 0) );
412+
}
413+
414+
return result;
239415
}
240416

241417
// Called initially when the camera is instantiated and then again (hypothetically) if the resolution ever changes
@@ -253,6 +429,35 @@ - (void)avCaptureInputPortFormatDescriptionDidChange:(NSNotification *)notificat
253429
}
254430
}
255431

432+
// Session notification handlers for device disconnection detection
433+
- (void)sessionRuntimeError:(NSNotification *)notification
434+
{
435+
NSError *error = notification.userInfo[AVCaptureSessionErrorKey];
436+
CI_LOG_E( "AVCapture session runtime error: " << [error.localizedDescription UTF8String]
437+
<< " (code: " << error.code << ")" );
438+
439+
// Common error codes that indicate device disconnection:
440+
// -11814: Device disconnected
441+
// -11819: Media services were reset
442+
// -11808: Recording stopped (can be disconnection)
443+
// Check if the device is still connected
444+
if( error.code == -11814 || error.code == -11819 || error.code == -11808 ) {
445+
// Mark session as not capturing on these critical errors
446+
mIsCapturing = false;
447+
CI_LOG_W( "Device likely disconnected - marking capture as stopped" );
448+
}
449+
}
450+
451+
- (void)sessionWasInterrupted:(NSNotification *)notification
452+
{
453+
CI_LOG_W( "AVCapture session was interrupted (possible device disconnection)" );
454+
}
455+
456+
- (void)sessionInterruptionEnded:(NSNotification *)notification
457+
{
458+
CI_LOG_I( "AVCapture session interruption ended" );
459+
}
460+
256461
// Delegate routine that is called when a sample buffer was written
257462
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
258463
{
@@ -338,4 +543,4 @@ - (int32_t)getCurrentFrameHeight
338543
return mExposedFrameHeight;
339544
}
340545

341-
@end
546+
@end

0 commit comments

Comments
 (0)