Skip to content

Commit 719bd84

Browse files
authored
[camera_avfoundation] enable more than 30 fps (#7394)
Camera plugin was crashing when I tried to set fps to 60 on most media presets (except maybe on 1280x720, although tested device supports 60 fps for up to 1920x1440 and can do 240 fps on 1280x720) because when is `activeVideoMinFrameDuration` and `activeVideoMaxFrameDuration` set to fps outside of what active format supports it throws exception. Now it tries to find a format which supports fps closest to wanted fps and clamps it if it cannot be set to exact value to prevent crashes. It searches for formats with the exact same resolution. For example in format list it can be like "1920x1080 { 3- 30 fps}", "1920x1080 { 3- 60 fps}" and "1920x1080 { 6-120 fps}, but when setting `sessionPreset` then "1920x1080 { 3- 30 fps}" is selected by default. On the tested device there are 2 "media subtypes" `420f` and `420v` for each format where the first is denoted as "supports wide color" in debug description and the system has tendency to choose this one. So it tries to preserve the media subtype to what is preferred by the system and this is also added to `highestResolutionFormatForCaptureDevice` (with lower priority than max resolution/fps). Also there was nested `lockForConfiguration` and `unlockForConfiguration` when using `ResolutionPreset.max` together with setting up fps.
1 parent 5d419d1 commit 719bd84

File tree

5 files changed

+121
-19
lines changed

5 files changed

+121
-19
lines changed

packages/camera/camera_avfoundation/CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.9.17+5
2+
3+
* Adds ability to use any supported FPS and fixes crash when using unsupported FPS.
4+
15
## 0.9.17+4
26

37
* Updates Pigeon for non-nullable collection type support.
@@ -13,7 +17,7 @@
1317

1418
## 0.9.17+1
1519

16-
* Fixes a crash due to appending sample buffers when readyForMoreMediaData is NO
20+
* Fixes a crash due to appending sample buffers when readyForMoreMediaData is NO.
1721

1822
## 0.9.17
1923

packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.m

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,4 +202,20 @@ - (void)testSettings_ShouldBeSupportedByMethodCall {
202202
XCTAssertNotNil(resultValue);
203203
}
204204

205+
- (void)testSettings_ShouldSelectFormatWhichSupports60FPS {
206+
FCPPlatformMediaSettings *settings =
207+
[FCPPlatformMediaSettings makeWithResolutionPreset:gTestResolutionPreset
208+
framesPerSecond:@(60)
209+
videoBitrate:@(gTestVideoBitrate)
210+
audioBitrate:@(gTestAudioBitrate)
211+
enableAudio:gTestEnableAudio];
212+
213+
FLTCam *camera = FLTCreateCamWithCaptureSessionQueueAndMediaSettings(
214+
dispatch_queue_create("test", NULL), settings, nil, nil);
215+
216+
AVFrameRateRange *range = camera.captureDevice.activeFormat.videoSupportedFrameRateRanges[0];
217+
XCTAssertLessThanOrEqual(range.minFrameRate, 60);
218+
XCTAssertGreaterThanOrEqual(range.maxFrameRate, 60);
219+
}
220+
205221
@end

packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraTestUtils.m

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,44 @@
5252
OCMStub([audioSessionMock addInputWithNoConnections:[OCMArg any]]);
5353
OCMStub([audioSessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES);
5454

55+
id frameRateRangeMock1 = OCMClassMock([AVFrameRateRange class]);
56+
OCMStub([frameRateRangeMock1 minFrameRate]).andReturn(3);
57+
OCMStub([frameRateRangeMock1 maxFrameRate]).andReturn(30);
58+
id captureDeviceFormatMock1 = OCMClassMock([AVCaptureDeviceFormat class]);
59+
OCMStub([captureDeviceFormatMock1 videoSupportedFrameRateRanges]).andReturn(@[
60+
frameRateRangeMock1
61+
]);
62+
63+
id frameRateRangeMock2 = OCMClassMock([AVFrameRateRange class]);
64+
OCMStub([frameRateRangeMock2 minFrameRate]).andReturn(3);
65+
OCMStub([frameRateRangeMock2 maxFrameRate]).andReturn(60);
66+
id captureDeviceFormatMock2 = OCMClassMock([AVCaptureDeviceFormat class]);
67+
OCMStub([captureDeviceFormatMock2 videoSupportedFrameRateRanges]).andReturn(@[
68+
frameRateRangeMock2
69+
]);
70+
71+
id captureDeviceMock = OCMClassMock([AVCaptureDevice class]);
72+
OCMStub([captureDeviceMock lockForConfiguration:[OCMArg setTo:nil]]).andReturn(YES);
73+
OCMStub([captureDeviceMock formats]).andReturn((@[
74+
captureDeviceFormatMock1, captureDeviceFormatMock2
75+
]));
76+
__block AVCaptureDeviceFormat *format = captureDeviceFormatMock1;
77+
OCMStub([captureDeviceMock setActiveFormat:[OCMArg any]]).andDo(^(NSInvocation *invocation) {
78+
[invocation retainArguments];
79+
[invocation getArgument:&format atIndex:2];
80+
});
81+
OCMStub([captureDeviceMock activeFormat]).andDo(^(NSInvocation *invocation) {
82+
[invocation setReturnValue:&format];
83+
});
84+
5585
id fltCam = [[FLTCam alloc] initWithMediaSettings:mediaSettings
5686
mediaSettingsAVWrapper:mediaSettingsAVWrapper
5787
orientation:UIDeviceOrientationPortrait
5888
videoCaptureSession:videoSessionMock
5989
audioCaptureSession:audioSessionMock
6090
captureSessionQueue:captureSessionQueue
6191
captureDeviceFactory:captureDeviceFactory ?: ^AVCaptureDevice *(void) {
62-
return [AVCaptureDevice deviceWithUniqueID:@"camera"];
92+
return captureDeviceMock;
6393
}
6494
videoDimensionsForFormat:^CMVideoDimensions(AVCaptureDeviceFormat *format) {
6595
return CMVideoFormatDescriptionGetDimensions(format.formatDescription);

packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCam.m

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,59 @@ - (instancetype)initWithCameraName:(NSString *)cameraName
153153
error:error];
154154
}
155155

156+
// Returns frame rate supported by format closest to targetFrameRate.
157+
static double bestFrameRateForFormat(AVCaptureDeviceFormat *format, double targetFrameRate) {
158+
double bestFrameRate = 0;
159+
double minDistance = DBL_MAX;
160+
for (AVFrameRateRange *range in format.videoSupportedFrameRateRanges) {
161+
double frameRate = MIN(MAX(targetFrameRate, range.minFrameRate), range.maxFrameRate);
162+
double distance = fabs(frameRate - targetFrameRate);
163+
if (distance < minDistance) {
164+
bestFrameRate = frameRate;
165+
minDistance = distance;
166+
}
167+
}
168+
return bestFrameRate;
169+
}
170+
171+
// Finds format with same resolution as current activeFormat in captureDevice for which
172+
// bestFrameRateForFormat returned frame rate closest to mediaSettings.framesPerSecond.
173+
// Preferred are formats with the same subtype as current activeFormat. Sets this format
174+
// as activeFormat and also updates mediaSettings.framesPerSecond to value which
175+
// bestFrameRateForFormat returned for that format.
176+
static void selectBestFormatForRequestedFrameRate(
177+
AVCaptureDevice *captureDevice, FCPPlatformMediaSettings *mediaSettings,
178+
VideoDimensionsForFormat videoDimensionsForFormat) {
179+
CMVideoDimensions targetResolution = videoDimensionsForFormat(captureDevice.activeFormat);
180+
double targetFrameRate = mediaSettings.framesPerSecond.doubleValue;
181+
FourCharCode preferredSubType =
182+
CMFormatDescriptionGetMediaSubType(captureDevice.activeFormat.formatDescription);
183+
AVCaptureDeviceFormat *bestFormat = captureDevice.activeFormat;
184+
double bestFrameRate = bestFrameRateForFormat(bestFormat, targetFrameRate);
185+
double minDistance = fabs(bestFrameRate - targetFrameRate);
186+
BOOL isBestSubTypePreferred = YES;
187+
for (AVCaptureDeviceFormat *format in captureDevice.formats) {
188+
CMVideoDimensions resolution = videoDimensionsForFormat(format);
189+
if (resolution.width != targetResolution.width ||
190+
resolution.height != targetResolution.height) {
191+
continue;
192+
}
193+
double frameRate = bestFrameRateForFormat(format, targetFrameRate);
194+
double distance = fabs(frameRate - targetFrameRate);
195+
FourCharCode subType = CMFormatDescriptionGetMediaSubType(format.formatDescription);
196+
BOOL isSubTypePreferred = subType == preferredSubType;
197+
if (distance < minDistance ||
198+
(distance == minDistance && isSubTypePreferred && !isBestSubTypePreferred)) {
199+
bestFormat = format;
200+
bestFrameRate = frameRate;
201+
minDistance = distance;
202+
isBestSubTypePreferred = isSubTypePreferred;
203+
}
204+
}
205+
captureDevice.activeFormat = bestFormat;
206+
mediaSettings.framesPerSecond = @(bestFrameRate);
207+
}
208+
156209
- (instancetype)initWithMediaSettings:(FCPPlatformMediaSettings *)mediaSettings
157210
mediaSettingsAVWrapper:(FLTCamMediaSettingsAVWrapper *)mediaSettingsAVWrapper
158211
orientation:(UIDeviceOrientation)orientation
@@ -226,6 +279,9 @@ - (instancetype)initWithMediaSettings:(FCPPlatformMediaSettings *)mediaSettings
226279
return nil;
227280
}
228281

282+
selectBestFormatForRequestedFrameRate(_captureDevice, _mediaSettings,
283+
_videoDimensionsForFormat);
284+
229285
// Set frame rate with 1/10 precision allowing not integral values.
230286
int fpsNominator = floor([_mediaSettings.framesPerSecond doubleValue] * 10.0);
231287
CMTime duration = CMTimeMake(10, fpsNominator);
@@ -474,56 +530,42 @@ - (BOOL)setCaptureSessionPreset:(FCPPlatformResolutionPreset)resolutionPreset
474530
// Set the best device format found and finish the device configuration.
475531
_captureDevice.activeFormat = bestFormat;
476532
[_captureDevice unlockForConfiguration];
477-
478-
// Set the preview size based on values from the current capture device.
479-
_previewSize =
480-
CGSizeMake(_captureDevice.activeFormat.highResolutionStillImageDimensions.width,
481-
_captureDevice.activeFormat.highResolutionStillImageDimensions.height);
482533
break;
483534
}
484535
}
485536
}
486537
case FCPPlatformResolutionPresetUltraHigh:
487538
if ([_videoCaptureSession canSetSessionPreset:AVCaptureSessionPreset3840x2160]) {
488539
_videoCaptureSession.sessionPreset = AVCaptureSessionPreset3840x2160;
489-
_previewSize = CGSizeMake(3840, 2160);
490540
break;
491541
}
492542
if ([_videoCaptureSession canSetSessionPreset:AVCaptureSessionPresetHigh]) {
493543
_videoCaptureSession.sessionPreset = AVCaptureSessionPresetHigh;
494-
_previewSize =
495-
CGSizeMake(_captureDevice.activeFormat.highResolutionStillImageDimensions.width,
496-
_captureDevice.activeFormat.highResolutionStillImageDimensions.height);
497544
break;
498545
}
499546
case FCPPlatformResolutionPresetVeryHigh:
500547
if ([_videoCaptureSession canSetSessionPreset:AVCaptureSessionPreset1920x1080]) {
501548
_videoCaptureSession.sessionPreset = AVCaptureSessionPreset1920x1080;
502-
_previewSize = CGSizeMake(1920, 1080);
503549
break;
504550
}
505551
case FCPPlatformResolutionPresetHigh:
506552
if ([_videoCaptureSession canSetSessionPreset:AVCaptureSessionPreset1280x720]) {
507553
_videoCaptureSession.sessionPreset = AVCaptureSessionPreset1280x720;
508-
_previewSize = CGSizeMake(1280, 720);
509554
break;
510555
}
511556
case FCPPlatformResolutionPresetMedium:
512557
if ([_videoCaptureSession canSetSessionPreset:AVCaptureSessionPreset640x480]) {
513558
_videoCaptureSession.sessionPreset = AVCaptureSessionPreset640x480;
514-
_previewSize = CGSizeMake(640, 480);
515559
break;
516560
}
517561
case FCPPlatformResolutionPresetLow:
518562
if ([_videoCaptureSession canSetSessionPreset:AVCaptureSessionPreset352x288]) {
519563
_videoCaptureSession.sessionPreset = AVCaptureSessionPreset352x288;
520-
_previewSize = CGSizeMake(352, 288);
521564
break;
522565
}
523566
default:
524567
if ([_videoCaptureSession canSetSessionPreset:AVCaptureSessionPresetLow]) {
525568
_videoCaptureSession.sessionPreset = AVCaptureSessionPresetLow;
526-
_previewSize = CGSizeMake(352, 288);
527569
} else {
528570
if (error != nil) {
529571
*error =
@@ -537,23 +579,33 @@ - (BOOL)setCaptureSessionPreset:(FCPPlatformResolutionPreset)resolutionPreset
537579
return NO;
538580
}
539581
}
582+
CMVideoDimensions size = self.videoDimensionsForFormat(_captureDevice.activeFormat);
583+
_previewSize = CGSizeMake(size.width, size.height);
540584
_audioCaptureSession.sessionPreset = _videoCaptureSession.sessionPreset;
541585
return YES;
542586
}
543587

