Skip to content

[video_player_avfoundation, camera_avfoundation] never overwrite but only upgrade audio session category #7143

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

Merged
merged 22 commits into from
Jan 13, 2025

Conversation

misos1
Copy link
Contributor

@misos1 misos1 commented Jul 16, 2024

Setting category of AVAudioSession was moved into a wrapper which ensures that only changes which do not disable the ability to play in silent mode (play) and ability to record (record) are considered. If either the new category or old category is play/record then also the set category will be play/record. This currently means that the video player will not overwrite PlayAndRecord with Playback which causes inability to record audio by camera. Options are treated selectively so the video player will no longer overwrite flags set by camera like DefaultToSpeaker and camera will not overwrite MixWithOthers set by video player setMixWithOthers. It will also only change category or options if there is change to prevent lags every time is constructed VideoPlayerController with non-null videoPlayerOptions which always causes call to setMixWithOthers.

Test testSeekToWhilePausedStartsDisplayLinkTemporarily is failing on the main branch on the tested device:

XCTAssertEqual([player position], 1234); // (([player position]) equal to (1234)) failed: ("0") is not equal to ("1234")

Fixes flutter/flutter#131553

Pre-launch Checklist

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

@misos1 misos1 force-pushed the upgrade_audio_session branch from 0043944 to 787f41c Compare July 16, 2024 19:00
@misos1 misos1 marked this pull request as ready for review July 17, 2024 16:06
@misos1 misos1 requested a review from hellohuanlin as a code owner July 17, 2024 16:06
AVAudioSessionCategoryOptions clearOptions) {
if (!NSThread.isMainThread) {
dispatch_sync(dispatch_get_main_queue(), ^{
upgradeAudioSessionCategory(category, options, clearOptions);
Copy link
Contributor

Choose a reason for hiding this comment

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

haven't looked into the details. Does this have to be on main queue? Can it be on capture session queue?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Audio session category is global state, it is changed based on old state so it must be synchronized with other plugins changing it.

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 set a breakpoint and check which thread the caller is in? This function is only called in one place, so if it's called in background, then we simply do a dispatch_async to avoid this unnecessary recursion.

Copy link
Contributor Author

@misos1 misos1 Jul 23, 2024

Choose a reason for hiding this comment

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

There is needed dispatch_sync. Category must be record before adding inputs/outputs to capture session otherwise there would be no audio. It is called on the capture session queue so not on the main thread. Seems also reportErrorMessage is currently called only on capture session queue but there is that FLTEnsureToRunOnMainQueue anyway. If there was similar method which used dispatch_sync it would not make any difference it would just be upgradeAudioSessionCategory->FLTEnsureToRunOnMainQueueSync->dispatch_sync instead of upgradeAudioSessionCategory->dispatch_sync->upgradeAudioSessionCategory so the same depth in stack. Can it be even called recursion if that second call is in another thread on another stack? I think it would be ok to change it to just dispatch_sync as when this is called from the main thread it will crash with an unhandled exception instead of deadlock so that would be obvious for someone who did such a change. Btw are there better options on how to synchronize between independent plugins? Ideally it would be called on the original thread but with some mutex shared between these plugins (and something like NSDistributedLock seems like overkill).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Btw many functions from FLTCam are called on the main thread in tests, now that dispatch_sync causes many tests to fail.

@misos1
Copy link
Contributor Author

misos1 commented Jul 23, 2024

I have no way how to test AVAudioSessionCategoryOptionOverrideMutedMicrophoneInterruption so this one was not added although it might prevent some errors for which camera may not be prepared: "Attempting to start audio input after the system mutes the microphone results in an error.".

@misos1
Copy link
Contributor Author

misos1 commented Jul 24, 2024

I removed AVAudioSessionCategoryOptionAllowBluetooth which basically just enables and prefers HFP profile over A2DP. This is not implicitly present with the playback category and has unintended consequences as output is not stereo and lower quality so if there ever is HFP support it should be opt-in from the dart side.

@hellohuanlin hellohuanlin requested a review from tarrinneal July 24, 2024 21:27
if (_mediaSettings.enableAudio && !_isAudioSetup) {
[self setUpCaptureSessionForAudio];
}
[self setUpCaptureSessionForAudio];
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: setUpCaptureSessionForAudioIfNeeded since you moved condition inside

AVAudioSessionCategoryOptionDefaultToSpeaker |
AVAudioSessionCategoryOptionAllowBluetoothA2DP |
AVAudioSessionCategoryOptionAllowAirPlay,
0);
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we only pass 0 here? Should we just remove this parameter?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It looked like a good idea during development as there is this same function also in the player which uses that argument and it was easier to work on it in a single place and then copy it into the other plugin. One time this argument was actually used when I tried to use AVAudioSessionCategoryOptionAllowBluetooth it looked like a good idea to remove that option when the camera was no longer used.

@@ -1302,9 +1303,50 @@ - (BOOL)setupWriterForPath:(NSString *)path {
return YES;
}

// configure application wide audio session manually to prevent overwriting
// flag MixWithOthers by capture session, only change category if it is considered
// as upgrade which means it can only enable ability to play in silent mode or
Copy link
Contributor

Choose a reason for hiding this comment

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

enable ability to play in silent mode

is it desired behavior?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

System itself is doing this at some point at or after AVCaptureSession addInput or addOutput when automaticallyConfiguresApplicationAudioSession is YES which is default, this behaviour was there also before.

// as upgrade which means it can only enable ability to play in silent mode or
// ability to record audio but never disables it, that could affect other plugins
// which depend on this global state, only change category or options if there is
// change to prevent unnecessary route changes which can cause lags
Copy link
Contributor

Choose a reason for hiding this comment

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

unnecessary route changes

What is a "route change"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

https://developer.apple.com/documentation/avfaudio/responding_to_audio_route_changes?language=objc

When category changes it can cause route change seems:

https://developer.apple.com/documentation/avfaudio/avaudiosessionroutechangereason/avaudiosessionroutechangereasoncategorychange?language=objc

Anyway the effect of setting AVAudioSessionCategoryPlayAndRecord multiple times even when the category is unchanged and there are even no headphones causes half of second or longer silence on my device. Also it causes the video_player plugin to pause and must be manually called play again.

// configure application wide audio session manually to prevent overwriting
// flag MixWithOthers by capture session, only change category if it is considered
// as upgrade which means it can only enable ability to play in silent mode or
// ability to record audio but never disables it, that could affect other plugins
Copy link
Contributor

Choose a reason for hiding this comment

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

affect other plugins

can you explicitly mention video player as an example?

upgradeAudioSessionCategory(AVAudioSessionCategoryPlayAndRecord,
AVAudioSessionCategoryOptionDefaultToSpeaker |
AVAudioSessionCategoryOptionAllowBluetoothA2DP |
AVAudioSessionCategoryOptionAllowAirPlay,
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 explain why these options?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

DefaultToSpeaker is what it was setting also before with automatic session configuration, so better to not change that behaviour. It is also implicit default for AVAudioSessionCategoryPlayback along with AllowBluetoothA2DP and AllowAirPlay which cannot be turned off (it is not set by default for PlayAndRecord). This category uses the video player so it is better to not change player behaviour by just initializing the camera (with audio).

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 add the comments explaining this?

AVAudioSessionCategoryOptionDefaultToSpeaker |
AVAudioSessionCategoryOptionAllowBluetoothA2DP |
AVAudioSessionCategoryOptionAllowAirPlay,
0);
Copy link
Contributor

Choose a reason for hiding this comment

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

I still think it's better not to pass this param. It's making the function a bit complicated to read.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ideally that function would be in a separate file shared with video_player. Is it possible to do that?

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 you mean that better would be to have it as an objc selector rather than C function?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think it's possible to share video player and camera code. Our plugins are not setup to do so.

I meant it seems we only pass 0 and never other values to this param. So do we really need this param?

static void upgradeAudioSessionCategory(AVAudioSessionCategory category,
AVAudioSessionCategoryOptions options,
AVAudioSessionCategoryOptions clearOptions) {
if (category == nil) {
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 explain what category means here? is it the category that we want to upgrade to? or the category that we want to upgrade from?

Copy link
Contributor Author

@misos1 misos1 Aug 20, 2024

Choose a reason for hiding this comment

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

Actually it is a category with which we need to combine an existing category. Newly set category should be such a combination.

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 rename this to requestedCategory? Also when will we need to pass a nil category param? What does it mean by requesting a nil category? and why are we assigning category = AVAudioSession.sharedInstance.category when it's nil

setWithObjects:AVAudioSessionCategoryPlayback, AVAudioSessionCategoryPlayAndRecord, nil];
NSSet *recordCategories =
[NSSet setWithObjects:AVAudioSessionCategoryRecord, AVAudioSessionCategoryPlayAndRecord, nil];
NSSet *categories = [NSSet setWithObjects:category, AVAudioSession.sharedInstance.category, nil];
Copy link
Contributor

Choose a reason for hiding this comment

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

what does this categories mean? why are we combining the set? Can you add some comments to explain?

Copy link
Contributor Author

@misos1 misos1 Aug 20, 2024

Choose a reason for hiding this comment

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

Categories which should be combined. Upgrading means we need to combine categories so the new category has play flavour if any of them has it, and has record if any of them has it. Test whether any of these categories has play is done by intersection which means whether anything in the first set is present in the second set.

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 rename this to requiredFinalCategories or something similar

Copy link
Contributor

@hellohuanlin hellohuanlin left a comment

Choose a reason for hiding this comment

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

Thanks for the comments

static void upgradeAudioSessionCategory(AVAudioSessionCategory category,
AVAudioSessionCategoryOptions options,
AVAudioSessionCategoryOptions clearOptions) {
if (category == nil) {
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 rename this to requestedCategory? Also when will we need to pass a nil category param? What does it mean by requesting a nil category? and why are we assigning category = AVAudioSession.sharedInstance.category when it's nil

setWithObjects:AVAudioSessionCategoryPlayback, AVAudioSessionCategoryPlayAndRecord, nil];
NSSet *recordCategories =
[NSSet setWithObjects:AVAudioSessionCategoryRecord, AVAudioSessionCategoryPlayAndRecord, nil];
NSSet *categories = [NSSet setWithObjects:category, AVAudioSession.sharedInstance.category, nil];
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 rename this to requiredFinalCategories or something similar

upgradeAudioSessionCategory(AVAudioSessionCategoryPlayAndRecord,
AVAudioSessionCategoryOptionDefaultToSpeaker |
AVAudioSessionCategoryOptionAllowBluetoothA2DP |
AVAudioSessionCategoryOptionAllowAirPlay,
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 add the comments explaining this?

AVAudioSessionCategoryOptionDefaultToSpeaker |
AVAudioSessionCategoryOptionAllowBluetoothA2DP |
AVAudioSessionCategoryOptionAllowAirPlay,
0);
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think it's possible to share video player and camera code. Our plugins are not setup to do so.

I meant it seems we only pass 0 and never other values to this param. So do we really need this param?

@misos1
Copy link
Contributor Author

misos1 commented Aug 27, 2024

Also when will we need to pass a nil category param?

It means it should not be changed, used in player, AVAudioSession.sharedInstance.category could not be passed as argument when there was thread switch inside function, now that changed so I can remove that check and make player pass AVAudioSession.sharedInstance.category instead of nil.

I meant it seems we only pass 0 and never other values to this param. So do we really need this param?

No, not in camera at the moment.

@hellohuanlin hellohuanlin requested review from stuartmorgan-g and removed request for tarrinneal September 24, 2024 19:15
Copy link
Contributor

@stuartmorgan-g stuartmorgan-g left a comment

Choose a reason for hiding this comment

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

Overall this looks like a good strategy to make the global state changes we unfortunately have less disruptive without causing any breaking changes.

(Long term we should still try to separate out the global changes into their own methods, and maybe even their own plugin, but that's a much larger undertaking that I don't expect us to get to for some time.)


* Fixes overwriting flag MixWithOthers set by video_player.
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think it's going to be clear to a client of camera what this means; please add more context about what the actual problem being solved is. (Also, it's fine to give video_player as an example, but it shouldn't make it sound like this is specific to video_player; any other native code could collide.)

@@ -1313,9 +1314,42 @@ - (BOOL)setupWriterForPath:(NSString *)path {
return YES;
}

- (void)setUpCaptureSessionForAudio {
// this same function is also in video_player_avfoundation
Copy link
Contributor

Choose a reason for hiding this comment

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

Comments should be properly formatted sentences.

https://google.github.io/styleguide/objcguide.html#comments

// as upgrade which means it can only enable ability to play in silent mode or
// ability to record audio but never disables it, that could affect other plugins
// which depend on this global state, only change category or options if there is
// change to prevent unnecessary lags and silence
Copy link
Contributor

Choose a reason for hiding this comment

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

It's not clear to me how this last part is different from what the comment already said so far (possibly because the sentence is a run-on by this point so it's hard to follow). Please reword to clarify if this is supposed to be adding new details.

// ability to record audio but never disables it, that could affect other plugins
// which depend on this global state, only change category or options if there is
// change to prevent unnecessary lags and silence
// https://github.com/flutter/flutter/issues/131553
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't need an issue link; we generally only link to issues for TODOs, or cases that are not understandable from a comment (e.g., very subtle edge cases). The idea of only upgrading global mode is clear enough.

@@ -1331,6 +1365,19 @@ - (void)setUpCaptureSessionForAudio {
// Setup the audio output.
_audioOutput = [[AVCaptureAudioDataOutput alloc] init];

dispatch_block_t block = ^{
// Setup options implicit to AVAudioSessionCategoryPlayback to not disturb video_player.
Copy link
Contributor

Choose a reason for hiding this comment

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

This comment doesn't make sense to me in this context; this call is primarily about making a change, not not making a change. The comment on the function itself covers this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is to explain why there are such options: #7143 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on the function does not cover this.

Copy link
Contributor

Choose a reason for hiding this comment

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

How about Upgrade options requested by camera without conflicting other plugins like video_player.

@@ -113,7 +113,7 @@ NS_ASSUME_NONNULL_BEGIN
- (void)startImageStreamWithMessenger:(NSObject<FlutterBinaryMessenger> *)messenger;
- (void)stopImageStream;
- (void)setZoomLevel:(CGFloat)zoom withCompletion:(void (^)(FlutterError *_Nullable))completion;
- (void)setUpCaptureSessionForAudio;
- (void)setUpCaptureSessionForAudioIfNeeded;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this called outside the implementation? If not it should be moved to the private category in the .m file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is also called from prepareForVideoRecordingWithCompletion in CameraPlugin.m.


* Fixes audio recorded only with first recording.
Copy link
Contributor

Choose a reason for hiding this comment

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

This description doesn't make sense in the context of this package, which does not record anything. Please describe the fix in terms of what is changing generally, not the specific effect it has on a specific test app that readers of the changelog won't have context on.

@@ -705,10 +705,46 @@ - (int64_t)onPlayerSetup:(FVPVideoPlayer *)player frameUpdater:(FVPFrameUpdater
return textureId;
}

// this same function is also in camera_avfoundation
Copy link
Contributor

Choose a reason for hiding this comment

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

This is the least important information in the comment, so should be last as a final note.

Copy link
Contributor

Choose a reason for hiding this comment

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

And it's not actually true, because they are slightly different due to clearOptions. So this should just be removed.

Copy link
Contributor Author

@misos1 misos1 Oct 14, 2024

Choose a reason for hiding this comment

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

Before it was the same (it was removed due to a comment about it not being used in camera). It was supposed to be in a single place but I could not find a way to share source files between plugins so I put it twice for the time being until it is possibly moved elsewhere into a single place. I think such a comment makes sense (actually it may be important enough to be on top) because without this context making certain changes to it in one plugin without changing it also in other plugins can result in it no longer working as intended (like simple change in camera to call it on capture session queue instead of main, that change may seem reasonable without context). Maybe I should change it a little like "..similar function..." and that these two need to be "synchronized" resp. on the same queue.

Copy link
Contributor

Choose a reason for hiding this comment

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

The comments and implementation strategy seem overly focused on these two plugins to me. There could be 50 plugins in the ecosystem that set global audio state, only two of which we happen to control, so any solution that relies on these two plugins being exactly synchronized doesn't scale.

I would like the solution (and its comments) here to focus on things that are generally true:

  • AVAudioSession is not explicitly documented as being thread safe, so we should only use it from the main thread. We can mention that other plugin or application code could be using AVAudioSession.sharedInstance as well, and that we have to hope that they are doing the same.
  • We should upgrade rather than stomp the state because other code may be setting the global state as well, and we don't want to remove options they may have set.

Neither of those statements require exact synchronization with other code, and both generalize to the entire ecosystem. (Although as mentioned earlier, the real ecosystem-friendly solution is to move away from doing this in individual plugins at all, and leaving it for apps to do.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is not enough to just use AVAudioSession on the main thread, something like this would not work well:

dispatch_sync(main_queue, () => old_options = AVAudioSession.sharedInstance.categoryOptions);
new_options = old_options | AVAudioSessionCategoryOptionMixWithOthers;
dispatch_sync(main_queue, () => AVAudioSession.sharedInstance.categoryOptions = new_options);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

AVAudioSession is not explicitly documented as being thread safe

Btw category and categoryOptions are atomic properties and I doubt that capture session is changing them on the main thread when automaticallyConfiguresApplicationAudioSession is true.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just for curiosity, here it is changed by capture session and it is not possible that it happened on the main thread:

      dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"%@", AVAudioSession.sharedInstance.category);
        dispatch_sync(_captureSessionQueue, ^{
          [_audioCaptureSession addOutput:_audioOutput];
        });
        NSLog(@"%@", AVAudioSession.sharedInstance.category);
      });
AVAudioSessionCategoryPlayback
AVAudioSessionCategoryPlayAndRecord

Copy link
Contributor

Choose a reason for hiding this comment

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

It is not enough to just use AVAudioSession on the main thread, something like this would not work well:

dispatch_sync(main_queue, () => old_options = AVAudioSession.sharedInstance.categoryOptions);
new_options = old_options | AVAudioSessionCategoryOptionMixWithOthers;
dispatch_sync(main_queue, () => AVAudioSession.sharedInstance.categoryOptions = new_options);

We don't need comments to tell people not to use basic threading anti-patterns. Reading, modifying, and writing back a value in a non-atomic ways is one of the most basic things people familiar with threading and locking learn that they should not do.

Copy link
Contributor Author

@misos1 misos1 Dec 19, 2024

Choose a reason for hiding this comment

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

Is this the last change request still not fulfilled after the latest commits? Should that comment be further rewritten to not explicitly mention the other plugin but something more general? You mentioned "solution" but I am not sure what changes you mean to be done to that implementation.

// ability to record audio but never disables it, that could affect other plugins
// which depend on this global state, only change category or options if there is
// change to prevent unnecessary lags and silence
// https://github.com/flutter/flutter/issues/131553
Copy link
Contributor

Choose a reason for hiding this comment

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

All the comments in the other file apply here as well.

@misos1 misos1 requested a review from stuartmorgan-g October 16, 2024 15:59
Copy link
Contributor

@hellohuanlin hellohuanlin left a comment

Choose a reason for hiding this comment

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

Sorry for the delay. It's looking good. Just a few questions.

@@ -1,3 +1,7 @@
## 0.9.17+5

* Fixes changing global audio session category to be collision free.
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: ...collision free across plugins

@@ -1313,9 +1314,42 @@ - (BOOL)setupWriterForPath:(NSString *)path {
return YES;
}

- (void)setUpCaptureSessionForAudio {
// This function, although slightly modified, is also in video_player_avfoundation.
// Both need to do the same thing and run on the same thread.
Copy link
Contributor

Choose a reason for hiding this comment

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

...run on the main thread?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

"Same" is a requirement for correct synchronization. But if AVAudioSession must really be used only on the main thread and the fact that probably the only reasonable thread to use here between plugins is the main thread maybe it would be better to change it to "main".

if (!NSThread.isMainThread) {
dispatch_sync(dispatch_get_main_queue(), block);
} else {
block();
Copy link
Contributor

Choose a reason for hiding this comment

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

is setUpCaptureSessionForAudioIfNeeded already guaranteed to be on background? Can you double check the caller of this function? If it's already on background, we can simply do

NSAssert(!NSThread.isMainThread);
dispatch_sync(dispatch_get_main_queue()) {
  // your logic here
};

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is called on the main thread in tests if I remember correctly #7143 (comment).

Copy link
Contributor

Choose a reason for hiding this comment

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

is it only called on main thread in tests? Can you give an example of such failure? I'd like to see if it's possible to run on background thread in those tests

Copy link
Contributor Author

@misos1 misos1 Nov 29, 2024

Choose a reason for hiding this comment

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

Try to run tests with this:

  //if (!NSThread.isMainThread) {
    dispatch_sync(dispatch_get_main_queue(), block);
  //} else {
    //block();
  //}

And you will get this inside xcode:

Thread 1: EXC_BREAKPOINT (code=1, subcode=0x18082dc74)

When running outside of xcode with debugging there will be probably just crash, you can add NSLog(@"%@", NSThread.currentThread); to see that it runs on main thread <_NSMainThread: 0x2804b0080>{number = 1, name = main}.

You will probably need to run flutter pub upgrade first as without that I am getting this error during flutter run (win32 on macos?):

Could not build the precompiled application for the device.
Error (Xcode): ../../../../../../../../.pub-cache/hosted/pub.dev/win32-5.2.0/lib/src/guid.dart:32:9: Error: Type 'UnmodifiableUint8ListView' not found.

Copy link
Contributor

Choose a reason for hiding this comment

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

It is called on the main thread in tests if I remember correctly #7143 (comment).

When running outside of xcode with debugging there will be probably just crash, you can add NSLog(@"%@", NSThread.currentThread); to see that it runs on main thread <_NSMainThread: 0x2804b0080>{number = 1, name = main}.

So setUpCaptureSessionForAudioIfNeeded is called on main thread somewhere (not just in test). Could you find the place it's called on main? The session setup shouldn't happen on main thread.

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 same situation is also with FLTEnsureToRunOnMainQueue.

Could you explain the problem with FLTEnsureToRunOnMainQueue? Maybe a concrete example?

How exactly is this a problem anyway? There are many other "adaptations" only for tests, like interfaces and factories just to simplify testing, in both camera and video_player.

I don't know why you are comparing with interfaces and factories.

Imagine you have this code:

func setupSession() {
  if is background thread {
    Logic A
  } else {
    Logic B
  }
}

Here your production code only calls Logic A, and test code only calls Logic B. Then the test isn't really providing any confidence right?

What I suggest is:

func setupSession() { 
  Assert(session must be setup on background);
  Logic A;
}

Then in the test, you can do:

func testSetupSession() {
  dispatch_to_background {
    setupSession();
  }
}

This way, the test code actually covers Logic A, providing us with confidence in production.

Copy link
Contributor Author

@misos1 misos1 Dec 11, 2024

Choose a reason for hiding this comment

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

Your assertion will fail because other tests call setupSession on the main thread. And would this not cause a deadlock due to dispatch_sync waiting for block dispatched on main queue while also waitForExpectation is waiting on main thread until dispatch_sync is done and expectation fulfilled? discussion_r1876564910

testFoo() {
  let expectation = ...
  sessionQueue.async {
    dispatch_sync(main_queue(), ...)
    expectation.fulfill()
  }
  waitForExpectation()
}

What I proposed does not have such problems and tests also cover production logic as I wrote.

Copy link
Contributor

Choose a reason for hiding this comment

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

And would this not cause a deadlock due to dispatch_sync waiting for block dispatched on main queue while also waitForExpectation is waiting on main thread until dispatch_sync is done and expectation fulfilled?

Oh I overlooked the part that it's dispatch_sync to main, which leads to deadlock in test.

What I proposed does not have such problems and tests also cover production logic as I wrote.

It would be the same problem, that dispatch_sync(main) is inherently not testable - You simply moved the same problem to FLTEnsureToRunOnMainQueueSync (i.e. the dispatch_sync branch of this helper would not be testable for the same reason). So I'd accept the current code as it is.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It would be the same problem

Oh I did not think of that, but as you wrote that, I realized that means that also the async test should be untestable, maybe it actually waits in some non blocking way. But anyway I did not see that documented anywhere, maybe that async test is not 100% clean then?

Copy link
Contributor

Choose a reason for hiding this comment

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

I realized that means that also the async test should be untestable

dispatch_async is different and it can be tested. The problem with dispatch_sync is that it causes deadlock when dispatching to the same queue.

@@ -1331,6 +1365,19 @@ - (void)setUpCaptureSessionForAudio {
// Setup the audio output.
_audioOutput = [[AVCaptureAudioDataOutput alloc] init];

dispatch_block_t block = ^{
// Setup options implicit to AVAudioSessionCategoryPlayback to not disturb video_player.
Copy link
Contributor

Choose a reason for hiding this comment

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

How about Upgrade options requested by camera without conflicting other plugins like video_player.

@misos1 misos1 requested a review from hellohuanlin November 28, 2024 18:53
Copy link
Contributor

@stuartmorgan-g stuartmorgan-g left a comment

Choose a reason for hiding this comment

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

LGTM!

Thanks for your patience on this review; I'm still catching up on reviews after being out of office for several weeks

@stuartmorgan-g stuartmorgan-g added the autosubmit Merge PR when tree becomes green via auto submit App label Jan 13, 2025
Copy link
Contributor

auto-submit bot commented Jan 13, 2025

auto label is removed for flutter/packages/7143, due to - The status or check suite ci.yaml validation has failed. Please fix the issues identified (or deflake) before re-applying this label.

  • The status or check suite Merge Queue Guard has failed. Please fix the issues identified (or deflake) before re-applying this label.

@auto-submit auto-submit bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Jan 13, 2025
@stuartmorgan-g stuartmorgan-g added the autosubmit Merge PR when tree becomes green via auto submit App label Jan 13, 2025
Copy link
Contributor

auto-submit bot commented Jan 13, 2025

auto label is removed for flutter/packages/7143, due to - The status or check suite Mac_arm64 macos_platform_tests master - packages has failed. Please fix the issues identified (or deflake) before re-applying this label.

@auto-submit auto-submit bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Jan 13, 2025
@stuartmorgan-g stuartmorgan-g added the autosubmit Merge PR when tree becomes green via auto submit App label Jan 13, 2025
@auto-submit auto-submit bot merged commit 5bb6a8c into flutter:main Jan 13, 2025
77 checks passed
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Jan 14, 2025
github-merge-queue bot pushed a commit to flutter/flutter that referenced this pull request Jan 14, 2025
flutter/packages@3c3bc68...d1fd623

2025-01-13 [email protected] [camera] Add API support query for
image streaming (flutter/packages#8250)
2025-01-13 [email protected] [webview_flutter_android] Add additional
WebSettings methods (flutter/packages#8270)
2025-01-13 [email protected] Roll Flutter from
864d4f5 to 72db8f6 (11 revisions) (flutter/packages#8421)
2025-01-13 [email protected]
[video_player_avfoundation, camera_avfoundation] never overwrite but
only upgrade audio session category (flutter/packages#7143)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-packages-flutter-autoroll
Please CC [email protected] on the revert to ensure that a
human
is aware of the problem.

To file a bug in Flutter:
https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
maheshj01 pushed a commit to maheshj01/flutter that referenced this pull request Jan 15, 2025
…r#161597)

flutter/packages@3c3bc68...d1fd623

2025-01-13 [email protected] [camera] Add API support query for
image streaming (flutter/packages#8250)
2025-01-13 [email protected] [webview_flutter_android] Add additional
WebSettings methods (flutter/packages#8270)
2025-01-13 [email protected] Roll Flutter from
864d4f5 to 72db8f6 (11 revisions) (flutter/packages#8421)
2025-01-13 [email protected]
[video_player_avfoundation, camera_avfoundation] never overwrite but
only upgrade audio session category (flutter/packages#7143)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-packages-flutter-autoroll
Please CC [email protected] on the revert to ensure that a
human
is aware of the problem.

To file a bug in Flutter:
https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
autosubmit Merge PR when tree becomes green via auto submit App p: camera p: video_player platform-ios platform-macos
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Camera] Audio is only recorded with first recording on specific iOS device (iPhone 7)
3 participants