Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[camera_avfoundation] enable more than 30 fps #7394

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

misos1
Copy link
Contributor

@misos1 misos1 commented Aug 12, 2024

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.

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@misos1 misos1 marked this pull request as ready for review August 14, 2024 18:46
@stuartmorgan
Copy link
Contributor

@misos1 Could you split this into two different PRs? Having non-trivial changes to two unrelated plugins in the same PR makes review much more difficult.

@misos1 misos1 changed the title [video_player_avfoundation, camera_avfoundation] enable more than 30 fps [camera_avfoundation] enable more than 30 fps Aug 21, 2024

* Adds possibility to use any supported FPS and fixes crash when using unsupported FPS.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*Adds ability to...

double bestFrameRate = [self frameRateForFormat:bestFormat closestTo:targetFrameRate];
double minDistance = fabs(bestFrameRate - targetFrameRate);
int bestSubTypeScore = 1;
for (AVCaptureDeviceFormat *format in _captureDevice.formats) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you also move this to a helper function?

@@ -211,20 +211,50 @@ - (instancetype)initWithMediaSettings:(FCPPlatformMediaSettings *)mediaSettings
[_motionManager startAccelerometerUpdates];

if (_mediaSettings.framesPerSecond) {
[_mediaSettingsAVWrapper beginConfigurationForSession:_videoCaptureSession];

// Possible values for presets are hard-coded in FLT interface having
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is all this code moving out of the locked section; is that safe?

Copy link
Contributor Author

@misos1 misos1 Sep 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not see anything saying that the order of beginConfiguration and lockForConfiguration is important or that setting the session preset must be under that lock. There is example where it is used in that order: https://developer.apple.com/documentation/avfoundation/avcapturedevice/1389221-activeformat?language=objc

I moved setCaptureSessionPreset outside of lock because it can itself call lockForConfiguration and unlockForConfiguration and I did not see anything about whether that lock is recursive.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems lockForConfiguration is actually recursive from my observation (I could not get mentioned exceptions until now) as locking twice and unlocking once does not cause exceptions but unlocking twice yes. So I can revert this change (?).

Also it seems not all commitConfiguration calls go through _mediaSettingsAVWrapper.

[_captureDevice unlockForConfiguration];
return nil;
// find the format which frame rate ranges are closest to the wanted frame rate
CMVideoDimensions targetRes = self.videoDimensionsForFormat(_captureDevice.activeFormat);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've added several calls to self in this init method. The fact that there was already such a call is a problem, and that should not be made worse by adding more.

If there are general, stateless utilities that you want to call from init, they should be turned into freestanding static functions.

[_videoCaptureSession commitConfiguration];
[_captureDevice unlockForConfiguration];
return nil;
// find the format which frame rate ranges are closest to the wanted frame rate
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments need to follow standard sentence rules. https://google.github.io/styleguide/objcguide.html#comments

[_captureDevice unlockForConfiguration];
return nil;
// find the format which frame rate ranges are closest to the wanted frame rate
CMVideoDimensions targetRes = self.videoDimensionsForFormat(_captureDevice.activeFormat);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't abbreviate variable names. https://google.github.io/styleguide/objcguide.html#naming

FourCharCode subType = CMFormatDescriptionGetMediaSubType(format.formatDescription);
int subTypeScore = subType == preferredSubType ? 1 : 0;
if (distance < minDistance ||
(distance == minDistance && subTypeScore > bestSubTypeScore)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused by this condition. bestSubTypeScore is initialized to 1, and subTypeScore is always either 0 or 1. How could this last piece of the condition ever be true?

I'm not clear on what this score even is; this whole algorithm needs some explanatory comment at the beginning.

Copy link
Contributor Author

@misos1 misos1 Aug 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if distance < minDistance then bestSubTypeScore can be overwritten by 0. This line explains it: subTypeScore = subType == preferredSubType ? 1 : 0, the score of the subtype is 1 when it is the preferred subtype and 0 otherwise. This is how to choose by minimum distance and then from formats which have equal distance choose that one with preferred subtype, it gives to subtypes some order. Instead of subTypeScore > bestSubTypeScore there could be something like bestSubType != preferredSubType && subType == preferredSubType but that makes it less clear as it deviates from simple way how to order something by multiple properties current.A < other.A || (current.A == other.A && current.B < other.B).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but that makes it less clear

I strongly disagree that abstracting the thing that matters—whether the current type is a preferred type—behind a completely arbitrary numeric value makes this code easier to understand. The code you have there that you described as "less clear" makes sense to me when I read it, whereas I did not understand what this score was, or why something that could only have two possible values was being called a score in the first place.


* Adds possibility to use any supported FPS and fixes crash when using unsupported FPS.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is the new test that triggers the codepath that used to crash?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the real captureDevice.activeVideoMinFrameDuration (or ...Max...) is set to value outside of what is supported by active format it immediately throws an objc exception. For tests, mocked wrappers are used so this cannot be triggered. But the test testSettings_ShouldSelectFormatWhichSupports60FPS tries to set 60 fps which actually triggered this crash on my device for media presets other than 1280x720.

@@ -251,6 +254,55 @@ - (instancetype)initWithMediaSettings:(FCPPlatformMediaSettings *)mediaSettings
return self;
}

static void selectBestFormatForRequestedFrameRate(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These helpers should be defined above their call sites; I'm suprised this is even compiling.

@@ -251,6 +254,55 @@ - (instancetype)initWithMediaSettings:(FCPPlatformMediaSettings *)mediaSettings
return self;
}

static void selectBestFormatForRequestedFrameRate(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per the Obj-C style guide, all declarations require declaration comments explaining them.

In this case, that should include covering what "select" means exactly, given that this doesn't return anything. Presumably it's modifying one or more arguments, but I can't tell without reading the implementation.

mediaSettings.framesPerSecond = @(bestFrameRate);
}

static double bestFrameRateForFormat(AVCaptureDeviceFormat *format, double targetFrameRate) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also needs a comment (e.g., explaining what "best" means).

double distance = fabs(frameRate - targetFrameRate);
FourCharCode subType = CMFormatDescriptionGetMediaSubType(format.formatDescription);
if (distance < minDistance || (distance == minDistance && subType == preferredSubType &&
bestSubType != preferredSubType)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here and in the very similar code below, I think you could improve the readability of the complex condition by extracting a local variable just before this if:

BOOL isBetterTypeMatch = subType == preferredSubType && bestSubType != preferredSubType;

Then you can use that in the if condition. (Yes, this reduces short-circuiting, but this isn't an expensive calculation.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe subType == preferredSubType can be extracted into variable subTypeIsPreferred and instead of bestSubType will be stored bestSubTypeIsPreferred then that condition would become distance == minDistance && subTypeIsPreferred && !bestSubTypeIsPreferred (which is actually similar to that original with scores just there is bool instead of int and a && !b instead of a > b).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants