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
2933namespace cinder {
3034
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
57141void 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