Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/camera/camera_avfoundation/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## NEXT
## 0.9.17+4

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

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.)

* Updates minimum supported SDK version to Flutter 3.19/Dart 3.3.

## 0.9.17+3
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,4 +338,27 @@ - (void)testStartWritingShouldNotBeCalledBetweenSampleCreationAndAppending {
CFRelease(videoSample);
}

- (void)testStartVideoRecordingWithCompletionShouldNotDisableMixWithOthers {
FLTCam *cam = FLTCreateCamWithCaptureSessionQueue(dispatch_queue_create("testing", NULL));

id writerMock = OCMClassMock([AVAssetWriter class]);
OCMStub([writerMock alloc]).andReturn(writerMock);
OCMStub([writerMock initWithURL:OCMOCK_ANY fileType:OCMOCK_ANY error:[OCMArg setTo:nil]])
.andReturn(writerMock);

[AVAudioSession.sharedInstance setCategory:AVAudioSessionCategoryPlayback
withOptions:AVAudioSessionCategoryOptionMixWithOthers
error:nil];

[cam
startVideoRecordingWithCompletion:^(FlutterError *_Nullable error) {
}
messengerForStreaming:nil];
XCTAssert(
AVAudioSession.sharedInstance.categoryOptions & AVAudioSessionCategoryOptionMixWithOthers,
@"Flag MixWithOthers was removed.");
XCTAssert(AVAudioSession.sharedInstance.category == AVAudioSessionCategoryPlayAndRecord,
@"Category should be PlayAndRecord.");
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ - (void)prepareForVideoRecordingWithCompletion:
(nonnull void (^)(FlutterError *_Nullable))completion {
__weak typeof(self) weakSelf = self;
dispatch_async(self.captureSessionQueue, ^{
[weakSelf.camera setUpCaptureSessionForAudio];
[weakSelf.camera setUpCaptureSessionForAudioIfNeeded];
completion(nil);
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ - (instancetype)initWithMediaSettings:(FCPPlatformMediaSettings *)mediaSettings
_videoFormat = kCVPixelFormatType_32BGRA;
_inProgressSavePhotoDelegates = [NSMutableDictionary dictionary];
_fileFormat = FCPPlatformImageFileFormatJpeg;
_videoCaptureSession.automaticallyConfiguresApplicationAudioSession = NO;
_audioCaptureSession.automaticallyConfiguresApplicationAudioSession = NO;

// To limit memory consumption, limit the number of frames pending processing.
// After some testing, 4 was determined to be the best maximum value.
Expand Down Expand Up @@ -673,7 +675,8 @@ - (void)captureOutput:(AVCaptureOutput *)output
if (_isFirstVideoSample) {
[_videoWriter startSessionAtSourceTime:currentSampleTime];
// fix sample times not being numeric when pause/resume happens before first sample buffer
// arrives https://github.com/flutter/flutter/issues/132014
// arrives
// https://github.com/flutter/flutter/issues/132014
_lastVideoSampleTime = currentSampleTime;
_lastAudioSampleTime = currentSampleTime;
_isFirstVideoSample = NO;
Expand Down Expand Up @@ -1231,9 +1234,7 @@ - (BOOL)setupWriterForPath:(NSString *)path {
return NO;
}

if (_mediaSettings.enableAudio && !_isAudioSetup) {
[self setUpCaptureSessionForAudio];
}
[self setUpCaptureSessionForAudioIfNeeded];

_videoWriter = [[AVAssetWriter alloc] initWithURL:outputURL
fileType:AVFileTypeMPEG4
Expand Down Expand Up @@ -1313,9 +1314,42 @@ - (BOOL)setupWriterForPath:(NSString *)path {
return YES;
}

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

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

// 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.

// 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?

// which depend on this global state, only change category or options if there is
// change to prevent unnecessary lags and silence
Copy link
Collaborator

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.

// https://github.com/flutter/flutter/issues/131553
Copy link
Collaborator

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.

static void upgradeAudioSessionCategory(AVAudioSessionCategory requestedCategory,
AVAudioSessionCategoryOptions options) {
NSSet *playCategories = [NSSet
setWithObjects:AVAudioSessionCategoryPlayback, AVAudioSessionCategoryPlayAndRecord, nil];
NSSet *recordCategories =
[NSSet setWithObjects:AVAudioSessionCategoryRecord, AVAudioSessionCategoryPlayAndRecord, nil];
NSSet *requiredCategories =
[NSSet setWithObjects:requestedCategory, AVAudioSession.sharedInstance.category, nil];
BOOL needPlay = [requiredCategories intersectsSet:playCategories];
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: requiresPlay, requiresRecord (rather than introduce a second term that means the same thing within the same method).

BOOL needRecord = [requiredCategories intersectsSet:recordCategories];
if (needPlay && needRecord) {
requestedCategory = AVAudioSessionCategoryPlayAndRecord;
} else if (needPlay) {
requestedCategory = AVAudioSessionCategoryPlayback;
} else if (needRecord) {
requestedCategory = AVAudioSessionCategoryRecord;
}
options = AVAudioSession.sharedInstance.categoryOptions | options;
if ([requestedCategory isEqualToString:AVAudioSession.sharedInstance.category] &&
options == AVAudioSession.sharedInstance.categoryOptions) {
return;
}
[AVAudioSession.sharedInstance setCategory:requestedCategory withOptions:options error:nil];
}

- (void)setUpCaptureSessionForAudioIfNeeded {
// Don't setup audio twice or we will lose the audio.
if (_isAudioSetup) {
if (!_mediaSettings.enableAudio || _isAudioSetup) {
return;
}

Expand All @@ -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
Collaborator

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.

upgradeAudioSessionCategory(AVAudioSessionCategoryPlayAndRecord,
AVAudioSessionCategoryOptionDefaultToSpeaker |
AVAudioSessionCategoryOptionAllowBluetoothA2DP |
AVAudioSessionCategoryOptionAllowAirPlay);
};
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.

}

if ([_audioCaptureSession canAddInput:audioInput]) {
[_audioCaptureSession addInput:audioInput];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Collaborator

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.


@end

Expand Down
2 changes: 1 addition & 1 deletion packages/camera/camera_avfoundation/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: camera_avfoundation
description: iOS implementation of the camera plugin.
repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_avfoundation
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
version: 0.9.17+3
version: 0.9.17+4

environment:
sdk: ^3.3.0
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## NEXT
## 2.6.2

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

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.

* Updates minimum supported SDK version to Flutter 3.19/Dart 3.3.

## 2.6.1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,40 @@ - (void)testPublishesInRegistration {
}

#if TARGET_OS_IOS
- (void)testVideoPlayerShouldNotOverwritePlayAndRecordNorDefaultToSpeaker {
NSObject<FlutterPluginRegistrar> *registrar = [GetPluginRegistry()
registrarForPlugin:@"testVideoPlayerShouldNotOverwritePlayAndRecordNorDefaultToSpeaker"];
FVPVideoPlayerPlugin *videoPlayerPlugin =
[[FVPVideoPlayerPlugin alloc] initWithRegistrar:registrar];
FlutterError *error;

[AVAudioSession.sharedInstance setCategory:AVAudioSessionCategoryPlayAndRecord
withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker
error:nil];

[videoPlayerPlugin initialize:&error];
[videoPlayerPlugin setMixWithOthers:true error:&error];
XCTAssert(AVAudioSession.sharedInstance.category == AVAudioSessionCategoryPlayAndRecord,
@"Category should be PlayAndRecord.");
XCTAssert(
AVAudioSession.sharedInstance.categoryOptions & AVAudioSessionCategoryOptionDefaultToSpeaker,
@"Flag DefaultToSpeaker was removed.");
XCTAssert(
AVAudioSession.sharedInstance.categoryOptions & AVAudioSessionCategoryOptionMixWithOthers,
@"Flag MixWithOthers should be set.");

id sessionMock = OCMClassMock([AVAudioSession class]);
OCMStub([sessionMock sharedInstance]).andReturn(sessionMock);
OCMStub([sessionMock category]).andReturn(AVAudioSessionCategoryPlayAndRecord);
OCMStub([sessionMock categoryOptions])
.andReturn(AVAudioSessionCategoryOptionMixWithOthers |
AVAudioSessionCategoryOptionDefaultToSpeaker);
OCMReject([sessionMock setCategory:OCMOCK_ANY withOptions:0 error:[OCMArg setTo:nil]])
.ignoringNonObjectArgs();

[videoPlayerPlugin setMixWithOthers:true error:&error];
}

- (void)validateTransformFixForOrientation:(UIImageOrientation)orientation {
AVAssetTrack *track = [[FakeAVAssetTrack alloc] initWithOrientation:orientation];
CGAffineTransform t = FVPGetStandardizedTransformForTrack(track);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -705,10 +705,46 @@ - (int64_t)onPlayerSetup:(FVPVideoPlayer *)player frameUpdater:(FVPFrameUpdater
return textureId;
}

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

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
Collaborator

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
Collaborator

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
Collaborator

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.

// do not overwrite PlayAndRecord with Playback which causes inability to record
// audio, do not overwrite all options, 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
// 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
Collaborator

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.

#if TARGET_OS_IOS
static void upgradeAudioSessionCategory(AVAudioSessionCategory requestedCategory,
AVAudioSessionCategoryOptions options,
AVAudioSessionCategoryOptions clearOptions) {
NSSet *playCategories = [NSSet
setWithObjects:AVAudioSessionCategoryPlayback, AVAudioSessionCategoryPlayAndRecord, nil];
NSSet *recordCategories =
[NSSet setWithObjects:AVAudioSessionCategoryRecord, AVAudioSessionCategoryPlayAndRecord, nil];
NSSet *requiredCategories =
[NSSet setWithObjects:requestedCategory, AVAudioSession.sharedInstance.category, nil];
BOOL needPlay = [requiredCategories intersectsSet:playCategories];
BOOL needRecord = [requiredCategories intersectsSet:recordCategories];
if (needPlay && needRecord) {
requestedCategory = AVAudioSessionCategoryPlayAndRecord;
} else if (needPlay) {
requestedCategory = AVAudioSessionCategoryPlayback;
} else if (needRecord) {
requestedCategory = AVAudioSessionCategoryRecord;
}
options = (AVAudioSession.sharedInstance.categoryOptions & ~clearOptions) | options;
if ([requestedCategory isEqualToString:AVAudioSession.sharedInstance.category] &&
options == AVAudioSession.sharedInstance.categoryOptions) {
return;
}
[AVAudioSession.sharedInstance setCategory:requestedCategory withOptions:options error:nil];
}
#endif

- (void)initialize:(FlutterError *__autoreleasing *)error {
#if TARGET_OS_IOS
// Allow audio playback when the Ring/Silent switch is set to silent
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
upgradeAudioSessionCategory(AVAudioSessionCategoryPlayback, 0, 0);
#endif

[self.playersByTextureId
Expand Down Expand Up @@ -819,11 +855,11 @@ - (void)setMixWithOthers:(BOOL)mixWithOthers
// AVAudioSession doesn't exist on macOS, and audio always mixes, so just no-op.
#else
if (mixWithOthers) {
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback
withOptions:AVAudioSessionCategoryOptionMixWithOthers
error:nil];
upgradeAudioSessionCategory(AVAudioSession.sharedInstance.category,
AVAudioSessionCategoryOptionMixWithOthers, 0);
} else {
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
upgradeAudioSessionCategory(AVAudioSession.sharedInstance.category, 0,
AVAudioSessionCategoryOptionMixWithOthers);
}
#endif
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: video_player_avfoundation
description: iOS and macOS implementation of the video_player plugin.
repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_avfoundation
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
version: 2.6.1
version: 2.6.2

environment:
sdk: ^3.3.0
Expand Down