544588
/// Finds the highest available resolution in terms of pixel count for the given device.
589+
/// Preferred are formats with the same subtype as current activeFormat.
545590
- (AVCaptureDeviceFormat *)highestResolutionFormatForCaptureDevice:
546591
(AVCaptureDevice *)captureDevice {
592+
FourCharCode preferredSubType =
593+
CMFormatDescriptionGetMediaSubType(_captureDevice.activeFormat.formatDescription);
547594
AVCaptureDeviceFormat *bestFormat = nil;
548595
NSUInteger maxPixelCount = 0;
596+
BOOL isBestSubTypePreferred = NO;
549597
for (AVCaptureDeviceFormat *format in _captureDevice.formats) {
550598
CMVideoDimensions res = self.videoDimensionsForFormat(format);
551599
NSUInteger height = res.height;
552600
NSUInteger width = res.width;
553601
NSUInteger pixelCount = height * width;
554-
if (pixelCount > maxPixelCount) {
555-
maxPixelCount = pixelCount;
602+
FourCharCode subType = CMFormatDescriptionGetMediaSubType(format.formatDescription);
603+
BOOL isSubTypePreferred = subType == preferredSubType;
604+
if (pixelCount > maxPixelCount ||
605+
(pixelCount == maxPixelCount && isSubTypePreferred && !isBestSubTypePreferred)) {
556606
bestFormat = format;
607+
maxPixelCount = pixelCount;
608+
isBestSubTypePreferred = isSubTypePreferred;
557609
}
558610
}
559611
return bestFormat;

packages/camera/camera_avfoundation/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: camera_avfoundation
22
description: iOS implementation of the camera plugin.
33
repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_avfoundation
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
5-
version: 0.9.17+4
5+
version: 0.9.17+5
66

77
environment:
88
sdk: ^3.3.0

0 commit comments

Comments
 (0)