diff --git a/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt b/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt index 9d6d869ef..094b0d89a 100644 --- a/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt +++ b/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt @@ -125,6 +125,25 @@ class MusicService : HeadlessJsTaskService() { stopForeground(true) } + private fun getContentTypeFromString(contentTypeString: String?): AudioContentType { + return when(contentTypeString) { + "music" -> AudioContentType.MUSIC + "speech" -> AudioContentType.SPEECH + "sonification" -> AudioContentType.SONIFICATION + "movie" -> AudioContentType.MOVIE + "unknown" -> AudioContentType.UNKNOWN + else -> AudioContentType.MUSIC + } + } + + private fun getAudioUsageFromString(contentTypeString: String?): AudioUsageType { + return when(contentTypeString) { + "music" -> AudioUsageType.MEDIA + "voice_communication" -> AudioUsageType.VOICE_COMMUNICATION + else -> AudioUsageType.MEDIA + } + } + @MainThread fun setupPlayer(playerOptions: Bundle?) { if (this::player.isInitialized) { @@ -144,14 +163,8 @@ class MusicService : HeadlessJsTaskService() { interceptPlayerActionsTriggeredExternally = true, handleAudioBecomingNoisy = true, handleAudioFocus = playerOptions?.getBoolean(AUTO_HANDLE_INTERRUPTIONS) ?: false, - audioContentType = when(playerOptions?.getString(ANDROID_AUDIO_CONTENT_TYPE)) { - "music" -> AudioContentType.MUSIC - "speech" -> AudioContentType.SPEECH - "sonification" -> AudioContentType.SONIFICATION - "movie" -> AudioContentType.MOVIE - "unknown" -> AudioContentType.UNKNOWN - else -> AudioContentType.MUSIC - } + audioUsageType = getAudioUsageFromString(playerOptions?.getString(ANDROID_AUDIO_USAGE_TYPE)), + audioContentType = getContentTypeFromString(playerOptions?.getString(ANDROID_AUDIO_CONTENT_TYPE)) ) val automaticallyUpdateNotificationMetadata = playerOptions?.getBoolean(AUTO_UPDATE_METADATA, true) ?: true @@ -179,6 +192,12 @@ class MusicService : HeadlessJsTaskService() { } } + player.setAudioAttributes(AudioAttributeConfig( + handleAudioFocus = options.getBoolean(AUTO_HANDLE_INTERRUPTIONS), + audioUsageType = getAudioUsageFromString(options.getString(ANDROID_AUDIO_USAGE_TYPE)), + audioContentType = getContentTypeFromString(options?.getString(ANDROID_AUDIO_CONTENT_TYPE)) + )) + ratingType = BundleUtils.getInt(options, "ratingType", RatingCompat.RATING_NONE) player.playerOptions.alwaysPauseOnInterruption = androidOptions?.getBoolean(PAUSE_ON_INTERRUPTION_KEY) ?: false @@ -845,6 +864,7 @@ class MusicService : HeadlessJsTaskService() { const val AUTO_UPDATE_METADATA = "autoUpdateMetadata" const val AUTO_HANDLE_INTERRUPTIONS = "autoHandleInterruptions" const val ANDROID_AUDIO_CONTENT_TYPE = "androidAudioContentType" + const val ANDROID_AUDIO_USAGE_TYPE = "androidAudioUsageType" const val IS_FOCUS_LOSS_PERMANENT_KEY = "permanent" const val IS_PAUSED_KEY = "paused" diff --git a/docs/docs/api/objects/player-options.md b/docs/docs/api/objects/player-options.md index 089a7125b..d7e66269e 100644 --- a/docs/docs/api/objects/player-options.md +++ b/docs/docs/api/objects/player-options.md @@ -15,4 +15,5 @@ All parameters are optional. You also only need to specify the ones you want to | `waitForBuffer` | `boolean` | Indicates whether the player should automatically delay playback in order to minimize stalling. Defaults to `true`. @deprecated This option has been nominated for removal in a future version of RNTP. If you have this set to `true`, you can safely remove this from the options. If you are setting this to `false` and have a reason for that, please post a comment in the following discussion: https://github.com/doublesymmetry/react-native-track-player/pull/1695 and describe why you are doing so. | ✅ | ✅ | | `autoUpdateMetadata` | `boolean` | Indicates whether the player should automatically update now playing metadata data in control center / notification. Defaults to `true`. | ✅ | ✅ | | `autoHandleInterruptions` | `boolean` | Indicates whether the player should automatically handle audio interruptions. Defaults to `false`. | ✅ | ✅ | -| `androidAudioContentType` | `boolean` | The audio content type indicates to the android system how you intend to use audio in your app. With `autoHandleInterruptions: true` and `androidAudioContentType: AndroidAudioContentType.Speech`, the audio will be paused during short interruptions, such as when a message arrives. Otherwise the playback volume is reduced while the notification is playing. Defaults to `AndroidAudioContentType.Music` | ✅ | ❌ | +| `androidAudioUsageType` | `AndroidAudioUsageType` | (Android only) Specifies "why" the source is playing and controls routing, focus, and volume decisions. With `androidAudioUsageType` set to `AndroidAudioUsageType.VoiceCommunication` the audio will come from the earpiece speaker | ✅ | ❌ | +| `androidAudioContentType` | `AndroidAudioContentType` | The audio content type indicates to the android system how you intend to use audio in your app. With `autoHandleInterruptions: true` and `androidAudioContentType: AndroidAudioContentType.Speech`, the audio will be paused during short interruptions, such as when a message arrives. Otherwise the playback volume is reduced while the notification is playing. Defaults to `AndroidAudioContentType.Music` | ✅ | ❌ | diff --git a/docs/docs/api/objects/update-options.md b/docs/docs/api/objects/update-options.md index 8c361c688..4f7284217 100644 --- a/docs/docs/api/objects/update-options.md +++ b/docs/docs/api/objects/update-options.md @@ -25,5 +25,10 @@ All parameters are optional. You also only need to specify the ones you want to | `forwardIcon` | [Resource Object](../objects/resource.md) | The jump forward icon¹ | ✅ | ❌ | ❌ | | `color` | `number` | The notification color in an ARGB hex | ✅ | ❌ | ❌ | | `progressUpdateEventInterval` | `number` | The interval (in seconds) that the [`Event.PlaybackProgressUpdated`](../events.md#playbackprogressupdated) will be fired. `undefined` by default. | ✅ | ✅ | ✅ | +| `iosCategory` | [`IOSCategory`](../constants/ios-category.md) | An [`IOSCategory`](../constants/ios-category.md). | ❌ | ✅ | ❌ | +| `iosCategoryMode` | [`IOSCategoryMode`](../constants/ios-category-mode.md) | The audio session mode, together with the audio session category, indicates to the system how you intend to use audio in your app. You can use a mode to configure the audio system for specific use cases such as video recording, voice or video chat, or audio analysis. | ❌ | ✅ | ❌ | +| `iosCategoryOptions` | [`IOSCategoryOptions[]`](../constants/ios-category-options.md) | An array of [`IOSCategoryOptions`](../constants/ios-category-options.md). | ❌ | ✅ | ❌ | +| `androidAudioUsageType` | `AndroidAudioUsageType` | (Android only) Specifies "why" the source is playing and controls routing, focus, and volume decisions. With `androidAudioUsageType` set to `AndroidAudioUsageType.VoiceCommunication` the audio will come from the earpiece speaker | ✅ | ❌ | +| `androidAudioContentType` | `AndroidAudioContentType` | The audio content type indicates to the android system how you intend to use audio in your app. With `autoHandleInterruptions: true` and `androidAudioContentType: AndroidAudioContentType.Speech`, the audio will be paused during short interruptions, such as when a message arrives. Otherwise the playback volume is reduced while the notification is playing. Defaults to `AndroidAudioContentType.Music` | ✅ | ❌ | *¹ - The custom icons will only work in release builds* diff --git a/ios/RNTrackPlayer/RNTrackPlayer.swift b/ios/RNTrackPlayer/RNTrackPlayer.swift index 3be134273..146f36cae 100644 --- a/ios/RNTrackPlayer/RNTrackPlayer.swift +++ b/ios/RNTrackPlayer/RNTrackPlayer.swift @@ -183,29 +183,13 @@ public class RNTrackPlayer: RCTEventEmitter, AudioSessionControllerDelegate { // configure wether control center metdata should auto update player.automaticallyUpdateNowPlayingInfo = config["autoUpdateMetadata"] as? Bool ?? true - // configure audio session - category, options & mode - if - let sessionCategoryStr = config["iosCategory"] as? String, - let mappedCategory = SessionCategory(rawValue: sessionCategoryStr) { - sessionCategory = mappedCategory.mapConfigToAVAudioSessionCategory() - } - - if - let sessionCategoryModeStr = config["iosCategoryMode"] as? String, - let mappedCategoryMode = SessionCategoryMode(rawValue: sessionCategoryModeStr) { - sessionCategoryMode = mappedCategoryMode.mapConfigToAVAudioSessionCategoryMode() - } - if let sessionCategoryPolicyStr = config["iosCategoryPolicy"] as? String, let mappedCategoryPolicy = SessionCategoryPolicy(rawValue: sessionCategoryPolicyStr) { sessionCategoryPolicy = mappedCategoryPolicy.mapConfigToAVAudioSessionCategoryPolicy() } - - let sessionCategoryOptsStr = config["iosCategoryOptions"] as? [String] - let mappedCategoryOpts = sessionCategoryOptsStr?.compactMap { SessionCategoryOptions(rawValue: $0)?.mapConfigToAVAudioSessionCategoryOptions() } ?? [] - sessionCategoryOptions = AVAudioSession.CategoryOptions(mappedCategoryOpts) - + + setCategoryFrom(config: config) configureAudioSession() // setup event listeners @@ -290,6 +274,25 @@ public class RNTrackPlayer: RCTEventEmitter, AudioSessionControllerDelegate { hasInitialized = true resolve(NSNull()) } + + private func setCategoryFrom(config: [String: Any]) { + // configure audio session - category, options & mode + if + let sessionCategoryStr = config["iosCategory"] as? String, + let mappedCategory = SessionCategory(rawValue: sessionCategoryStr) { + sessionCategory = mappedCategory.mapConfigToAVAudioSessionCategory() + } + + if + let sessionCategoryModeStr = config["iosCategoryMode"] as? String, + let mappedCategoryMode = SessionCategoryMode(rawValue: sessionCategoryModeStr) { + sessionCategoryMode = mappedCategoryMode.mapConfigToAVAudioSessionCategoryMode() + } + + let sessionCategoryOptsStr = config["iosCategoryOptions"] as? [String] + let mappedCategoryOpts = sessionCategoryOptsStr?.compactMap { SessionCategoryOptions(rawValue: $0)?.mapConfigToAVAudioSessionCategoryOptions() } ?? [] + sessionCategoryOptions = AVAudioSession.CategoryOptions(mappedCategoryOpts) + } private func configureAudioSession() { @@ -329,6 +332,9 @@ public class RNTrackPlayer: RCTEventEmitter, AudioSessionControllerDelegate { forwardJumpInterval = options["forwardJumpInterval"] as? NSNumber ?? forwardJumpInterval backwardJumpInterval = options["backwardJumpInterval"] as? NSNumber ?? backwardJumpInterval + + setCategoryFrom(config: options) + configureAudioSession() player.remoteCommands = capabilitiesStr .compactMap { Capability(rawValue: $0) } diff --git a/src/constants/AndroidAudioUsageType.ts b/src/constants/AndroidAudioUsageType.ts new file mode 100644 index 000000000..7f97c4c5d --- /dev/null +++ b/src/constants/AndroidAudioUsageType.ts @@ -0,0 +1,4 @@ +export enum AndroidAudioUsageType { + Media = 'media', + VoiceCommunication = 'voice_communication', +} diff --git a/src/constants/index.ts b/src/constants/index.ts index c9381810c..82cadab17 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,4 +1,5 @@ export * from './AndroidAudioContentType'; +export * from './AndroidAudioUsageType'; export * from './AppKilledPlaybackBehavior'; export * from './Capability'; export * from './Event'; diff --git a/src/interfaces/PlayerOptions.ts b/src/interfaces/PlayerOptions.ts index 4d132b23c..0764eda62 100644 --- a/src/interfaces/PlayerOptions.ts +++ b/src/interfaces/PlayerOptions.ts @@ -1,5 +1,6 @@ import type { AndroidAudioContentType, + AndroidAudioUsageType, IOSCategory, IOSCategoryMode, IOSCategoryOptions, @@ -71,6 +72,15 @@ export interface PlayerOptions { * Sets on `play()`. */ iosCategoryOptions?: IOSCategoryOptions[]; + + /** + * (Android only) Specifies why the source is playing and controls routing, focus, and volume decisions. + * With `androidAudioUsageType` set to VoiceCommunication the audio will come from the earpiece speaker + * + * @default AndroidAudioUsageType.Media + */ + androidAudioUsageType?: AndroidAudioUsageType; + /** * (Android only) The audio content type indicates to the android system how * you intend to use audio in your app. diff --git a/src/interfaces/UpdateOptions.ts b/src/interfaces/UpdateOptions.ts index 9657caec9..30ef554b7 100644 --- a/src/interfaces/UpdateOptions.ts +++ b/src/interfaces/UpdateOptions.ts @@ -1,7 +1,15 @@ import type { AndroidOptions } from './AndroidOptions'; import type { FeedbackOptions } from './FeedbackOptions'; import type { ResourceObject } from './ResourceObject'; -import type { RatingType, Capability } from '../constants'; +import type { + RatingType, + Capability, + IOSCategory, + IOSCategoryMode, + IOSCategoryOptions, + AndroidAudioContentType, + AndroidAudioUsageType, +} from '../constants'; export interface UpdateOptions { android?: AndroidOptions; @@ -46,4 +54,49 @@ export interface UpdateOptions { rewindIcon?: ResourceObject; forwardIcon?: ResourceObject; color?: number; + + /** + * [AVAudioSession.Category](https://developer.apple.com/documentation/avfoundation/avaudiosession/1616615-category) + */ + iosCategory?: IOSCategory; + /** + * (iOS only) The audio session mode, together with the audio session category, + * indicates to the system how you intend to use audio in your app. You can use + * a mode to configure the audio system for specific use cases such as video + * recording, voice or video chat, or audio analysis. + * + * See https://developer.apple.com/documentation/avfoundation/avaudiosession/1616508-mode + */ + iosCategoryMode?: IOSCategoryMode; + /** + * [AVAudioSession.CategoryOptions](https://developer.apple.com/documentation/avfoundation/avaudiosession/1616503-categoryoptions) for iOS. + */ + iosCategoryOptions?: IOSCategoryOptions[]; + + /** + * (Android only) Specifies why the source is playing and controls routing, focus, and volume decisions. + * With `androidAudioUsageType` set to VoiceCommunication the audio will come from the earpiece speaker + * + * @default AndroidAudioUsageType.Media + */ + androidAudioUsageType?: AndroidAudioUsageType; + + /** + * (Android only) The audio content type indicates to the android system how + * you intend to use audio in your app. + * + * With `autoHandleInterruptions: true` and + * `androidAudioContentType: AndroidAudioContentType.Speech`, the audio will be + * paused during short interruptions, such as when a message arrives. + * Otherwise the playback volume is reduced while the notification is playing. + * + * @default AndroidAudioContentType.Music + */ + androidAudioContentType?: AndroidAudioContentType; + + /** + * Indicates whether the player should automatically handle audio interruptions. + * Defaults to `false`. + */ + autoHandleInterruptions?: boolean; }