Skip to content

Commit 6c769f9

Browse files
committed
enable more than 30 fps
1 parent 19daf6f commit 6c769f9

File tree

9 files changed

+253
-53
lines changed

9 files changed

+253
-53
lines changed

packages/camera/camera_avfoundation/CHANGELOG.md

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

3-
* Fixes a crash due to appending sample buffers when readyForMoreMediaData is NO
7+
* Fixes a crash due to appending sample buffers when readyForMoreMediaData is NO.
48

59
## 0.9.17
610

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,9 @@ - (void)testSettings_shouldPassConfigurationToCameraDeviceAndWriter {
148148

149149
// Expect FPS configuration is passed to camera device.
150150
[self waitForExpectations:@[
151-
injectedWrapper.lockExpectation, injectedWrapper.beginConfigurationExpectation,
151+
injectedWrapper.beginConfigurationExpectation, injectedWrapper.lockExpectation,
152152
injectedWrapper.minFrameDurationExpectation, injectedWrapper.maxFrameDurationExpectation,
153-
injectedWrapper.commitConfigurationExpectation, injectedWrapper.unlockExpectation
153+
injectedWrapper.unlockExpectation, injectedWrapper.commitConfigurationExpectation
154154
]
155155
timeout:1
156156
enforceOrder:YES];
@@ -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: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -210,20 +210,50 @@ - (instancetype)initWithMediaSettings:(FCPPlatformMediaSettings *)mediaSettings
210210
[_motionManager startAccelerometerUpdates];
211211

212212
if (_mediaSettings.framesPerSecond) {
213+
[_mediaSettingsAVWrapper beginConfigurationForSession:_videoCaptureSession];
214+
215+
// Possible values for presets are hard-coded in FLT interface having
216+
// corresponding AVCaptureSessionPreset counterparts.
217+
// If _resolutionPreset is not supported by camera there is
218+
// fallback to lower resolution presets.
219+
// If none can be selected there is error condition.
220+
if (![self setCaptureSessionPreset:_mediaSettings.resolutionPreset withError:error]) {
221+
[_videoCaptureSession commitConfiguration];
222+
return nil;
223+
}
224+
213225
// The frame rate can be changed only on a locked for configuration device.
214226
if ([mediaSettingsAVWrapper lockDevice:_captureDevice error:error]) {
215-
[_mediaSettingsAVWrapper beginConfigurationForSession:_videoCaptureSession];
216-
217-
// Possible values for presets are hard-coded in FLT interface having
218-
// corresponding AVCaptureSessionPreset counterparts.
219-
// If _resolutionPreset is not supported by camera there is
220-
// fallback to lower resolution presets.
221-
// If none can be selected there is error condition.
222-
if (![self setCaptureSessionPreset:_mediaSettings.resolutionPreset withError:error]) {
223-
[_videoCaptureSession commitConfiguration];
224-
[_captureDevice unlockForConfiguration];
225-
return nil;
227+
// find the format which frame rate ranges are closest to the wanted frame rate
228+
CMVideoDimensions targetRes = self.videoDimensionsForFormat(_captureDevice.activeFormat);
229+
double targetFrameRate = _mediaSettings.framesPerSecond.doubleValue;
230+
FourCharCode preferredSubType =
231+
CMFormatDescriptionGetMediaSubType(_captureDevice.activeFormat.formatDescription);
232+
AVCaptureDeviceFormat *bestFormat = _captureDevice.activeFormat;
233+
double bestFrameRate = [self frameRateForFormat:bestFormat closestTo:targetFrameRate];
234+
double minDistance = fabs(bestFrameRate - targetFrameRate);
235+
int bestSubTypeScore = 1;
236+
for (AVCaptureDeviceFormat *format in _captureDevice.formats) {
237+
CMVideoDimensions res = self.videoDimensionsForFormat(format);
238+
if (res.width != targetRes.width || res.height != targetRes.height) {
239+
continue;
240+
}
241+
double frameRate = [self frameRateForFormat:format closestTo:targetFrameRate];
242+
double distance = fabs(frameRate - targetFrameRate);
243+
FourCharCode subType = CMFormatDescriptionGetMediaSubType(format.formatDescription);
244+
int subTypeScore = subType == preferredSubType ? 1 : 0;
245+
if (distance < minDistance ||
246+
(distance == minDistance && subTypeScore > bestSubTypeScore)) {
247+
bestFormat = format;
248+
bestFrameRate = frameRate;
249+
minDistance = distance;
250+
bestSubTypeScore = subTypeScore;
251+
}
226252
}
253+
if (![bestFormat isEqual:_captureDevice.activeFormat]) {
254+
_captureDevice.activeFormat = bestFormat;
255+
}
256+
_mediaSettings.framesPerSecond = @(bestFrameRate);
227257

228258
// Set frame rate with 1/10 precision allowing not integral values.
229259
int fpsNominator = floor([_mediaSettings.framesPerSecond doubleValue] * 10.0);
@@ -232,9 +262,10 @@ - (instancetype)initWithMediaSettings:(FCPPlatformMediaSettings *)mediaSettings
232262
[mediaSettingsAVWrapper setMinFrameDuration:duration onDevice:_captureDevice];
233263
[mediaSettingsAVWrapper setMaxFrameDuration:duration onDevice:_captureDevice];
234264

235-
[_mediaSettingsAVWrapper commitConfigurationForSession:_videoCaptureSession];
236265
[_mediaSettingsAVWrapper unlockDevice:_captureDevice];
266+
[_mediaSettingsAVWrapper commitConfigurationForSession:_videoCaptureSession];
237267
} else {
268+
[_mediaSettingsAVWrapper commitConfigurationForSession:_videoCaptureSession];
238269
return nil;
239270
}
240271
} else {
@@ -250,6 +281,20 @@ - (instancetype)initWithMediaSettings:(FCPPlatformMediaSettings *)mediaSettings
250281
return self;
251282
}
252283

284+
- (double)frameRateForFormat:(AVCaptureDeviceFormat *)format closestTo:(double)targetFrameRate {
285+
double bestFrameRate = 0;
286+
double minDistance = DBL_MAX;
287+
for (AVFrameRateRange *range in format.videoSupportedFrameRateRanges) {
288+
double frameRate = MIN(MAX(targetFrameRate, range.minFrameRate), range.maxFrameRate);
289+
double distance = fabs(frameRate - targetFrameRate);
290+
if (distance < minDistance) {
291+
bestFrameRate = frameRate;
292+
minDistance = distance;
293+
}
294+
}
295+
return bestFrameRate;
296+
}
297+
253298
- (AVCaptureConnection *)createConnection:(NSError **)error {
254299
// Setup video capture input.
255300
_captureVideoInput = [AVCaptureDeviceInput deviceInputWithDevice:_captureDevice error:error];
@@ -543,16 +588,23 @@ - (BOOL)setCaptureSessionPreset:(FCPPlatformResolutionPreset)resolutionPreset
543588
/// Finds the highest available resolution in terms of pixel count for the given device.
544589
- (AVCaptureDeviceFormat *)highestResolutionFormatForCaptureDevice:
545590
(AVCaptureDevice *)captureDevice {
591+
FourCharCode preferredSubType =
592+
CMFormatDescriptionGetMediaSubType(_captureDevice.activeFormat.formatDescription);
546593
AVCaptureDeviceFormat *bestFormat = nil;
547594
NSUInteger maxPixelCount = 0;
595+
int bestSubTypeScore = 0;
548596
for (AVCaptureDeviceFormat *format in _captureDevice.formats) {
549597
CMVideoDimensions res = self.videoDimensionsForFormat(format);
550598
NSUInteger height = res.height;
551599
NSUInteger width = res.width;
552600
NSUInteger pixelCount = height * width;
553-
if (pixelCount > maxPixelCount) {
554-
maxPixelCount = pixelCount;
601+
FourCharCode subType = CMFormatDescriptionGetMediaSubType(format.formatDescription);
602+
int subTypeScore = subType == preferredSubType ? 1 : 0;
603+
if (pixelCount > maxPixelCount ||
604+
(pixelCount == maxPixelCount && subTypeScore > bestSubTypeScore)) {
555605
bestFormat = format;
606+
maxPixelCount = pixelCount;
607+
bestSubTypeScore = subTypeScore;
556608
}
557609
}
558610
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+1
5+
version: 0.9.17+2
66

77
environment:
88
sdk: ^3.2.3

packages/video_player/video_player_avfoundation/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 2.6.2
2+
3+
* Adds possibility to play videos at more than 30 FPS.
4+
* Fixes playing state not updating in some paths.
5+
16
## 2.6.1
27

38
* Adds files to make include directory permanent.

packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m

Lines changed: 93 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ @interface StubFVPDisplayLinkFactory : NSObject <FVPDisplayLinkFactory>
125125

126126
/** This display link to return. */
127127
@property(nonatomic, strong) FVPDisplayLink *displayLink;
128+
@property(nonatomic) void (^fireDisplayLink)(void);
128129

129130
- (instancetype)initWithDisplayLink:(FVPDisplayLink *)displayLink;
130131

@@ -138,6 +139,7 @@ - (instancetype)initWithDisplayLink:(FVPDisplayLink *)displayLink {
138139
}
139140
- (FVPDisplayLink *)displayLinkWithRegistrar:(id<FlutterPluginRegistrar>)registrar
140141
callback:(void (^)(void))callback {
142+
self.fireDisplayLink = callback;
141143
return self.displayLink;
142144
}
143145

@@ -243,13 +245,14 @@ - (void)testSeekToWhilePausedStartsDisplayLinkTemporarily {
243245
OCMStub([mockVideoOutput hasNewPixelBufferForItemTime:kCMTimeZero])
244246
.ignoringNonObjectArgs()
245247
.andReturn(YES);
246-
// Any non-zero value is fine here since it won't actually be used, just NULL-checked.
247-
CVPixelBufferRef fakeBufferRef = (CVPixelBufferRef)1;
248+
CVPixelBufferRef bufferRef;
249+
CVPixelBufferCreate(NULL, 1, 1, kCVPixelFormatType_32BGRA, NULL, &bufferRef);
248250
OCMStub([mockVideoOutput copyPixelBufferForItemTime:kCMTimeZero itemTimeForDisplay:NULL])
249251
.ignoringNonObjectArgs()
250-
.andReturn(fakeBufferRef);
252+
.andReturn(bufferRef);
251253
// Simulate a callback from the engine to request a new frame.
252-
[player copyPixelBuffer];
254+
stubDisplayLinkFactory.fireDisplayLink();
255+
CFRelease([player copyPixelBuffer]);
253256
// Since a frame was found, and the video is paused, the display link should be paused again.
254257
OCMVerify([mockDisplayLink setRunning:NO]);
255258
}
@@ -294,14 +297,15 @@ - (void)testInitStartsDisplayLinkTemporarily {
294297
OCMStub([mockVideoOutput hasNewPixelBufferForItemTime:kCMTimeZero])
295298
.ignoringNonObjectArgs()
296299
.andReturn(YES);
297-
// Any non-zero value is fine here since it won't actually be used, just NULL-checked.
298-
CVPixelBufferRef fakeBufferRef = (CVPixelBufferRef)1;
300+
CVPixelBufferRef bufferRef;
301+
CVPixelBufferCreate(NULL, 1, 1, kCVPixelFormatType_32BGRA, NULL, &bufferRef);
299302
OCMStub([mockVideoOutput copyPixelBufferForItemTime:kCMTimeZero itemTimeForDisplay:NULL])
300303
.ignoringNonObjectArgs()
301-
.andReturn(fakeBufferRef);
304+
.andReturn(bufferRef);
302305
// Simulate a callback from the engine to request a new frame.
303306
FVPVideoPlayer *player = videoPlayerPlugin.playersByTextureId[textureId];
304-
[player copyPixelBuffer];
307+
stubDisplayLinkFactory.fireDisplayLink();
308+
CFRelease([player copyPixelBuffer]);
305309
// Since a frame was found, and the video is paused, the display link should be paused again.
306310
OCMVerify([mockDisplayLink setRunning:NO]);
307311
}
@@ -357,13 +361,14 @@ - (void)testSeekToWhilePlayingDoesNotStopDisplayLink {
357361
OCMStub([mockVideoOutput hasNewPixelBufferForItemTime:kCMTimeZero])
358362
.ignoringNonObjectArgs()
359363
.andReturn(YES);
360-
// Any non-zero value is fine here since it won't actually be used, just NULL-checked.
361-
CVPixelBufferRef fakeBufferRef = (CVPixelBufferRef)1;
364+
CVPixelBufferRef bufferRef;
365+
CVPixelBufferCreate(NULL, 1, 1, kCVPixelFormatType_32BGRA, NULL, &bufferRef);
362366
OCMStub([mockVideoOutput copyPixelBufferForItemTime:kCMTimeZero itemTimeForDisplay:NULL])
363367
.ignoringNonObjectArgs()
364-
.andReturn(fakeBufferRef);
368+
.andReturn(bufferRef);
365369
// Simulate a callback from the engine to request a new frame.
366-
[player copyPixelBuffer];
370+
stubDisplayLinkFactory.fireDisplayLink();
371+
CFRelease([player copyPixelBuffer]);
367372
// Since the video was playing, the display link should not be paused after getting a buffer.
368373
OCMVerify(never(), [mockDisplayLink setRunning:NO]);
369374
}
@@ -790,6 +795,82 @@ - (void)testPublishesInRegistration {
790795
XCTAssertTrue([publishedValue isKindOfClass:[FVPVideoPlayerPlugin class]]);
791796
}
792797

798+
- (void)testPlayerShouldNotDropEverySecondFrame {
799+
NSObject<FlutterPluginRegistrar> *registrar =
800+
[GetPluginRegistry() registrarForPlugin:@"testPlayerShouldNotDropEverySecondFrame"];
801+
NSObject<FlutterPluginRegistrar> *partialRegistrar = OCMPartialMock(registrar);
802+
NSObject<FlutterTextureRegistry> *mockTextureRegistry =
803+
OCMProtocolMock(@protocol(FlutterTextureRegistry));
804+
OCMStub([partialRegistrar textures]).andReturn(mockTextureRegistry);
805+
806+
FVPDisplayLink *displayLink = [[FVPDisplayLink alloc] initWithRegistrar:registrar
807+
callback:^(){
808+
}];
809+
StubFVPDisplayLinkFactory *stubDisplayLinkFactory =
810+
[[StubFVPDisplayLinkFactory alloc] initWithDisplayLink:displayLink];
811+
AVPlayerItemVideoOutput *mockVideoOutput = OCMPartialMock([[AVPlayerItemVideoOutput alloc] init]);
812+
FVPVideoPlayerPlugin *videoPlayerPlugin = [[FVPVideoPlayerPlugin alloc]
813+
initWithAVFactory:[[StubFVPAVFactory alloc] initWithPlayer:nil output:mockVideoOutput]
814+
displayLinkFactory:stubDisplayLinkFactory
815+
registrar:partialRegistrar];
816+
817+
FlutterError *error;
818+
[videoPlayerPlugin initialize:&error];
819+
XCTAssertNil(error);
820+
FVPCreationOptions *create = [FVPCreationOptions
821+
makeWithAsset:nil
822+
uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4"
823+
packageName:nil
824+
formatHint:nil
825+
httpHeaders:@{}];
826+
NSNumber *textureId = [videoPlayerPlugin createWithOptions:create error:&error];
827+
FVPVideoPlayer *player = videoPlayerPlugin.playersByTextureId[textureId];
828+
829+
__block CMTime currentTime = kCMTimeZero;
830+
OCMStub([mockVideoOutput itemTimeForHostTime:0])
831+
.ignoringNonObjectArgs()
832+
.andDo(^(NSInvocation *invocation) {
833+
[invocation setReturnValue:&currentTime];
834+
});
835+
__block NSMutableSet *pixelBuffers = NSMutableSet.new;
836+
OCMStub([mockVideoOutput hasNewPixelBufferForItemTime:kCMTimeZero])
837+
.ignoringNonObjectArgs()
838+
.andDo(^(NSInvocation *invocation) {
839+
CMTime itemTime;
840+
[invocation getArgument:&itemTime atIndex:2];
841+
BOOL has = [pixelBuffers containsObject:[NSValue valueWithCMTime:itemTime]];
842+
[invocation setReturnValue:&has];
843+
});
844+
OCMStub([mockVideoOutput copyPixelBufferForItemTime:kCMTimeZero
845+
itemTimeForDisplay:[OCMArg anyPointer]])
846+
.ignoringNonObjectArgs()
847+
.andDo(^(NSInvocation *invocation) {
848+
CMTime itemTime;
849+
[invocation getArgument:&itemTime atIndex:2];
850+
CVPixelBufferRef bufferRef = NULL;
851+
if ([pixelBuffers containsObject:[NSValue valueWithCMTime:itemTime]]) {
852+
CVPixelBufferCreate(NULL, 1, 1, kCVPixelFormatType_32BGRA, NULL, &bufferRef);
853+
}
854+
[pixelBuffers removeObject:[NSValue valueWithCMTime:itemTime]];
855+
[invocation setReturnValue:&bufferRef];
856+
});
857+
void (^advanceFrame)(void) = ^{
858+
currentTime.value++;
859+
[pixelBuffers addObject:[NSValue valueWithCMTime:currentTime]];
860+
};
861+
862+
advanceFrame();
863+
OCMExpect([mockTextureRegistry textureFrameAvailable:textureId.intValue]);
864+
stubDisplayLinkFactory.fireDisplayLink();
865+
OCMVerifyAllWithDelay(mockTextureRegistry, 10);
866+
867+
advanceFrame();
868+
OCMExpect([mockTextureRegistry textureFrameAvailable:textureId.intValue]);
869+
CFRelease([player copyPixelBuffer]);
870+
stubDisplayLinkFactory.fireDisplayLink();
871+
OCMVerifyAllWithDelay(mockTextureRegistry, 10);
872+
}
873+
793874
#if TARGET_OS_IOS
794875
- (void)validateTransformFixForOrientation:(UIImageOrientation)orientation {
795876
AVAssetTrack *track = [[FakeAVAssetTrack alloc] initWithOrientation:orientation];

0 commit comments

Comments
 (0)