Skip to content
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

feat: make it possible to play audio through the earpiece speaker #2285

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"

Expand Down
3 changes: 2 additions & 1 deletion docs/docs/api/objects/player-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` | ✅ | ❌ |
5 changes: 5 additions & 0 deletions docs/docs/api/objects/update-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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*
42 changes: 24 additions & 18 deletions ios/RNTrackPlayer/RNTrackPlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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) }
Expand Down
4 changes: 4 additions & 0 deletions src/constants/AndroidAudioUsageType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum AndroidAudioUsageType {
Media = 'media',
VoiceCommunication = 'voice_communication',
}
1 change: 1 addition & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './AndroidAudioContentType';
export * from './AndroidAudioUsageType';
export * from './AppKilledPlaybackBehavior';
export * from './Capability';
export * from './Event';
Expand Down
10 changes: 10 additions & 0 deletions src/interfaces/PlayerOptions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {
AndroidAudioContentType,
AndroidAudioUsageType,
IOSCategory,
IOSCategoryMode,
IOSCategoryOptions,
Expand Down Expand Up @@ -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.
Expand Down
55 changes: 54 additions & 1 deletion src/interfaces/UpdateOptions.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
}