From b874b9cc309822d90b940fdfbc8fd1ad3afc2450 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 22 Aug 2026 21:21:28 +0200 Subject: [PATCH] fix audio issue --- .../plugins/videoplayer/VideoPlayer.java | 74 +++++++++++++++++-- .../platformview/PlatformViewVideoPlayer.java | 4 +- .../texture/TextureVideoPlayer.java | 4 +- .../videoplayer/TextureVideoPlayerTest.java | 3 +- .../FVPAVFactory.m | 6 ++ .../FVPVideoPlayer.m | 16 ++++ .../FVPAVFactory.h | 4 + .../FVPVideoPlayer_Internal.h | 2 + 8 files changed, 103 insertions(+), 10 deletions(-) diff --git a/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java index 3a24e56..98f9813 100644 --- a/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java +++ b/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java @@ -25,6 +25,10 @@ import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; import io.flutter.view.TextureRegistry.SurfaceProducer; import java.util.ArrayList; import java.util.List; +import android.content.Context; +import android.media.AudioManager; +import android.media.AudioFocusRequest; +import android.os.Build; /** * A class responsible for managing video playback using {@link ExoPlayer}. @@ -37,6 +41,11 @@ public abstract class VideoPlayer implements VideoPlayerInstanceApi { @Nullable private DisposeHandler disposeHandler; @Nullable private ExoPlayerEventListener exoPlayerEventListener; @NonNull protected ExoPlayer exoPlayer; + @NonNull protected VideoPlayerOptions options; + @NonNull protected Context applicationContext; + @Nullable private AudioManager audioManager; + @Nullable private AudioFocusRequest audioFocusRequest; + @Nullable private AudioManager.OnAudioFocusChangeListener focusChangeListener; // TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039. @UnstableApi @Nullable protected DefaultTrackSelector trackSelector; @@ -67,13 +76,16 @@ public abstract class VideoPlayer implements VideoPlayerInstanceApi { // https://github.com/flutter/packages/pull/10193 @SuppressWarnings("this-escape") public VideoPlayer( + @NonNull Context context, @NonNull VideoPlayerCallbacks events, @NonNull MediaItem mediaItem, @NonNull VideoPlayerOptions options, @Nullable SurfaceProducer surfaceProducer, @NonNull ExoPlayerProvider exoPlayerProvider) { + this.applicationContext = context; this.videoPlayerEvents = events; this.surfaceProducer = surfaceProducer; + this.options = options; exoPlayer = exoPlayerProvider.get(); // Try to get the track selector from the ExoPlayer if it was built with one @@ -85,7 +97,11 @@ public abstract class VideoPlayer implements VideoPlayerInstanceApi { exoPlayer.prepare(); exoPlayerEventListener = createExoPlayerEventListener(exoPlayer, surfaceProducer); exoPlayer.addListener(exoPlayerEventListener); - setAudioAttributes(exoPlayer, options.mixWithOthers); + + // Disable ExoPlayer's automatic audio focus management so we can handle it manually. + exoPlayer.setAudioAttributes( + new AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MOVIE).build(), + false); } public void setDisposeHandler(@Nullable DisposeHandler handler) { @@ -96,12 +112,6 @@ public abstract class VideoPlayer implements VideoPlayerInstanceApi { protected abstract ExoPlayerEventListener createExoPlayerEventListener( @NonNull ExoPlayer exoPlayer, @Nullable SurfaceProducer surfaceProducer); - private static void setAudioAttributes(ExoPlayer exoPlayer, boolean isMixMode) { - exoPlayer.setAudioAttributes( - new AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MOVIE).build(), - !isMixMode); - } - /** * Helper method to extract a long value from a Format field, returning null if the value is * Format.NO_VALUE. @@ -124,13 +134,62 @@ public abstract class VideoPlayer implements VideoPlayerInstanceApi { return value != Format.NO_VALUE ? value : null; } + private void setupAudioFocus() { + if (audioManager == null) { + audioManager = (AudioManager) applicationContext.getSystemService(Context.AUDIO_SERVICE); + } + if (focusChangeListener == null) { + focusChangeListener = focusChange -> { + if (focusChange == AudioManager.AUDIOFOCUS_LOSS || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) { + pause(); + } + }; + } + } + + private void requestAudioFocus() { + if (options.mixWithOthers || audioManager == null) { + return; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (audioFocusRequest == null) { + audioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT) + .setAudioAttributes(new android.media.AudioAttributes.Builder() + .setUsage(android.media.AudioAttributes.USAGE_MEDIA) + .setContentType(android.media.AudioAttributes.CONTENT_TYPE_MOVIE) + .build()) + .setOnAudioFocusChangeListener(focusChangeListener) + .build(); + } + audioManager.requestAudioFocus(audioFocusRequest); + } else { + audioManager.requestAudioFocus(focusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); + } + } + + private void abandonAudioFocus() { + if (options.mixWithOthers || audioManager == null) { + return; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (audioFocusRequest != null) { + audioManager.abandonAudioFocusRequest(audioFocusRequest); + } + } else { + audioManager.abandonAudioFocus(focusChangeListener); + } + } + @Override public void play() { + setupAudioFocus(); + requestAudioFocus(); exoPlayer.play(); } @Override public void pause() { + abandonAudioFocus(); exoPlayer.pause(); } @@ -459,6 +518,7 @@ public abstract class VideoPlayer implements VideoPlayerInstanceApi { exoPlayerEventListener.dispose(); exoPlayerEventListener = null; } + abandonAudioFocus(); exoPlayer.release(); } } diff --git a/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewVideoPlayer.java b/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewVideoPlayer.java index a4b5a93..8a766b6 100644 --- a/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewVideoPlayer.java +++ b/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewVideoPlayer.java @@ -28,11 +28,12 @@ public class PlatformViewVideoPlayer extends VideoPlayer { @UnstableApi @VisibleForTesting public PlatformViewVideoPlayer( + @NonNull Context context, @NonNull VideoPlayerCallbacks events, @NonNull MediaItem mediaItem, @NonNull VideoPlayerOptions options, @NonNull ExoPlayerProvider exoPlayerProvider) { - super(events, mediaItem, options, /* surfaceProducer */ null, exoPlayerProvider); + super(context, events, mediaItem, options, /* surfaceProducer */ null, exoPlayerProvider); } /** @@ -53,6 +54,7 @@ public class PlatformViewVideoPlayer extends VideoPlayer { @NonNull VideoAsset asset, @NonNull VideoPlayerOptions options) { return new PlatformViewVideoPlayer( + context, events, asset.getMediaItem(), options, diff --git a/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureVideoPlayer.java b/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureVideoPlayer.java index c0a2d41..f8d6439 100644 --- a/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureVideoPlayer.java +++ b/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureVideoPlayer.java @@ -52,6 +52,7 @@ public final class TextureVideoPlayer extends VideoPlayer implements SurfaceProd @NonNull VideoAsset asset, @NonNull VideoPlayerOptions options) { return new TextureVideoPlayer( + context, events, surfaceProducer, asset.getMediaItem(), @@ -87,12 +88,13 @@ public final class TextureVideoPlayer extends VideoPlayer implements SurfaceProd @UnstableApi @VisibleForTesting public TextureVideoPlayer( + @NonNull Context context, @NonNull VideoPlayerCallbacks events, @NonNull SurfaceProducer surfaceProducer, @NonNull MediaItem mediaItem, @NonNull VideoPlayerOptions options, @NonNull ExoPlayerProvider exoPlayerProvider) { - super(events, mediaItem, options, surfaceProducer, exoPlayerProvider); + super(context, events, mediaItem, options, surfaceProducer, exoPlayerProvider); surfaceProducer.setCallback(this); diff --git a/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/TextureVideoPlayerTest.java b/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/TextureVideoPlayerTest.java index befb227..f229094 100644 --- a/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/TextureVideoPlayerTest.java +++ b/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/TextureVideoPlayerTest.java @@ -8,6 +8,7 @@ import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; +import android.content.Context; import android.view.Surface; import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; @@ -65,7 +66,7 @@ public final class TextureVideoPlayerTest { private TextureVideoPlayer createVideoPlayer(VideoPlayerOptions options) { return new TextureVideoPlayer( - mockEvents, mockProducer, fakeVideoAsset.getMediaItem(), options, () -> mockExoPlayer); + mock(Context.class), mockEvents, mockProducer, fakeVideoAsset.getMediaItem(), options, () -> mockExoPlayer); } @Test diff --git a/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/FVPAVFactory.m b/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/FVPAVFactory.m index d87a7c4..97b480b 100644 --- a/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/FVPAVFactory.m +++ b/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/FVPAVFactory.m @@ -128,6 +128,12 @@ error:(NSError **)outError { return [AVAudioSession.sharedInstance setCategory:category withOptions:options error:outError]; } + +- (BOOL)setActive:(BOOL)active + withOptions:(AVAudioSessionSetActiveOptions)options + error:(NSError **)outError { + return [AVAudioSession.sharedInstance setActive:active withOptions:options error:outError]; +} @end #endif diff --git a/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/FVPVideoPlayer.m b/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/FVPVideoPlayer.m index 316aa62..d2cd01d 100644 --- a/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/FVPVideoPlayer.m +++ b/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/FVPVideoPlayer.m @@ -84,6 +84,7 @@ static NSDictionary *FVPGetPlayerItemObservations(void) { self = [super init]; NSAssert(self, @"super init cannot be nil"); + _avFactory = avFactory; _viewProvider = viewProvider; NSObject *asset = item.asset; @@ -191,6 +192,11 @@ static NSDictionary *FVPGetPlayerItemObservations(void) { if (_onDisposed) { _onDisposed(); } +#if TARGET_OS_IOS + if (_avFactory != nil) { + [[_avFactory sharedAudioSession] setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil]; + } +#endif [self.eventListener videoPlayerWasDisposed]; } @@ -218,6 +224,11 @@ static NSDictionary *FVPGetPlayerItemObservations(void) { AVPlayerItem *p = [notification object]; [p seekToTime:kCMTimeZero completionHandler:nil]; } else { +#if TARGET_OS_IOS + if (_avFactory != nil) { + [[_avFactory sharedAudioSession] setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil]; + } +#endif [self.eventListener videoPlayerDidComplete]; } } @@ -348,6 +359,11 @@ NS_INLINE CGFloat radiansToDegrees(CGFloat radians) { } } else { [_player pause]; +#if TARGET_OS_IOS + if (_avFactory != nil) { + [[_avFactory sharedAudioSession] setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil]; + } +#endif } } diff --git a/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/include/video_player_avfoundation_objc/FVPAVFactory.h b/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/include/video_player_avfoundation_objc/FVPAVFactory.h index 6e80b8a..edbe085 100644 --- a/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/include/video_player_avfoundation_objc/FVPAVFactory.h +++ b/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/include/video_player_avfoundation_objc/FVPAVFactory.h @@ -76,6 +76,10 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)setCategory:(AVAudioSessionCategory)category withOptions:(AVAudioSessionCategoryOptions)options error:(NSError **)outError; +/// Wraps the AVAudioSession method of the same name. +- (BOOL)setActive:(BOOL)active + withOptions:(AVAudioSessionSetActiveOptions)options + error:(NSError **)outError; @end #endif diff --git a/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/include/video_player_avfoundation_objc/FVPVideoPlayer_Internal.h b/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/include/video_player_avfoundation_objc/FVPVideoPlayer_Internal.h index 6f86b7d..c345ff7 100644 --- a/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/include/video_player_avfoundation_objc/FVPVideoPlayer_Internal.h +++ b/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation_objc/include/video_player_avfoundation_objc/FVPVideoPlayer_Internal.h @@ -17,6 +17,8 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, readonly) NSObject *pixelBufferSource; /// The view provider, to obtain view information from. @property(nonatomic, readonly, nullable) NSObject *viewProvider; +/// The AVFactory used to create AVFoundation objects. +@property(nonatomic, readonly) id avFactory; /// The preferred transform for the video. It can be used to handle the rotation of the video. @property(nonatomic) CGAffineTransform preferredTransform; /// The target playback speed requested by the plugin client.