fix audio issue

This commit is contained in:
otsmr 2026-08-22 21:21:28 +02:00
parent d1d70e1559
commit b874b9cc30
8 changed files with 103 additions and 10 deletions

View file

@ -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();
}
}

View file

@ -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,

View file

@ -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);

View file

@ -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

View file

@ -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

View file

@ -84,6 +84,7 @@ static NSDictionary<NSString *, NSValue *> *FVPGetPlayerItemObservations(void) {
self = [super init];
NSAssert(self, @"super init cannot be nil");
_avFactory = avFactory;
_viewProvider = viewProvider;
NSObject<FVPAVAsset> *asset = item.asset;
@ -191,6 +192,11 @@ static NSDictionary<NSString *, NSValue *> *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<NSString *, NSValue *> *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
}
}

View file

@ -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

View file

@ -17,6 +17,8 @@ NS_ASSUME_NONNULL_BEGIN
@property(nonatomic, readonly) NSObject<FVPPixelBufferSource> *pixelBufferSource;
/// The view provider, to obtain view information from.
@property(nonatomic, readonly, nullable) NSObject<FVPViewProvider> *viewProvider;
/// The AVFactory used to create AVFoundation objects.
@property(nonatomic, readonly) id<FVPAVFactory> 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.