From 3c58b34ac49de1c9f9b957c88fe539fe1636df94 Mon Sep 17 00:00:00 2001 From: Yubi Lee Date: Sun, 1 Feb 2026 17:05:44 +0900 Subject: [PATCH 1/2] Fix audio ducking issue by using Android's automatic audio focus management Previously, manual audio focus handling caused volume to not be restored after transient ducking on some devices. This change removes manual AudioFocusRequest management and relies on ExoPlayer's built-in audio focus handling (handleAudioFocus=true), which properly manages automatic ducking as recommended in: https://developer.android.com/media/optimize/audio-focus#automatic-ducking Fixes #9710 --- .../org/schabi/newpipe/player/Player.java | 23 ++--- .../newpipe/player/helper/AudioReactor.java | 95 +------------------ 2 files changed, 10 insertions(+), 108 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index b07b15a4585..0fdcc972c3f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -641,6 +641,15 @@ private void initPlayer(final boolean playOnReady) { simpleExoPlayer.setWakeMode(C.WAKE_MODE_NETWORK); simpleExoPlayer.setHandleAudioBecomingNoisy(true); + // Enable automatic audio focus management - let Android handle ducking automatically + simpleExoPlayer.setAudioAttributes( + new com.google.android.exoplayer2.audio.AudioAttributes.Builder() + .setUsage(C.USAGE_MEDIA) + .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) + .build(), + true // handleAudioFocus = true for automatic management + ); + audioReactor = new AudioReactor(context, simpleExoPlayer); registerBroadcastReceiver(); @@ -1190,10 +1199,6 @@ private void onPrepared(final boolean playWhenReady) { } UIs.call(PlayerUi::onPrepared); - - if (playWhenReady && !isMuted()) { - audioReactor.requestAudioFocus(); - } } private void onBlocked() { @@ -1341,11 +1346,6 @@ public void toggleShuffleModeEnabled() { public void toggleMute() { final boolean wasMuted = isMuted(); simpleExoPlayer.setVolume(wasMuted ? 1 : 0); - if (wasMuted) { - audioReactor.requestAudioFocus(); - } else { - audioReactor.abandonAudioFocus(); - } UIs.call(playerUi -> playerUi.onMuteUnmuteChanged(!wasMuted)); notifyPlaybackUpdateToListeners(); } @@ -1757,10 +1757,6 @@ public void play() { return; } - if (!isMuted()) { - audioReactor.requestAudioFocus(); - } - if (currentState == STATE_COMPLETED) { if (playQueue.getIndex() == 0) { seekToDefault(); @@ -1781,7 +1777,6 @@ public void pause() { return; } - audioReactor.abandonAudioFocus(); simpleExoPlayer.pause(); saveStreamProgressState(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java index 084336d5483..2ec33dc0f42 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java @@ -1,54 +1,36 @@ package org.schabi.newpipe.player.helper; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; import android.content.Context; import android.content.Intent; import android.media.AudioManager; import android.media.audiofx.AudioEffect; -import android.util.Log; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; -import androidx.media.AudioFocusRequestCompat; import androidx.media.AudioManagerCompat; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.analytics.AnalyticsListener; -public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, AnalyticsListener { +public class AudioReactor implements AnalyticsListener { private static final String TAG = "AudioFocusReactor"; - private static final int DUCK_DURATION = 1500; - private static final float DUCK_AUDIO_TO = .2f; - - private static final int FOCUS_GAIN_TYPE = AudioManagerCompat.AUDIOFOCUS_GAIN; private static final int STREAM_TYPE = AudioManager.STREAM_MUSIC; private final ExoPlayer player; private final Context context; private final AudioManager audioManager; - private final AudioFocusRequestCompat request; - public AudioReactor(@NonNull final Context context, @NonNull final ExoPlayer player) { this.player = player; this.context = context; this.audioManager = ContextCompat.getSystemService(context, AudioManager.class); player.addAnalyticsListener(this); - - request = new AudioFocusRequestCompat.Builder(FOCUS_GAIN_TYPE) - //.setAcceptsDelayedFocusGain(true) - .setWillPauseWhenDucked(true) - .setOnAudioFocusChangeListener(this) - .build(); } public void dispose() { - abandonAudioFocus(); player.removeAnalyticsListener(this); notifyAudioSessionUpdate(false, player.getAudioSessionId()); } @@ -57,14 +39,6 @@ public void dispose() { // Audio Manager //////////////////////////////////////////////////////////////////////////*/ - public void requestAudioFocus() { - AudioManagerCompat.requestAudioFocus(audioManager, request); - } - - public void abandonAudioFocus() { - AudioManagerCompat.abandonAudioFocusRequest(audioManager, request); - } - public int getVolume() { return audioManager.getStreamVolume(STREAM_TYPE); } @@ -77,73 +51,6 @@ public int getMaxVolume() { return AudioManagerCompat.getStreamMaxVolume(audioManager, STREAM_TYPE); } - /*////////////////////////////////////////////////////////////////////////// - // AudioFocus - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onAudioFocusChange(final int focusChange) { - Log.d(TAG, "onAudioFocusChange() called with: focusChange = [" + focusChange + "]"); - switch (focusChange) { - case AudioManager.AUDIOFOCUS_GAIN: - onAudioFocusGain(); - break; - case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: - onAudioFocusLossCanDuck(); - break; - case AudioManager.AUDIOFOCUS_LOSS: - case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: - onAudioFocusLoss(); - break; - } - } - - private void onAudioFocusGain() { - Log.d(TAG, "onAudioFocusGain() called"); - player.setVolume(DUCK_AUDIO_TO); - animateAudio(DUCK_AUDIO_TO, 1.0f); - - if (PlayerHelper.isResumeAfterAudioFocusGain(context)) { - player.play(); - } - } - - private void onAudioFocusLoss() { - Log.d(TAG, "onAudioFocusLoss() called"); - player.pause(); - } - - private void onAudioFocusLossCanDuck() { - Log.d(TAG, "onAudioFocusLossCanDuck() called"); - // Set the volume to 1/10 on ducking - player.setVolume(DUCK_AUDIO_TO); - } - - private void animateAudio(final float from, final float to) { - final ValueAnimator valueAnimator = new ValueAnimator(); - valueAnimator.setFloatValues(from, to); - valueAnimator.setDuration(AudioReactor.DUCK_DURATION); - valueAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(final Animator animation) { - player.setVolume(from); - } - - @Override - public void onAnimationCancel(final Animator animation) { - player.setVolume(to); - } - - @Override - public void onAnimationEnd(final Animator animation) { - player.setVolume(to); - } - }); - valueAnimator.addUpdateListener(animation -> - player.setVolume(((float) animation.getAnimatedValue()))); - valueAnimator.start(); - } - /*////////////////////////////////////////////////////////////////////////// // Audio Processing //////////////////////////////////////////////////////////////////////////*/ From 5031409f92aa9445f05c83e46f914932cba08a32 Mon Sep 17 00:00:00 2001 From: Yubi Lee Date: Sat, 28 Mar 2026 09:14:30 +0900 Subject: [PATCH 2/2] Fix broken mute It should play video with mute on even though another app is playing audio. --- .../org/schabi/newpipe/player/Player.java | 27 +++++++++++++++++++ .../newpipe/player/helper/AudioReactor.java | 25 +++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index 0fdcc972c3f..ef4f86b2a2a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -1345,7 +1345,34 @@ public void toggleShuffleModeEnabled() { public void toggleMute() { final boolean wasMuted = isMuted(); + final boolean wasPlaying = simpleExoPlayer.isPlaying(); + Log.d(TAG, "toggleMute: wasMuted=" + wasMuted + ", wasPlaying=" + wasPlaying); simpleExoPlayer.setVolume(wasMuted ? 1 : 0); + if (wasMuted) { + Log.d(TAG, "toggleMute: enabling audio focus, willPlay=" + wasPlaying); + simpleExoPlayer.setAudioAttributes( + new com.google.android.exoplayer2.audio.AudioAttributes.Builder() + .setUsage(C.USAGE_MEDIA) + .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) + .build(), + true // handleAudioFocus = true - ExoPlayer handles focus automatically + ); + if (wasPlaying) { + Log.d(TAG, "toggleMute: calling play(), isPlaying=" + simpleExoPlayer.isPlaying()); + simpleExoPlayer.play(); + Log.d(TAG, "toggleMute: after play(), isPlaying=" + simpleExoPlayer.isPlaying()); + } + } else { + Log.d(TAG, "toggleMute: disabling audio focus, abandoning focus"); + simpleExoPlayer.setAudioAttributes( + new com.google.android.exoplayer2.audio.AudioAttributes.Builder() + .setUsage(C.USAGE_MEDIA) + .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) + .build(), + false // handleAudioFocus = false + ); + audioReactor.abandonAudioFocus(); + } UIs.call(playerUi -> playerUi.onMuteUnmuteChanged(!wasMuted)); notifyPlaybackUpdateToListeners(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java index 2ec33dc0f42..a0cfea2c457 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java @@ -4,9 +4,11 @@ import android.content.Intent; import android.media.AudioManager; import android.media.audiofx.AudioEffect; +import android.util.Log; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; +import androidx.media.AudioFocusRequestCompat; import androidx.media.AudioManagerCompat; import com.google.android.exoplayer2.ExoPlayer; @@ -16,11 +18,13 @@ public class AudioReactor implements AnalyticsListener { private static final String TAG = "AudioFocusReactor"; + private static final int FOCUS_GAIN_TYPE = AudioManagerCompat.AUDIOFOCUS_GAIN; private static final int STREAM_TYPE = AudioManager.STREAM_MUSIC; private final ExoPlayer player; private final Context context; private final AudioManager audioManager; + private final AudioFocusRequestCompat request; public AudioReactor(@NonNull final Context context, @NonNull final ExoPlayer player) { @@ -28,13 +32,34 @@ public AudioReactor(@NonNull final Context context, this.context = context; this.audioManager = ContextCompat.getSystemService(context, AudioManager.class); player.addAnalyticsListener(this); + + request = new AudioFocusRequestCompat.Builder(FOCUS_GAIN_TYPE) + .setWillPauseWhenDucked(true) + .setOnAudioFocusChangeListener(focusChange -> { + }) + .build(); } public void dispose() { + abandonAudioFocus(); player.removeAnalyticsListener(this); notifyAudioSessionUpdate(false, player.getAudioSessionId()); } + /*////////////////////////////////////////////////////////////////////////// + // Audio Focus + //////////////////////////////////////////////////////////////////////////*/ + + public void requestAudioFocus() { + Log.d(TAG, "requestAudioFocus() called"); + AudioManagerCompat.requestAudioFocus(audioManager, request); + } + + public void abandonAudioFocus() { + Log.d(TAG, "abandonAudioFocus() called"); + AudioManagerCompat.abandonAudioFocusRequest(audioManager, request); + } + /*////////////////////////////////////////////////////////////////////////// // Audio Manager //////////////////////////////////////////////////////////////////////////*/