From f3f37354a9e6c4fd54ccaad5637f9f75798d5b02 Mon Sep 17 00:00:00 2001 From: michal Date: Thu, 15 May 2025 11:55:05 +0200 Subject: [PATCH 1/8] feat: initial implementation of audio focus options --- .../com/swmansion/audioapi/AudioAPIModule.kt | 12 +- .../audioapi/system/AudioFocusListener.kt | 27 +++-- .../audioapi/system/MediaSessionManager.kt | 106 ++++++++++++++++-- .../src/specs/NativeAudioAPIModule.ts | 7 +- .../src/system/AudioManager.ts | 11 +- .../src/system/types.ts | 66 +++++++++++ 6 files changed, 201 insertions(+), 28 deletions(-) diff --git a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt index 791d5360f..7355463c9 100644 --- a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt +++ b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt @@ -78,10 +78,6 @@ class AudioAPIModule( override fun getDevicePreferredSampleRate(): Double = MediaSessionManager.getDevicePreferredSampleRate() - override fun observeAudioInterruptions(enabled: Boolean) { - MediaSessionManager.observeAudioInterruptions(enabled) - } - override fun observeVolumeChanges(enabled: Boolean) { MediaSessionManager.observeVolumeChanges(enabled) } @@ -95,4 +91,12 @@ class AudioAPIModule( val res = MediaSessionManager.checkRecordingPermissions() promise!!.resolve(res) } + + override fun requestAudioFocus(request: ReadableMap) { + MediaSessionManager.requestAudioFocus(request) + } + + override fun abandonAudioFocus() { + MediaSessionManager.abandonAudioFocus() + } } diff --git a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/AudioFocusListener.kt b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/AudioFocusListener.kt index 3c1d2f468..ec4a11a49 100644 --- a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/AudioFocusListener.kt +++ b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/AudioFocusListener.kt @@ -1,5 +1,6 @@ package com.swmansion.audioapi.system +import android.media.AudioAttributes import android.media.AudioFocusRequest import android.media.AudioManager import android.os.Build @@ -59,19 +60,25 @@ class AudioFocusListener( } } - fun requestAudioFocus() { + fun requestAudioFocus( + focusRequestBuilder: AudioFocusRequest.Builder, + audioAttributesBuilder: AudioAttributes.Builder, + legacyStreamType: Int, + focusGain: Int, + ): Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - this.focusRequest = - AudioFocusRequest - .Builder(AudioManager.AUDIOFOCUS_GAIN) - .setOnAudioFocusChangeListener(this) - .build() - - audioManager.get()?.requestAudioFocus(focusRequest!!) + val focusRequest = + focusRequestBuilder + .setFocusGain(focusGain) + .setAudioAttributes( + audioAttributesBuilder + .setLegacyStreamType(legacyStreamType) + .build(), + ).build() + audioManager.requestAudioFocus(focusRequest) } else { - audioManager.get()?.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN) + audioManager.requestAudioFocus(this, legacyStreamType, focusGain) } - } fun abandonAudioFocus() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && this.focusRequest != null) { diff --git a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt index 1fcde1ebc..9a75d8caa 100644 --- a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt +++ b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt @@ -10,6 +10,8 @@ import android.content.Intent import android.content.IntentFilter import android.content.ServiceConnection import android.content.pm.PackageManager +import android.media.AudioAttributes +import android.media.AudioFocusRequest import android.media.AudioManager import android.os.Build import android.os.IBinder @@ -137,14 +139,6 @@ object MediaSessionManager { return sampleRate.toDouble() } - fun observeAudioInterruptions(observe: Boolean) { - if (observe) { - audioFocusListener.requestAudioFocus() - } else { - audioFocusListener.abandonAudioFocus() - } - } - fun observeVolumeChanges(observe: Boolean) { if (observe) { ContextCompat.registerReceiver( @@ -159,10 +153,104 @@ object MediaSessionManager { } fun requestRecordingPermissions(currentActivity: Activity?): String { - ActivityCompat.requestPermissions(currentActivity!!, arrayOf(Manifest.permission.RECORD_AUDIO), 200) + ActivityCompat.requestPermissions( + currentActivity!!, + arrayOf(Manifest.permission.RECORD_AUDIO), + 200, + ) return checkRecordingPermissions() } + fun requestAudioFocus(request: ReadableMap) { + val afbd: AudioFocusRequest.Builder = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) + var focusGain: Int = AudioManager.AUDIOFOCUS_GAIN + val legacyStreamType: Int = AudioManager.STREAM_MUSIC + val aabd: AudioAttributes.Builder = AudioAttributes.Builder() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (request.hasKey("focusGain")) { + when (request.getString("focusGain")) { + "audiofocus_gain" -> focusGain = AudioManager.AUDIOFOCUS_GAIN + "audiofocus_gain_transient" -> focusGain = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT + "audiofocus_gain_transient_exclusive" -> + focusGain = + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE + + "audiofocus_gain_transient_may_duck" -> + focusGain = + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK + } + } + if (request.hasKey("pauseWhenDucked")) { + when (request.getBoolean("pauseWhenDucked")) { + true -> afbd.setWillPauseWhenDucked(true) + false -> afbd.setWillPauseWhenDucked(false) + } + } + if (request.hasKey("acceptsDelayedFocusGain")) { + when (request.getBoolean("acceptsDelayedFocusGain")) { + true -> afbd.setAcceptsDelayedFocusGain(true) + false -> afbd.setAcceptsDelayedFocusGain(false) + } + } + if (request.hasKey("audioAttributes")) { + val audioAttributesMap = request.getMap("audioAttributes") + if (audioAttributesMap != null) { + if (audioAttributesMap.hasKey("allowedCapturePolicy")) { + when (audioAttributesMap.getString("allowedCapturePolicy")) { + "allow_capture_by_all" -> aabd.setAllowedCapturePolicy(AudioAttributes.ALLOW_CAPTURE_BY_ALL) + "allow_capture_by_system" -> aabd.setAllowedCapturePolicy(AudioAttributes.ALLOW_CAPTURE_BY_SYSTEM) + "allow_capture_by_none" -> aabd.setAllowedCapturePolicy(AudioAttributes.ALLOW_CAPTURE_BY_NONE) + } + } + if (audioAttributesMap.hasKey("contentType")) { + when (audioAttributesMap.getString("contentType")) { + "content_type_sonification" -> aabd.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + "content_type_music" -> aabd.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + "content_type_movie" -> aabd.setContentType(AudioAttributes.CONTENT_TYPE_MOVIE) + "content_type_speech" -> aabd.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) + "content_type_unknown" -> aabd.setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN) + } + } + if (audioAttributesMap.hasKey("flag")) { + when (audioAttributesMap.getString("flag")) { + "flag_hw_av_sync" -> aabd.setFlags(AudioAttributes.FLAG_HW_AV_SYNC) + "flag_audibility_enforced" -> aabd.setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED) + } + } + if (audioAttributesMap.hasKey("hapticChannelsMuted")) { + when (audioAttributesMap.getBoolean("hapticChannelsMuted")) { + true -> aabd.setHapticChannelsMuted(true) + false -> aabd.setHapticChannelsMuted(false) + } + } + if (audioAttributesMap.hasKey("isContentSpatialized")) { + when (audioAttributesMap.getBoolean("isContentSpatialized")) { + true -> aabd.setIsContentSpatialized(true) + false -> aabd.setIsContentSpatialized(false) + } + } + if (audioAttributesMap.hasKey("legacyStreamType")) { + when (audioAttributesMap.getString("legacyStreamType")) { + "stream_voice_call" -> aabd.setLegacyStreamType(AudioManager.STREAM_VOICE_CALL) + "stream_system" -> aabd.setLegacyStreamType(AudioManager.STREAM_SYSTEM) + "stream_ring" -> aabd.setLegacyStreamType(AudioManager.STREAM_RING) + "stream_music" -> aabd.setLegacyStreamType(AudioManager.STREAM_MUSIC) + "stream_alarm" -> aabd.setLegacyStreamType(AudioManager.STREAM_ALARM) + "stream_notification" -> aabd.setLegacyStreamType(AudioManager.STREAM_NOTIFICATION) + } + } + if (audioAttributesMap.hasKey("spatializationBehavior")) { + } + } + audioFocusListener.requestAudioFocus(afbd, aabd, legacyStreamType, focusGain) + } + } + } + + fun abandonAudioFocus() { + audioFocusListener.abandonAudioFocus() + } + fun checkRecordingPermissions(): String = if (ContextCompat.checkSelfPermission( reactContext.get()!!, diff --git a/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts b/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts index 44ede514e..71dd4d2d9 100644 --- a/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts +++ b/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts @@ -1,7 +1,7 @@ 'use strict'; import { TurboModuleRegistry } from 'react-native'; import type { TurboModule } from 'react-native'; -import { PermissionStatus } from '../system/types'; +import { PermissionStatus, AudioAttributeType } from '../system/types'; interface Spec extends TurboModule { install(): boolean; @@ -17,7 +17,10 @@ interface Spec extends TurboModule { options: Array ): void; getDevicePreferredSampleRate(): number; - observeAudioInterruptions(enabled: boolean): void; + requestAudioFocus(request: { + [key: string]: string | boolean | number | AudioAttributeType | undefined; + }): void; + abandonAudioFocus(): void; observeVolumeChanges(enabled: boolean): void; requestRecordingPermissions(): Promise; checkRecordingPermissions(): Promise; diff --git a/packages/react-native-audio-api/src/system/AudioManager.ts b/packages/react-native-audio-api/src/system/AudioManager.ts index 224b5e868..78c44f9e4 100644 --- a/packages/react-native-audio-api/src/system/AudioManager.ts +++ b/packages/react-native-audio-api/src/system/AudioManager.ts @@ -1,4 +1,9 @@ -import { SessionOptions, LockScreenInfo, PermissionStatus } from './types'; +import { + SessionOptions, + LockScreenInfo, + PermissionStatus, + RequestAudioFocusOptions, +} from './types'; import { SystemEventName, SystemEventCallback } from '../events/types'; import { NativeAudioAPIModule } from '../specs'; import { AudioEventEmitter, AudioEventSubscription } from '../events'; @@ -39,8 +44,8 @@ class AudioManager { return NativeAudioAPIModule!.getDevicePreferredSampleRate(); } - observeAudioInterruptions(enabled: boolean) { - NativeAudioAPIModule!.observeAudioInterruptions(enabled); + requestAudioFocus(request: RequestAudioFocusOptions) { + NativeAudioAPIModule!.requestAudioFocus(request); } observeVolumeChanges(enabled: boolean) { diff --git a/packages/react-native-audio-api/src/system/types.ts b/packages/react-native-audio-api/src/system/types.ts index 0b681a380..ff9a0ee36 100644 --- a/packages/react-native-audio-api/src/system/types.ts +++ b/packages/react-native-audio-api/src/system/types.ts @@ -52,3 +52,69 @@ export interface LockScreenInfo extends BaseLockScreenInfo { } export type PermissionStatus = 'Undetermined' | 'Denied' | 'Granted'; + +type audioAttributeUsageType = + | 'usage_alarm' + | 'usage_assistance_accessibility' + | 'usage_assistance_navigation_guidance' + | 'usage_assistance_sonification' + | 'usage_assistant' + | 'usage_game' + | 'usage_media' + | 'usage_notification' + | 'usage_notification_event' + | 'usage_notification_ringtone' + | 'usage_notification_communication_request' + | 'usage_notification_communication_instant' + | 'usage_notification_communication_delayed' + | 'usage_unknown' + | 'usage_voice_communication' + | 'usage_voice_communication_signalling'; + +type audioAttributeContentType = + | 'content_type_movie' + | 'content_type_music' + | 'content_type_sonification' + | 'content_type_speech' + | 'content_type_unknown'; + +type audioAttributeLegacyStreamType = + | 'stream_voice_call' + | 'stream_system' + | 'stream_ring' + | 'stream_music' + | 'stream_alarm' + | 'stream_notification'; + +type focusGainType = + | 'audiofocus_gain' + | 'audiofocus_gain_transient' + | 'audiofocus_gain_transient_exclusive' + | 'audiofocus_gain_transient_may_duck'; + +export type AudioAttributeType = { + allowedCapturePolicy?: + | 'allow_capture_by_all' + | 'allow_capture_by_system' + | 'allow_capture_by_none'; + contentType?: audioAttributeContentType; + flag?: 'flag_hw_av_sync' | 'flag_audibility_enforced'; + hapticChannelsMuted?: boolean; + isContentSpatialized?: boolean; + legacyStreamType?: audioAttributeLegacyStreamType; + spatializationBehavior?: + | 'spatialization_behavior_auto' + | 'spatialization_behavior_never'; + usage?: audioAttributeUsageType; +}; + +interface BaseRequestAudioFocusOptions { + [key: string]: string | boolean | number | AudioAttributeType | undefined; +} + +export interface RequestAudioFocusOptions extends BaseRequestAudioFocusOptions { + acceptsDelayedFocusGain?: boolean; + audioAttributes?: AudioAttributeType; + focusGain?: focusGainType; + pauseWhenDucked?: boolean; +} From 155706b41fe011a0cfbf03eaadfea06062b34224 Mon Sep 17 00:00:00 2001 From: michal Date: Tue, 20 May 2025 10:26:30 +0200 Subject: [PATCH 2/8] feat: options for audio focus in android --- .../com/swmansion/audioapi/AudioAPIModule.kt | 7 +- .../audioapi/system/AudioFocusListener.kt | 20 +- .../audioapi/system/MediaSessionManager.kt | 212 +++++++++++------- .../src/specs/NativeAudioAPIModule.ts | 2 +- .../src/system/types.ts | 9 - 5 files changed, 146 insertions(+), 104 deletions(-) diff --git a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt index 7355463c9..a56589645 100644 --- a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt +++ b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt @@ -1,5 +1,7 @@ package com.swmansion.audioapi +import android.os.Build +import androidx.annotation.RequiresApi import com.facebook.jni.HybridData import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext @@ -92,8 +94,9 @@ class AudioAPIModule( promise!!.resolve(res) } - override fun requestAudioFocus(request: ReadableMap) { - MediaSessionManager.requestAudioFocus(request) + @RequiresApi(Build.VERSION_CODES.O) + override fun requestAudioFocus(options: ReadableMap) { + MediaSessionManager.requestAudioFocus(options) } override fun abandonAudioFocus() { diff --git a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/AudioFocusListener.kt b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/AudioFocusListener.kt index ec4a11a49..b502d91b1 100644 --- a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/AudioFocusListener.kt +++ b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/AudioFocusListener.kt @@ -1,6 +1,5 @@ package com.swmansion.audioapi.system -import android.media.AudioAttributes import android.media.AudioFocusRequest import android.media.AudioManager import android.os.Build @@ -60,24 +59,11 @@ class AudioFocusListener( } } - fun requestAudioFocus( - focusRequestBuilder: AudioFocusRequest.Builder, - audioAttributesBuilder: AudioAttributes.Builder, - legacyStreamType: Int, - focusGain: Int, - ): Int = + fun requestAudioFocus(focusRequest: AudioFocusRequest): Int? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val focusRequest = - focusRequestBuilder - .setFocusGain(focusGain) - .setAudioAttributes( - audioAttributesBuilder - .setLegacyStreamType(legacyStreamType) - .build(), - ).build() - audioManager.requestAudioFocus(focusRequest) + audioManager.get()?.requestAudioFocus(focusRequest) } else { - audioManager.requestAudioFocus(this, legacyStreamType, focusGain) + audioManager.get()?.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN) } fun abandonAudioFocus() { diff --git a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt index 9a75d8caa..6ab23018d 100644 --- a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt +++ b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt @@ -161,90 +161,152 @@ object MediaSessionManager { return checkRecordingPermissions() } - fun requestAudioFocus(request: ReadableMap) { - val afbd: AudioFocusRequest.Builder = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) - var focusGain: Int = AudioManager.AUDIOFOCUS_GAIN - val legacyStreamType: Int = AudioManager.STREAM_MUSIC - val aabd: AudioAttributes.Builder = AudioAttributes.Builder() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - if (request.hasKey("focusGain")) { - when (request.getString("focusGain")) { - "audiofocus_gain" -> focusGain = AudioManager.AUDIOFOCUS_GAIN - "audiofocus_gain_transient" -> focusGain = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT - "audiofocus_gain_transient_exclusive" -> - focusGain = - AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE - - "audiofocus_gain_transient_may_duck" -> - focusGain = - AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK + fun parseAudioFocusOptionMap(request: ReadableMap): Map { + val audioFocusOptions = HashMap() + if (request.hasKey("focusGain")) { + when (request.getString("focusGain")) { + "audiofocus_gain" -> audioFocusOptions["focusGain"] = AudioManager.AUDIOFOCUS_GAIN + "audiofocus_gain_transient" -> audioFocusOptions["focusGain"] = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT + "audiofocus_gain_transient_exclusive" -> + audioFocusOptions["focusGain"] = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE + "audiofocus_gain_transient_may_duck" -> AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK + } + } + if (request.hasKey("acceptsDelayedFocusGain")) { + when (request.getBoolean("acceptsDelayedFocusGain")) { + true -> audioFocusOptions["acceptsDelayedFocusGain"] = 1 + false -> audioFocusOptions["acceptsDelayedFocusGain"] = 0 + } + } + if (request.hasKey("pauseWhenDucked")) { + audioFocusOptions["pauseWhenDucked"] = if (request.getBoolean("pauseWhenDucked")) 1 else 0 + } + if (request.hasKey("audioAttributes")) { + val values: ReadableMap? = request.getMap("audioAttributes") + if (values?.hasKey("allowedCapturePolicy") == true) { + when (values.getString("allowedCapturePolicy")) { + "allow_capture_by_all" -> + audioFocusOptions["allowedCapturePolicy"] = + AudioAttributes.ALLOW_CAPTURE_BY_ALL + + "allow_capture_by_system" -> + audioFocusOptions["allowedCapturePolicy"] = + AudioAttributes.ALLOW_CAPTURE_BY_SYSTEM + + "allow_capture_by_none" -> + audioFocusOptions["allowedCapturePolicy"] = + AudioAttributes.ALLOW_CAPTURE_BY_NONE } } - if (request.hasKey("pauseWhenDucked")) { - when (request.getBoolean("pauseWhenDucked")) { - true -> afbd.setWillPauseWhenDucked(true) - false -> afbd.setWillPauseWhenDucked(false) + if (values?.hasKey("contentType") == true) { + when (values.getString("contentType")) { + "content_type_movie" -> + audioFocusOptions["contentType"] = + AudioAttributes.CONTENT_TYPE_MOVIE + + "content_type_music" -> + audioFocusOptions["contentType"] = + AudioAttributes.CONTENT_TYPE_MOVIE + + "content_type_music" -> + audioFocusOptions["contentType"] = + AudioAttributes.CONTENT_TYPE_MUSIC + + "content_type_speech" -> + audioFocusOptions["contentType"] = + AudioAttributes.CONTENT_TYPE_SPEECH + + "content_type_unknown" -> + audioFocusOptions["contentType"] = + AudioAttributes.CONTENT_TYPE_UNKNOWN } } - if (request.hasKey("acceptsDelayedFocusGain")) { - when (request.getBoolean("acceptsDelayedFocusGain")) { - true -> afbd.setAcceptsDelayedFocusGain(true) - false -> afbd.setAcceptsDelayedFocusGain(false) + if (values?.hasKey("flag") == true) { + when (values.getString("flag")) { + "flag_hw_av_sync" -> audioFocusOptions["flag"] = AudioAttributes.FLAG_HW_AV_SYNC + "flag_audibility_enforced" -> + audioFocusOptions["flag"] = + AudioAttributes.FLAG_AUDIBILITY_ENFORCED } } - if (request.hasKey("audioAttributes")) { - val audioAttributesMap = request.getMap("audioAttributes") - if (audioAttributesMap != null) { - if (audioAttributesMap.hasKey("allowedCapturePolicy")) { - when (audioAttributesMap.getString("allowedCapturePolicy")) { - "allow_capture_by_all" -> aabd.setAllowedCapturePolicy(AudioAttributes.ALLOW_CAPTURE_BY_ALL) - "allow_capture_by_system" -> aabd.setAllowedCapturePolicy(AudioAttributes.ALLOW_CAPTURE_BY_SYSTEM) - "allow_capture_by_none" -> aabd.setAllowedCapturePolicy(AudioAttributes.ALLOW_CAPTURE_BY_NONE) - } - } - if (audioAttributesMap.hasKey("contentType")) { - when (audioAttributesMap.getString("contentType")) { - "content_type_sonification" -> aabd.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - "content_type_music" -> aabd.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) - "content_type_movie" -> aabd.setContentType(AudioAttributes.CONTENT_TYPE_MOVIE) - "content_type_speech" -> aabd.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) - "content_type_unknown" -> aabd.setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN) - } - } - if (audioAttributesMap.hasKey("flag")) { - when (audioAttributesMap.getString("flag")) { - "flag_hw_av_sync" -> aabd.setFlags(AudioAttributes.FLAG_HW_AV_SYNC) - "flag_audibility_enforced" -> aabd.setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED) - } - } - if (audioAttributesMap.hasKey("hapticChannelsMuted")) { - when (audioAttributesMap.getBoolean("hapticChannelsMuted")) { - true -> aabd.setHapticChannelsMuted(true) - false -> aabd.setHapticChannelsMuted(false) - } - } - if (audioAttributesMap.hasKey("isContentSpatialized")) { - when (audioAttributesMap.getBoolean("isContentSpatialized")) { - true -> aabd.setIsContentSpatialized(true) - false -> aabd.setIsContentSpatialized(false) - } - } - if (audioAttributesMap.hasKey("legacyStreamType")) { - when (audioAttributesMap.getString("legacyStreamType")) { - "stream_voice_call" -> aabd.setLegacyStreamType(AudioManager.STREAM_VOICE_CALL) - "stream_system" -> aabd.setLegacyStreamType(AudioManager.STREAM_SYSTEM) - "stream_ring" -> aabd.setLegacyStreamType(AudioManager.STREAM_RING) - "stream_music" -> aabd.setLegacyStreamType(AudioManager.STREAM_MUSIC) - "stream_alarm" -> aabd.setLegacyStreamType(AudioManager.STREAM_ALARM) - "stream_notification" -> aabd.setLegacyStreamType(AudioManager.STREAM_NOTIFICATION) - } - } - if (audioAttributesMap.hasKey("spatializationBehavior")) { - } + if (values?.hasKey("hapticChannelsMuted") == true) { + audioFocusOptions["hapticChannelsMuted"] = if (values.getBoolean("hapticChannelsMuted")) 1 else 0 + } + if (values?.hasKey("isContentSpatialized") == true) { + audioFocusOptions["isContentSpatialized"] = if (values.getBoolean("isContentSpatialized")) 1 else 0 + } + if (values?.hasKey("spatializationBehavior") == true) { + when (values.getString("spatializationBehavior")) { + "spatialization_behavior_auto" -> + audioFocusOptions["spatializationBehavior"] = + AudioAttributes.SPATIALIZATION_BEHAVIOR_AUTO + + "spatialization_behavior_never" -> + audioFocusOptions["spatializationBehavior"] = + AudioAttributes.SPATIALIZATION_BEHAVIOR_NEVER + } + } + if (values?.hasKey("usage") == true) { + when (values.getString("usage")) { + "usage_alarm" -> audioFocusOptions["usage"] = AudioAttributes.USAGE_ALARM + "usage_assistance_accessibility" -> audioFocusOptions["usage"] = AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY + "usage_assistance_navigation_guidance" -> audioFocusOptions["usage"] = AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE + "usage_assistance_sonification" -> + audioFocusOptions["usage"] = AudioAttributes.USAGE_ASSISTANCE_SONIFICATION + "usage_assistant" -> audioFocusOptions["usage"] = AudioAttributes.USAGE_ASSISTANT + "usage_game" -> audioFocusOptions["usage"] = AudioAttributes.USAGE_GAME + "usage_media" -> audioFocusOptions["usage"] = AudioAttributes.USAGE_MEDIA + "usage_notification" -> audioFocusOptions["usage"] = AudioAttributes.USAGE_NOTIFICATION + "usage_notification_event" -> audioFocusOptions["usage"] = AudioAttributes.USAGE_NOTIFICATION_EVENT + "usage_notification_ringtone" -> audioFocusOptions["usage"] = AudioAttributes.USAGE_NOTIFICATION_RINGTONE + "usage_notification_communication_request" -> + audioFocusOptions["usage"] = + AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST + "usage_notification_communication_instant" -> + audioFocusOptions["usage"] = + AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT + "usage_notification_communication_delayed" -> + audioFocusOptions["usage"] = + AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED + "usage_unknown" -> audioFocusOptions["usage"] = AudioAttributes.USAGE_UNKNOWN + "usage_voice_communication" -> audioFocusOptions["usage"] = AudioAttributes.USAGE_VOICE_COMMUNICATION + "usage_voice_communication_signalling" -> audioFocusOptions["usage"] = AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING } - audioFocusListener.requestAudioFocus(afbd, aabd, legacyStreamType, focusGain) } } + + return audioFocusOptions + } + + @RequiresApi(Build.VERSION_CODES.O) + fun requestAudioFocus(options: ReadableMap) { + val parsedRequest = parseAudioFocusOptionMap(options) + val afbd = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) + val aabd = AudioAttributes.Builder() + if (parsedRequest.containsKey("pauseWhenDucked")) { + afbd.setWillPauseWhenDucked(parsedRequest["pauseWhenDucked"] == 1) + } + parsedRequest["focusGain"]?.let { afbd.setFocusGain(it) } + if (parsedRequest.containsKey("acceptsDelayedFocusGain")) { + afbd.setAcceptsDelayedFocusGain(parsedRequest["acceptsDelayedFocusGain"] == 1) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + parsedRequest["allowedCapturePolicy"]?.let { aabd.setAllowedCapturePolicy(it) } + parsedRequest["contentType"]?.let { aabd.setAllowedCapturePolicy(it) } + if (parsedRequest.containsKey("hapticChannelsMuted")) { + aabd.setHapticChannelsMuted(parsedRequest["hapticChannelsMuted"] == 1) + } + } + parsedRequest["flag"]?.let { aabd.setFlags(it) } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2) { + parsedRequest["spatializationBehavior"]?.let { aabd.setSpatializationBehavior(it) } + if (parsedRequest.containsKey("isContentSpatialized")) { + aabd.setIsContentSpatialized(parsedRequest["isContentSpatialized"] == 1) + } + } + parsedRequest["usage"]?.let { aabd.setUsage(it) } + afbd.setAudioAttributes(aabd.build()) + audioFocusListener.requestAudioFocus(afbd.build()) } fun abandonAudioFocus() { diff --git a/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts b/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts index 71dd4d2d9..4421ca729 100644 --- a/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts +++ b/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts @@ -17,7 +17,7 @@ interface Spec extends TurboModule { options: Array ): void; getDevicePreferredSampleRate(): number; - requestAudioFocus(request: { + requestAudioFocus(options: { [key: string]: string | boolean | number | AudioAttributeType | undefined; }): void; abandonAudioFocus(): void; diff --git a/packages/react-native-audio-api/src/system/types.ts b/packages/react-native-audio-api/src/system/types.ts index ff9a0ee36..44b6aff21 100644 --- a/packages/react-native-audio-api/src/system/types.ts +++ b/packages/react-native-audio-api/src/system/types.ts @@ -78,14 +78,6 @@ type audioAttributeContentType = | 'content_type_speech' | 'content_type_unknown'; -type audioAttributeLegacyStreamType = - | 'stream_voice_call' - | 'stream_system' - | 'stream_ring' - | 'stream_music' - | 'stream_alarm' - | 'stream_notification'; - type focusGainType = | 'audiofocus_gain' | 'audiofocus_gain_transient' @@ -101,7 +93,6 @@ export type AudioAttributeType = { flag?: 'flag_hw_av_sync' | 'flag_audibility_enforced'; hapticChannelsMuted?: boolean; isContentSpatialized?: boolean; - legacyStreamType?: audioAttributeLegacyStreamType; spatializationBehavior?: | 'spatialization_behavior_auto' | 'spatialization_behavior_never'; From 47ef03417c5e14acedb927bd2d4fe11812494db1 Mon Sep 17 00:00:00 2001 From: michal Date: Tue, 20 May 2025 10:30:23 +0200 Subject: [PATCH 3/8] feat: aligned example app with new impl --- apps/common-app/src/examples/AudioFile/AudioFile.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/common-app/src/examples/AudioFile/AudioFile.tsx b/apps/common-app/src/examples/AudioFile/AudioFile.tsx index 2b413347b..b85229d65 100644 --- a/apps/common-app/src/examples/AudioFile/AudioFile.tsx +++ b/apps/common-app/src/examples/AudioFile/AudioFile.tsx @@ -68,7 +68,7 @@ const AudioFile: FC = () => { state: 'state_playing', }); - AudioManager.observeAudioInterruptions(true); + AudioManager.requestAudioFocus({}); bufferSourceRef.current = audioContextRef.current.createBufferSource({ pitchCorrection: true, @@ -152,7 +152,7 @@ const AudioFile: FC = () => { } ); - AudioManager.observeAudioInterruptions(true); + AudioManager.requestAudioFocus({}); fetchAudioBuffer(); From 67f6550ba6ae74741d6295c01e45bf27905d82a6 Mon Sep 17 00:00:00 2001 From: michal Date: Tue, 20 May 2025 13:56:48 +0200 Subject: [PATCH 4/8] feat: added functions for compatibility with ios --- .../src/examples/AudioFile/AudioFile.tsx | 1 + .../ios/audioapi/ios/AudioAPIModule.mm | 14 ++++++++++---- .../src/system/AudioManager.ts | 4 ++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/apps/common-app/src/examples/AudioFile/AudioFile.tsx b/apps/common-app/src/examples/AudioFile/AudioFile.tsx index b85229d65..fadae599e 100644 --- a/apps/common-app/src/examples/AudioFile/AudioFile.tsx +++ b/apps/common-app/src/examples/AudioFile/AudioFile.tsx @@ -69,6 +69,7 @@ const AudioFile: FC = () => { }); AudioManager.requestAudioFocus({}); + // AudioManager.abandonAudioFocus(); bufferSourceRef.current = audioContextRef.current.createBufferSource({ pitchCorrection: true, diff --git a/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm b/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm index 084ab18bf..fafeb3b05 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm +++ b/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm @@ -105,14 +105,20 @@ - (void)invalidate return [self.audioSessionManager getDevicePreferredSampleRate]; } -RCT_EXPORT_METHOD(observeAudioInterruptions : (BOOL)enabled) +RCT_EXPORT_METHOD(observeVolumeChanges : (BOOL)enabled) { - [self.notificationManager observeAudioInterruptions:enabled]; + [self.notificationManager observeVolumeChanges:(BOOL)enabled]; } -RCT_EXPORT_METHOD(observeVolumeChanges : (BOOL)enabled) +// android-only function +RCT_EXPORT_METHOD(requestAudioFocus : (NSDictionary *)options) { - [self.notificationManager observeVolumeChanges:(BOOL)enabled]; + [self.notificationManager observeAudioInterruptions:true]; +} + +RCT_EXPORT_METHOD(abandonAudioFocus) +{ + [self.notificationManager observeAudioInterruptions:false]; } RCT_EXPORT_METHOD( diff --git a/packages/react-native-audio-api/src/system/AudioManager.ts b/packages/react-native-audio-api/src/system/AudioManager.ts index 78c44f9e4..9486088f2 100644 --- a/packages/react-native-audio-api/src/system/AudioManager.ts +++ b/packages/react-native-audio-api/src/system/AudioManager.ts @@ -48,6 +48,10 @@ class AudioManager { NativeAudioAPIModule!.requestAudioFocus(request); } + abandonAudioFocus() { + NativeAudioAPIModule!.abandonAudioFocus(); + } + observeVolumeChanges(enabled: boolean) { NativeAudioAPIModule!.observeVolumeChanges(enabled); } From 24c903c8d385b8e4648b3ebe3e2344c9cfb46852 Mon Sep 17 00:00:00 2001 From: michal Date: Tue, 20 May 2025 15:03:27 +0200 Subject: [PATCH 5/8] feat: added possibility of not observing audio interruptions --- .../com/swmansion/audioapi/AudioAPIModule.kt | 7 ++++-- .../audioapi/system/AudioFocusListener.kt | 9 ++++++-- .../audioapi/system/MediaSessionManager.kt | 22 +++++++++++++++---- .../ios/audioapi/ios/AudioAPIModule.mm | 6 ++--- .../src/specs/NativeAudioAPIModule.ts | 9 +++++--- .../src/system/AudioManager.ts | 7 ++++-- 6 files changed, 44 insertions(+), 16 deletions(-) diff --git a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt index a56589645..024a60a21 100644 --- a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt +++ b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt @@ -95,8 +95,11 @@ class AudioAPIModule( } @RequiresApi(Build.VERSION_CODES.O) - override fun requestAudioFocus(options: ReadableMap) { - MediaSessionManager.requestAudioFocus(options) + override fun requestAudioFocus( + options: ReadableMap, + observeAudioInterruptions: Boolean, + ) { + MediaSessionManager.requestAudioFocus(options, observeAudioInterruptions) } override fun abandonAudioFocus() { diff --git a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/AudioFocusListener.kt b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/AudioFocusListener.kt index b502d91b1..62834981f 100644 --- a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/AudioFocusListener.kt +++ b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/AudioFocusListener.kt @@ -59,9 +59,14 @@ class AudioFocusListener( } } - fun requestAudioFocus(focusRequest: AudioFocusRequest): Int? = + fun requestAudioFocus( + focusRequest: AudioFocusRequest.Builder, + observeAudioInterruptions: Boolean, + ): Int? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - audioManager.get()?.requestAudioFocus(focusRequest) + if (observeAudioInterruptions) focusRequest.setOnAudioFocusChangeListener(this) + this.focusRequest = focusRequest.build() + audioManager.get()?.requestAudioFocus(this.focusRequest!!) } else { audioManager.get()?.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN) } diff --git a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt index 6ab23018d..770e0a098 100644 --- a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt +++ b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt @@ -279,16 +279,23 @@ object MediaSessionManager { } @RequiresApi(Build.VERSION_CODES.O) - fun requestAudioFocus(options: ReadableMap) { + fun requestAudioFocus( + options: ReadableMap, + observeAudioInterruptions: Boolean, + ) { val parsedRequest = parseAudioFocusOptionMap(options) val afbd = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) val aabd = AudioAttributes.Builder() + var pauseWhenDucked = false + var acceptsDelayedFocusGain = false if (parsedRequest.containsKey("pauseWhenDucked")) { - afbd.setWillPauseWhenDucked(parsedRequest["pauseWhenDucked"] == 1) + pauseWhenDucked = parsedRequest["pauseWhenDucked"] == 1 + afbd.setWillPauseWhenDucked(pauseWhenDucked) } parsedRequest["focusGain"]?.let { afbd.setFocusGain(it) } if (parsedRequest.containsKey("acceptsDelayedFocusGain")) { - afbd.setAcceptsDelayedFocusGain(parsedRequest["acceptsDelayedFocusGain"] == 1) + acceptsDelayedFocusGain = parsedRequest["acceptsDelayedFocusGain"] == 1 + afbd.setAcceptsDelayedFocusGain(acceptsDelayedFocusGain) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { parsedRequest["allowedCapturePolicy"]?.let { aabd.setAllowedCapturePolicy(it) } @@ -306,7 +313,14 @@ object MediaSessionManager { } parsedRequest["usage"]?.let { aabd.setUsage(it) } afbd.setAudioAttributes(aabd.build()) - audioFocusListener.requestAudioFocus(afbd.build()) + // according to docs: OnAudioFocusChangeListener is only required + // if you also specify willPauseWhenDucked(true) or setAcceptsDelayedFocusGain(true) in the request. + if ((pauseWhenDucked || acceptsDelayedFocusGain) && !observeAudioInterruptions) { + throw IllegalArgumentException( + "observeAudioInterruptions must be true when pauseWhenDucked or acceptsDelayedFocusGain is set to true", + ) + } + audioFocusListener.requestAudioFocus(afbd, observeAudioInterruptions) } fun abandonAudioFocus() { diff --git a/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm b/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm index fafeb3b05..f344a72b6 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm +++ b/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm @@ -110,10 +110,10 @@ - (void)invalidate [self.notificationManager observeVolumeChanges:(BOOL)enabled]; } -// android-only function -RCT_EXPORT_METHOD(requestAudioFocus : (NSDictionary *)options) +// android-only support for options +RCT_EXPORT_METHOD(requestAudioFocus : (NSDictionary *)options) : (BOOL)observeAudioInterruptions { - [self.notificationManager observeAudioInterruptions:true]; + [self.notificationManager observeAudioInterruptions:observeAudioInterruptions]; } RCT_EXPORT_METHOD(abandonAudioFocus) diff --git a/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts b/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts index 4421ca729..409870497 100644 --- a/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts +++ b/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts @@ -17,9 +17,12 @@ interface Spec extends TurboModule { options: Array ): void; getDevicePreferredSampleRate(): number; - requestAudioFocus(options: { - [key: string]: string | boolean | number | AudioAttributeType | undefined; - }): void; + requestAudioFocus( + options: { + [key: string]: string | boolean | number | AudioAttributeType | undefined; + }, + observeAudioInterruptions: boolean + ): void; abandonAudioFocus(): void; observeVolumeChanges(enabled: boolean): void; requestRecordingPermissions(): Promise; diff --git a/packages/react-native-audio-api/src/system/AudioManager.ts b/packages/react-native-audio-api/src/system/AudioManager.ts index 9486088f2..c007c926a 100644 --- a/packages/react-native-audio-api/src/system/AudioManager.ts +++ b/packages/react-native-audio-api/src/system/AudioManager.ts @@ -44,8 +44,11 @@ class AudioManager { return NativeAudioAPIModule!.getDevicePreferredSampleRate(); } - requestAudioFocus(request: RequestAudioFocusOptions) { - NativeAudioAPIModule!.requestAudioFocus(request); + requestAudioFocus( + request: RequestAudioFocusOptions, + observeAudioInterruption = true + ) { + NativeAudioAPIModule!.requestAudioFocus(request, observeAudioInterruption); } abandonAudioFocus() { From 1e889ca8d5edd6c343b4267c8d6e9397f3b111ca Mon Sep 17 00:00:00 2001 From: michal Date: Tue, 20 May 2025 15:19:52 +0200 Subject: [PATCH 6/8] feat: improved android versioning --- .../audioapi/system/MediaSessionManager.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt index 770e0a098..b83861efe 100644 --- a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt +++ b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt @@ -161,7 +161,7 @@ object MediaSessionManager { return checkRecordingPermissions() } - fun parseAudioFocusOptionMap(request: ReadableMap): Map { + private fun parseAudioFocusOptionMap(request: ReadableMap): Map { val audioFocusOptions = HashMap() if (request.hasKey("focusGain")) { when (request.getString("focusGain")) { @@ -183,7 +183,7 @@ object MediaSessionManager { } if (request.hasKey("audioAttributes")) { val values: ReadableMap? = request.getMap("audioAttributes") - if (values?.hasKey("allowedCapturePolicy") == true) { + if (values?.hasKey("allowedCapturePolicy") == true && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { when (values.getString("allowedCapturePolicy")) { "allow_capture_by_all" -> audioFocusOptions["allowedCapturePolicy"] = @@ -208,9 +208,9 @@ object MediaSessionManager { audioFocusOptions["contentType"] = AudioAttributes.CONTENT_TYPE_MOVIE - "content_type_music" -> + "content_type_sonification" -> audioFocusOptions["contentType"] = - AudioAttributes.CONTENT_TYPE_MUSIC + AudioAttributes.CONTENT_TYPE_SONIFICATION "content_type_speech" -> audioFocusOptions["contentType"] = @@ -235,7 +235,7 @@ object MediaSessionManager { if (values?.hasKey("isContentSpatialized") == true) { audioFocusOptions["isContentSpatialized"] = if (values.getBoolean("isContentSpatialized")) 1 else 0 } - if (values?.hasKey("spatializationBehavior") == true) { + if (values?.hasKey("spatializationBehavior") == true && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2) { when (values.getString("spatializationBehavior")) { "spatialization_behavior_auto" -> audioFocusOptions["spatializationBehavior"] = @@ -253,7 +253,9 @@ object MediaSessionManager { "usage_assistance_navigation_guidance" -> audioFocusOptions["usage"] = AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE "usage_assistance_sonification" -> audioFocusOptions["usage"] = AudioAttributes.USAGE_ASSISTANCE_SONIFICATION - "usage_assistant" -> audioFocusOptions["usage"] = AudioAttributes.USAGE_ASSISTANT + "usage_assistant" -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) audioFocusOptions["usage"] = AudioAttributes.USAGE_ASSISTANT + } "usage_game" -> audioFocusOptions["usage"] = AudioAttributes.USAGE_GAME "usage_media" -> audioFocusOptions["usage"] = AudioAttributes.USAGE_MEDIA "usage_notification" -> audioFocusOptions["usage"] = AudioAttributes.USAGE_NOTIFICATION From c4dff2176782fb92e0be34cde5d3ab6e11a5464a Mon Sep 17 00:00:00 2001 From: michal Date: Wed, 21 May 2025 14:39:02 +0200 Subject: [PATCH 7/8] fix: brought back observe audio interruptions fun --- apps/common-app/src/examples/AudioFile/AudioFile.tsx | 5 ++--- .../main/java/com/swmansion/audioapi/AudioAPIModule.kt | 5 +++++ .../com/swmansion/audioapi/system/MediaSessionManager.kt | 9 +++++++++ .../ios/audioapi/ios/AudioAPIModule.mm | 7 ++++++- .../src/specs/NativeAudioAPIModule.ts | 1 + .../react-native-audio-api/src/system/AudioManager.ts | 4 ++++ 6 files changed, 27 insertions(+), 4 deletions(-) diff --git a/apps/common-app/src/examples/AudioFile/AudioFile.tsx b/apps/common-app/src/examples/AudioFile/AudioFile.tsx index fadae599e..2b413347b 100644 --- a/apps/common-app/src/examples/AudioFile/AudioFile.tsx +++ b/apps/common-app/src/examples/AudioFile/AudioFile.tsx @@ -68,8 +68,7 @@ const AudioFile: FC = () => { state: 'state_playing', }); - AudioManager.requestAudioFocus({}); - // AudioManager.abandonAudioFocus(); + AudioManager.observeAudioInterruptions(true); bufferSourceRef.current = audioContextRef.current.createBufferSource({ pitchCorrection: true, @@ -153,7 +152,7 @@ const AudioFile: FC = () => { } ); - AudioManager.requestAudioFocus({}); + AudioManager.observeAudioInterruptions(true); fetchAudioBuffer(); diff --git a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt index 024a60a21..c17ca69ed 100644 --- a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt +++ b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt @@ -80,6 +80,11 @@ class AudioAPIModule( override fun getDevicePreferredSampleRate(): Double = MediaSessionManager.getDevicePreferredSampleRate() + @RequiresApi(Build.VERSION_CODES.O) + override fun observeAudioInterruptions(enabled: Boolean) { + MediaSessionManager.observeAudioInterruptions(enabled) + } + override fun observeVolumeChanges(enabled: Boolean) { MediaSessionManager.observeVolumeChanges(enabled) } diff --git a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt index b83861efe..50cbeb6b2 100644 --- a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt +++ b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt @@ -139,6 +139,15 @@ object MediaSessionManager { return sampleRate.toDouble() } + @RequiresApi(Build.VERSION_CODES.O) + fun observeAudioInterruptions(observe: Boolean) { + if (observe) { + audioFocusListener.requestAudioFocus(AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN), true) + } else { + audioFocusListener.abandonAudioFocus() + } + } + fun observeVolumeChanges(observe: Boolean) { if (observe) { ContextCompat.registerReceiver( diff --git a/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm b/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm index f344a72b6..98084e470 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm +++ b/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm @@ -107,7 +107,12 @@ - (void)invalidate RCT_EXPORT_METHOD(observeVolumeChanges : (BOOL)enabled) { - [self.notificationManager observeVolumeChanges:(BOOL)enabled]; + [self.notificationManager observeVolumeChanges:enabled]; +} + +RCT_EXPORT_METHOD(observeAudioInterruptions : (BOOL)enabled) +{ + [self.notificationManager observeAudioInterruptions:enabled]; } // android-only support for options diff --git a/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts b/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts index 409870497..83f0d70ae 100644 --- a/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts +++ b/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts @@ -17,6 +17,7 @@ interface Spec extends TurboModule { options: Array ): void; getDevicePreferredSampleRate(): number; + observeAudioInterruptions(enabled: boolean): void; requestAudioFocus( options: { [key: string]: string | boolean | number | AudioAttributeType | undefined; diff --git a/packages/react-native-audio-api/src/system/AudioManager.ts b/packages/react-native-audio-api/src/system/AudioManager.ts index c007c926a..e44394cbe 100644 --- a/packages/react-native-audio-api/src/system/AudioManager.ts +++ b/packages/react-native-audio-api/src/system/AudioManager.ts @@ -44,6 +44,10 @@ class AudioManager { return NativeAudioAPIModule!.getDevicePreferredSampleRate(); } + observeAudioInterruptions(enabled: boolean) { + NativeAudioAPIModule!.observeAudioInterruptions(enabled); + } + requestAudioFocus( request: RequestAudioFocusOptions, observeAudioInterruption = true From 825ca84432ad7e2dca37f4804d749ad1da265c9d Mon Sep 17 00:00:00 2001 From: michal Date: Tue, 27 May 2025 16:11:27 +0200 Subject: [PATCH 8/8] feat: small parameter refactor --- .../com/swmansion/audioapi/AudioAPIModule.kt | 4 +- .../audioapi/system/MediaSessionManager.kt | 22 ++++++- .../ios/audioapi/ios/AudioAPIModule.mm | 2 +- .../src/specs/NativeAudioAPIModule.ts | 6 +- .../src/system/AudioManager.ts | 8 +-- .../src/system/types.ts | 62 +++++++++---------- 6 files changed, 61 insertions(+), 43 deletions(-) diff --git a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt index c17ca69ed..319d715be 100644 --- a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt +++ b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt @@ -101,10 +101,10 @@ class AudioAPIModule( @RequiresApi(Build.VERSION_CODES.O) override fun requestAudioFocus( - options: ReadableMap, observeAudioInterruptions: Boolean, + options: ReadableMap?, ) { - MediaSessionManager.requestAudioFocus(options, observeAudioInterruptions) + MediaSessionManager.requestAudioFocus(observeAudioInterruptions, options) } override fun abandonAudioFocus() { diff --git a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt index 50cbeb6b2..9e98b264f 100644 --- a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt +++ b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt @@ -170,8 +170,11 @@ object MediaSessionManager { return checkRecordingPermissions() } - private fun parseAudioFocusOptionMap(request: ReadableMap): Map { + private fun parseAudioFocusOptionMap(request: ReadableMap?): Map { val audioFocusOptions = HashMap() + if (request == null) { + return audioFocusOptions + } if (request.hasKey("focusGain")) { when (request.getString("focusGain")) { "audiofocus_gain" -> audioFocusOptions["focusGain"] = AudioManager.AUDIOFOCUS_GAIN @@ -291,8 +294,8 @@ object MediaSessionManager { @RequiresApi(Build.VERSION_CODES.O) fun requestAudioFocus( - options: ReadableMap, observeAudioInterruptions: Boolean, + options: ReadableMap?, ) { val parsedRequest = parseAudioFocusOptionMap(options) val afbd = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) @@ -349,6 +352,21 @@ object MediaSessionManager { "Denied" } + fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray, + ): String { + if (requestCode == 420) { + return if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + "Granted" + } else { + "Denied" + } + } + return "Undetermined" + } + @RequiresApi(Build.VERSION_CODES.O) private fun createChannel() { val notificationManager = diff --git a/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm b/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm index 98084e470..fe6a867f0 100644 --- a/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm +++ b/packages/react-native-audio-api/ios/audioapi/ios/AudioAPIModule.mm @@ -116,7 +116,7 @@ - (void)invalidate } // android-only support for options -RCT_EXPORT_METHOD(requestAudioFocus : (NSDictionary *)options) : (BOOL)observeAudioInterruptions +RCT_EXPORT_METHOD(requestAudioFocus : (BOOL)observeAudioInterruptions : (NSDictionary *)options) { [self.notificationManager observeAudioInterruptions:observeAudioInterruptions]; } diff --git a/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts b/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts index 83f0d70ae..f5b8d54a9 100644 --- a/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts +++ b/packages/react-native-audio-api/src/specs/NativeAudioAPIModule.ts @@ -19,10 +19,10 @@ interface Spec extends TurboModule { getDevicePreferredSampleRate(): number; observeAudioInterruptions(enabled: boolean): void; requestAudioFocus( - options: { + observeAudioInterruptions: boolean, + options?: { [key: string]: string | boolean | number | AudioAttributeType | undefined; - }, - observeAudioInterruptions: boolean + } ): void; abandonAudioFocus(): void; observeVolumeChanges(enabled: boolean): void; diff --git a/packages/react-native-audio-api/src/system/AudioManager.ts b/packages/react-native-audio-api/src/system/AudioManager.ts index e44394cbe..69cfa29ec 100644 --- a/packages/react-native-audio-api/src/system/AudioManager.ts +++ b/packages/react-native-audio-api/src/system/AudioManager.ts @@ -2,7 +2,7 @@ import { SessionOptions, LockScreenInfo, PermissionStatus, - RequestAudioFocusOptions, + AudioFocusOptions, } from './types'; import { SystemEventName, SystemEventCallback } from '../events/types'; import { NativeAudioAPIModule } from '../specs'; @@ -49,10 +49,10 @@ class AudioManager { } requestAudioFocus( - request: RequestAudioFocusOptions, - observeAudioInterruption = true + observeAudioInterruption = true, + options?: AudioFocusOptions ) { - NativeAudioAPIModule!.requestAudioFocus(request, observeAudioInterruption); + NativeAudioAPIModule!.requestAudioFocus(observeAudioInterruption, options); } abandonAudioFocus() { diff --git a/packages/react-native-audio-api/src/system/types.ts b/packages/react-native-audio-api/src/system/types.ts index 44b6aff21..bd713ce73 100644 --- a/packages/react-native-audio-api/src/system/types.ts +++ b/packages/react-native-audio-api/src/system/types.ts @@ -27,33 +27,7 @@ export type IOSOption = | 'overrideMutedMicrophoneInterruption' | 'interruptSpokenAudioAndMixWithOthers'; -export interface SessionOptions { - iosMode?: IOSMode; - iosOptions?: IOSOption[]; - iosCategory?: IOSCategory; -} - -export type MediaState = 'state_playing' | 'state_paused'; - -interface BaseLockScreenInfo { - [key: string]: string | boolean | number | undefined; -} - -export interface LockScreenInfo extends BaseLockScreenInfo { - title?: string; - artwork?: string; - artist?: string; - album?: string; - duration?: number; - description?: string; // android only - state?: MediaState; - speed?: number; - elapsedTime?: number; -} - -export type PermissionStatus = 'Undetermined' | 'Denied' | 'Granted'; - -type audioAttributeUsageType = +export type audioAttributeUsageType = | 'usage_alarm' | 'usage_assistance_accessibility' | 'usage_assistance_navigation_guidance' @@ -71,14 +45,14 @@ type audioAttributeUsageType = | 'usage_voice_communication' | 'usage_voice_communication_signalling'; -type audioAttributeContentType = +export type audioAttributeContentType = | 'content_type_movie' | 'content_type_music' | 'content_type_sonification' | 'content_type_speech' | 'content_type_unknown'; -type focusGainType = +export type focusGainType = | 'audiofocus_gain' | 'audiofocus_gain_transient' | 'audiofocus_gain_transient_exclusive' @@ -99,11 +73,37 @@ export type AudioAttributeType = { usage?: audioAttributeUsageType; }; -interface BaseRequestAudioFocusOptions { +export interface SessionOptions { + iosMode?: IOSMode; + iosOptions?: IOSOption[]; + iosCategory?: IOSCategory; +} + +export type MediaState = 'state_playing' | 'state_paused'; + +interface BaseLockScreenInfo { + [key: string]: string | boolean | number | undefined; +} + +export interface LockScreenInfo extends BaseLockScreenInfo { + title?: string; + artwork?: string; + artist?: string; + album?: string; + duration?: number; + description?: string; // android only + state?: MediaState; + speed?: number; + elapsedTime?: number; +} + +export type PermissionStatus = 'Undetermined' | 'Denied' | 'Granted'; + +interface BaseAudioFocusOptions { [key: string]: string | boolean | number | AudioAttributeType | undefined; } -export interface RequestAudioFocusOptions extends BaseRequestAudioFocusOptions { +export interface AudioFocusOptions extends BaseAudioFocusOptions { acceptsDelayedFocusGain?: boolean; audioAttributes?: AudioAttributeType; focusGain?: focusGainType;