From f2600ec542c7d79e136a94bf73f6acacce1b2ec1 Mon Sep 17 00:00:00 2001 From: Jun Date: Thu, 5 Mar 2026 17:36:20 +0900 Subject: [PATCH] feat: add abandonAudioFocus() method for Android audio focus release Adds a new API to explicitly abandon audio focus when the player is paused/stopped, allowing other apps (YouTube Music, etc.) to restore their volume immediately instead of requiring full app termination. (QAP-4557) Co-Authored-By: Claude Opus 4.6 --- .../kotlinaudio/players/BaseAudioPlayer.kt | 15 +++++++++++++++ .../trackplayer/module/MusicModule.kt | 6 ++++++ .../trackplayer/service/MusicService.kt | 5 +++++ ios/TrackPlayer.mm | 3 +++ src/NativeTrackPlayer.ts | 1 + src/trackPlayer.ts | 8 ++++++++ 6 files changed, 38 insertions(+) diff --git a/android/src/main/java/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer.kt b/android/src/main/java/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer.kt index b8390876a..278581987 100644 --- a/android/src/main/java/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer.kt +++ b/android/src/main/java/com/doublesymmetry/kotlinaudio/players/BaseAudioPlayer.kt @@ -260,6 +260,21 @@ abstract class BaseAudioPlayer internal constructor( exoPlayer.pauseAtEndOfMediaItems = pause } + /** + * Abandons audio focus so other apps can restore their volume. + * Uses ExoPlayer's setAudioAttributes toggle when handleAudioFocus is true, + * otherwise uses the custom FocusManager. + */ + fun abandonAudioFocus() { + if (options.handleAudioFocus) { + val attrs = exoPlayer.audioAttributes + exoPlayer.setAudioAttributes(attrs, false) + exoPlayer.setAudioAttributes(attrs, true) + } else { + focusManager.abandonAudioFocusIfHeld() + } + } + /** * Stops and destroys the player. Only call this when you are finished using the player, otherwise use [pause]. */ diff --git a/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicModule.kt b/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicModule.kt index 13a6c609f..4e1d5b1db 100644 --- a/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicModule.kt +++ b/android/src/main/java/com/doublesymmetry/trackplayer/module/MusicModule.kt @@ -556,6 +556,12 @@ class MusicModule(reactContext: ReactApplicationContext) : NativeTrackPlayerSpec callback.resolve(null) } + override fun abandonAudioFocus(callback: Promise) = launchInScope { + if (verifyServiceBoundOrReject(callback)) return@launchInScope + musicService.abandonAudioFocus() + callback.resolve(null) + } + override fun validateOnStartCommandIntent(callback: Promise) = launchInScope { if (verifyServiceBoundOrReject(callback)) return@launchInScope callback.resolve(musicService.onStartCommandIntentValid) 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 21f7c6282..d5dba7f47 100644 --- a/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt +++ b/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt @@ -78,6 +78,11 @@ class MusicService : HeadlessJsMediaService() { sWakeLock?.release() } + @MainThread + fun abandonAudioFocus() { + player.abandonAudioFocus() + } + fun getBitmapLoader(): BitmapLoader { return mediaSession.bitmapLoader } diff --git a/ios/TrackPlayer.mm b/ios/TrackPlayer.mm index e690de8da..971d06233 100644 --- a/ios/TrackPlayer.mm +++ b/ios/TrackPlayer.mm @@ -199,6 +199,9 @@ - (void)validateOnStartCommandIntent:(nonnull RCTPromiseResolveBlock)resolve rej - (void)abandonWakeLock:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { resolve(nil); } +- (void)abandonAudioFocus:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { + resolve(nil); +} - (void)acquireWakeLock:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { resolve(nil); } diff --git a/src/NativeTrackPlayer.ts b/src/NativeTrackPlayer.ts index a5dcd7575..7fca43a80 100644 --- a/src/NativeTrackPlayer.ts +++ b/src/NativeTrackPlayer.ts @@ -99,6 +99,7 @@ export interface Spec extends TurboModule { // android methods acquireWakeLock(): Promise; abandonWakeLock(): Promise; + abandonAudioFocus(): Promise; validateOnStartCommandIntent(): Promise; } diff --git a/src/trackPlayer.ts b/src/trackPlayer.ts index cc427703d..1c4bb8b0f 100644 --- a/src/trackPlayer.ts +++ b/src/trackPlayer.ts @@ -446,6 +446,14 @@ export async function abandonWakeLock() { TrackPlayer.abandonWakeLock(); } +/** + * Abandons audio focus so other apps can restore their volume (android only.) + */ +export async function abandonAudioFocus(): Promise { + if (!isAndroid) return; + return TrackPlayer.abandonAudioFocus(); +} + /** * get onStartCommandIntent is null or not (Android only.). this is used to identify * if musicservice is restarted or not.