-
-
Notifications
You must be signed in to change notification settings - Fork 21
Feat: Type-safe configureAVAudioSession function that ensures compatibility with AVAudioSessionModes and other customizations #53
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
base: main
Are you sure you want to change the base?
Conversation
…ility with AVAudioSessionModes
… more customization
|
So of the 4 commits:
For the record, I'm also quite stupid at git as well... sorry for the mess... |
|
@hirbod I got a little addicted to the cause and I spent all morning adding another method called I also improved some of the hover-over notes for the original new function I created. |
|
Amazing work, keep going! :D |
|
We don’t have to support anything before iOS 15.1. I usually follow Expo’s support path, and I don’t see any reason why we’d need to support anything this old either. |
| } | ||
|
|
||
| @ReactMethod | ||
| public void getAVAudioSessionStatus() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am really unexperienced here, but maybe we should dig a bit more and give Android some love too :D
package.json
Outdated
| "commitlint": "^17.6.1", | ||
| "eslint": "^8.39.0", | ||
| "eslint-config-prettier": "^8.8.0", | ||
| "eslint-plugin-ft-flow": "^3.0.11", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
whats this plugin
|
Awesome, I'll review all these comments and correct them. I can remove all the "AV"s and I'll clear the deprecations out entirely, good call on the next release. I'll also do some studying up on Android and its Audio sessions. I'll also remove that eslint plugin, sorry when I was first trying to build the example app, I got that error saying "eslint-plugin-xxx" is missing", install it, so I did lmfao. I think I was just making a mistake and not running yarn prepare first. I'll clear that eslint plugin out, forgot it was in there. |
|
Actually I removed that AV comment but seems like you already read the email lol :D |
|
@hirbod I agree with removing AV if we want to merge functionalities with Android somehow. It seems like Android has an equivalent to AVAudioSession called AudioManager. It seems like AudioSession has some similarities like I am sure we could come up with an equivalent So keeping the future in mind, how would you like to proceed? Option 1 (Two separate functions) configureAudioSessionIOS()
configureAudioSessionAndroid()Option 2 (Almost like Platform.select) configureAudioSession({
ios: {},
android: {},
})Option 3 (Merge functionalities - produces less customization) // These props are made up as an example
configureAudioSession({
allowRecording: true,
allowBluetooth: true,
mixWithOthers: false,
})I guess my vote as a package user is to allow as much customization as possible, given that is what this library is all about. The way I see it is To your other point about converting to swift and kotlin, I think it's a great idea, however, I should not be the one to perform this. I tried the swift conversion this morning and just fed the whole file to GPT to convert and finally got it to build right but there was a bunch of synthax and |
|
Sorry bro, I'm in some sort of endless commit loop I'm losing my mind lmfao.... |
51f295a to
d769d55
Compare
|
You can’t really use this package together with expo-video at the same time, as Expo overwrites the audio session. If you manipulate it, the video will pause, and if you start a video, it will overwrite the session. Expo-video has mixWithOthers implemented as prop. But this is not a package bug or limitation; it’s how the audio session in iOS works. Each AV session is responsible for its own handling. expo-video would need a “headless mode” where it trusts that another package has already configured the audio session, but that’s too complicated for most people. As soon as a video is running, it already has a session. You won’t be able to change expo-video’s audio session or parameters, as they are set in its own code. const player = useVideoPlayer(uri, (player) => {
player.audioMixingMode = 'doNotMix'
player.bufferOptions = {
waitsToMinimizeStalling: false,
minBufferForPlayback: 1,
preferredForwardBufferDuration: 2,
}
player.loop = true
})What I do though is to register AppState listeners to inform the OS that the Audio session is "free again", which allows paused music to continue. We added this part to allow Apple Music or Spotify to continue when the App was backgrounded. const onAppStateChange = (state: AppStateStatus) => {
if (isWeb) return
if (state === 'active') {
VolumeManager.enable(true)
}
if (state === 'background') {
VolumeManager.enable(false)
}
}And right before I initialize expo-video, I call useEffect(() => {
// Set the audio session to ambient
// Expo-Video is too slow to switch the audio session, so we use VolumeManager to set the audio session to ambient
// this allows for seamless audio switching
VolumeManager.setCategory('Ambient', false)
return () => {
// when we unmount this screen, we want to disable the Audio Session to allow the system to resume the audio
VolumeManager.enable(false)
VolumeManager.setActive(false)
}
}, []) |
|
Okay so just so I understand, I went into // MARK: - Audio Session Management
internal func setAppropriateAudioSessionOrWarn() {
// let audioSession = AVAudioSession.sharedInstance()
// let audioMixingMode = findAudioMixingMode()
// var audioSessionCategoryOptions: AVAudioSession.CategoryOptions = audioSession.categoryOptions
// let isOutputtingAudio = videoPlayers.allObjects.contains { player in
// player.isPlaying && !player.isMuted
// }
// let anyPlayerShowsNotification = videoPlayers.allObjects.contains { player in
// player.showNowPlayingNotification
// }
// let shouldMixOverride = audioMixingMode == .mixWithOthers
// let doNotMixOverride = audioMixingMode == .doNotMix
// let shouldDuckOthers = audioMixingMode == .duckOthers && isOutputtingAudio
// // The now playing notification won't be shown if we allow the audio to mix with others
// let autoShouldMix = !isOutputtingAudio && !anyPlayerShowsNotification
// let shouldMixWithOthers = shouldMixOverride || autoShouldMix
// if shouldMixWithOthers && !shouldDuckOthers && !doNotMixOverride {
// audioSessionCategoryOptions.insert(.mixWithOthers)
// } else {
// audioSessionCategoryOptions.remove(.mixWithOthers)
// }
// if shouldDuckOthers && !doNotMixOverride {
// audioSessionCategoryOptions.insert(.duckOthers)
// } else {
// audioSessionCategoryOptions.remove(.duckOthers)
// }
// if audioSession.categoryOptions != audioSessionCategoryOptions {
// log.warn("Set audio session to", audioSessionCategoryOptions)
// do {
// try audioSession.setCategory(.playback, mode: .moviePlayback, options: audioSessionCategoryOptions)
// } catch {
// log.warn("Failed to set audio session category. This might cause issues with audio playback and Picture in Picture. \(error.localizedDescription)")
// }
}
// Make sure audio session is active if any video is playing
// if isOutputtingAudio || doNotMixOverride {
// do {
// try audioSession.setActive(true)
// } catch {
// log.warn("Failed to activate the audio session. This might cause issues with audio playback. \(error.localizedDescription)")
// }
// }
// The now playing notification requires correct audio session category, notify the manager of about the change.
// NowPlayingManager.shared.refreshNowPlaying()
// }Would this not strip |
|
In theory, yes. You’d need to set an appropriate audio session before the video plays. You can see that this function prepares the category and calls .setActive() at the end. |
|
Just dropping this here to study—the audio session manager part: My library is definitely hard to use with others that already manipulate or overwrite the audio session. The main idea was to control volume, the silent switch, and hide the volume UI. To do this, you need a running audio session. I’m not saying this library doesn’t have its place—it’s definitely nice for controlling sessions when backgrounding/foregrounding the app—but chances are high that other libraries already have listeners for this (like the old expo-av, which was really bad). Unless libraries like expo-video or react-native-video provide “audio session-free modes,” it’s going to be really hard to make them work together nicely. But yeah, even for volume listening, you need access to the audio session. |
|
So I have this really weird use case in my app... My app will display a video and record a user on camera at the same time. This means that expo-video and react-native-vision-camera both trigger audio sessions and SOMETIMES they collide with each other and cause a recording failure. I solve this problem by stripping those of their audio sessions and using this library... So far, I'm using my PR and it seems to be working well. |
|
Yeah but this was only possible because you patched both libs, right? |
|
That is unfortunately correct lol.... |
|
We need to pitch this to @behenate and @lukmccall from Expo, and @mrousavy for Vision Camera, that a “no audio session” mode would be very helpful, where the user is responsible for activating/managing the audio session. Ideally, one could pass a flag to those libs (also React Native Video—CC @KrzysztofMoch). Could you guys consider a prop or method that completely avoids audio handling in your libraries? I’ll call this the “professional mode.” This would allow users with specialized audio handling needs to control the entire flow. Right now, it’s basically impossible since most methods in all the libs either activate, enable, or disable the audio session or change the category. I know this is beneficial in most cases for the average user, and the ask is not to remove it but rather to introduce a flag that disables audio management on your end and leaves it to the user. |
|
I agree. // Defaults to false (for obvious reasons)
<Camera
disableAudioSessionManagement={true} // Allows developers to manage custom sessions on their own
/>
<Video
disableAudioSessionManagement={true} // Allows developers to manage custom sessions on their own
/>I submitted this about a year ago: Some related issues: Honestly should be fairly simply PR / additions. Basically just an |
|
@hirbod I was thinking a nice feature adjustment to this new API I've been working on would be like some basic configurations pre-built? Like: configureAudioSession(PremadeConfig.IgnoreSilentSwitch)
configureAudioSession(PremadeConfig.DuckBackgroundAudio) |
Yeah this should be simple to do. It should affect all video components, so I am thinking about flag Prop approach is also fine for me Also I think we should add asserts in (video/vision-camera) in places where we require specific config, to avoid bugs and misconceptions |
|
Without reading any of the comments yet (i'm on vacation in japan), an ideal scenario would be if this would be a Nitro Module, and the audio session is a Hybrid Object. Then we can use it as a common type and VisionCamera can interop with it seamlessly - same as other libs. |
|
Disabling audio session management by VisionCamera/RNVideo is also okay I guess - but what would be really cool and most powerful is definitely just passing around a configured Audio Session that's gon be used. I guess approacg #1 is simplest tho |
|
@KrzysztofMoch don't use build flags for that |
|
While I can see why a Hybrid Object might be tempting, I’d look for something more generic and easier for now. Having a mode offered by RNV, Expo Video (+Audio), RNVC, and possibly the next big one—React Native Track Player—where the audio session is “skipped” seems relatively easy to implement for all parties without much overhead. By default, all libraries continue managing audio sessions, as that’s likely still the best approach for the ecosystem. However, having a professional mode for more sophisticated routing, where an “audio-less” mode is available, would be amazing. Very happy to see first positive signals here by @mrousavy (enjoy your vacation man!) and @KrzysztofMoch. I'll try to chat with @behenate via Slack to get a signal / how they think about. |
@hirbod This is my first time contributing to open-source... like ever. And I should also add that this is my first time coding in Obj-C.... So please feel free to crush my soul on how I did all of this lmfao...
The gist of it is, after reading all the docs: https://developer.apple.com/documentation/avfaudio/avaudiosession, it became clear that each
AVAudioSessionCategorycould only be used with specificAVAudioSessionModes. I was pretty thorough in making sure that I have the compatible modes with each category.In order to make this type-safe, I needed to combine them into one function. I also used this to change the types of
AVAudioSessionModeandAVAudioSessionCategoryto anenumtype so that could properly get good developer experience tool tips when mousing over each category. For example:Additionally, you'll note here that the

setCategoryfunction had multiple different parametersI took my best shot at implementing this level of customization.
An example of this new function would be:
I also marked the
setCategoryfunction and thesetModefunction as deprecated using this new API.Among other changes, it would appear that the previously had
Alarmcategory is no longer listed, and theAudioProcessingcategory is listed as deprecated. https://developer.apple.com/documentation/avfaudio/avaudiosession/category-swift.structSo I removed the
Alarmoption and flagged theAudioProcessingas deprecated. (Note that you actually did not have it in your native code anyway...).One last comment... I noticed while studying
react-native-vision-cameraAudio Session setup he has this code:So I added this plus one other option I ran across in the docs.
So... the kicker... I compiled the app and did a little bit of testing, I worked through a few compile errors I missed (small things) and everything seems to be not crashing and the function appears to be calling correctly on iOS.
That being said.... You may want to review my code and test it somehow... Sorry for not knowing a lot, I just really wanted to help out with what I was trying to accomplish...