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 io.flutter.view.TextureRegistry.SurfaceProducer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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}. * 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 DisposeHandler disposeHandler;
@Nullable private ExoPlayerEventListener exoPlayerEventListener; @Nullable private ExoPlayerEventListener exoPlayerEventListener;
@NonNull protected ExoPlayer exoPlayer; @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. // TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039.
@UnstableApi @Nullable protected DefaultTrackSelector trackSelector; @UnstableApi @Nullable protected DefaultTrackSelector trackSelector;
@ -67,13 +76,16 @@ public abstract class VideoPlayer implements VideoPlayerInstanceApi {
// https://github.com/flutter/packages/pull/10193 // https://github.com/flutter/packages/pull/10193
@SuppressWarnings("this-escape") @SuppressWarnings("this-escape")
public VideoPlayer( public VideoPlayer(
@NonNull Context context,
@NonNull VideoPlayerCallbacks events, @NonNull VideoPlayerCallbacks events,
@NonNull MediaItem mediaItem, @NonNull MediaItem mediaItem,
@NonNull VideoPlayerOptions options, @NonNull VideoPlayerOptions options,
@Nullable SurfaceProducer surfaceProducer, @Nullable SurfaceProducer surfaceProducer,
@NonNull ExoPlayerProvider exoPlayerProvider) { @NonNull ExoPlayerProvider exoPlayerProvider) {
this.applicationContext = context;
this.videoPlayerEvents = events; this.videoPlayerEvents = events;
this.surfaceProducer = surfaceProducer; this.surfaceProducer = surfaceProducer;
this.options = options;
exoPlayer = exoPlayerProvider.get(); exoPlayer = exoPlayerProvider.get();
// Try to get the track selector from the ExoPlayer if it was built with one // 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(); exoPlayer.prepare();
exoPlayerEventListener = createExoPlayerEventListener(exoPlayer, surfaceProducer); exoPlayerEventListener = createExoPlayerEventListener(exoPlayer, surfaceProducer);
exoPlayer.addListener(exoPlayerEventListener); 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) { public void setDisposeHandler(@Nullable DisposeHandler handler) {
@ -96,12 +112,6 @@ public abstract class VideoPlayer implements VideoPlayerInstanceApi {
protected abstract ExoPlayerEventListener createExoPlayerEventListener( protected abstract ExoPlayerEventListener createExoPlayerEventListener(
@NonNull ExoPlayer exoPlayer, @Nullable SurfaceProducer surfaceProducer); @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 * Helper method to extract a long value from a Format field, returning null if the value is
* Format.NO_VALUE. * Format.NO_VALUE.
@ -124,13 +134,62 @@ public abstract class VideoPlayer implements VideoPlayerInstanceApi {
return value != Format.NO_VALUE ? value : null; 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 @Override
public void play() { public void play() {
setupAudioFocus();
requestAudioFocus();
exoPlayer.play(); exoPlayer.play();
} }
@Override @Override
public void pause() { public void pause() {
abandonAudioFocus();
exoPlayer.pause(); exoPlayer.pause();
} }
@ -459,6 +518,7 @@ public abstract class VideoPlayer implements VideoPlayerInstanceApi {
exoPlayerEventListener.dispose(); exoPlayerEventListener.dispose();
exoPlayerEventListener = null; exoPlayerEventListener = null;
} }
abandonAudioFocus();
exoPlayer.release(); exoPlayer.release();
} }
} }

View file

@ -28,11 +28,12 @@ public class PlatformViewVideoPlayer extends VideoPlayer {
@UnstableApi @UnstableApi
@VisibleForTesting @VisibleForTesting
public PlatformViewVideoPlayer( public PlatformViewVideoPlayer(
@NonNull Context context,
@NonNull VideoPlayerCallbacks events, @NonNull VideoPlayerCallbacks events,
@NonNull MediaItem mediaItem, @NonNull MediaItem mediaItem,
@NonNull VideoPlayerOptions options, @NonNull VideoPlayerOptions options,
@NonNull ExoPlayerProvider exoPlayerProvider) { @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 VideoAsset asset,
@NonNull VideoPlayerOptions options) { @NonNull VideoPlayerOptions options) {
return new PlatformViewVideoPlayer( return new PlatformViewVideoPlayer(
context,
events, events,
asset.getMediaItem(), asset.getMediaItem(),
options, options,

View file

@ -52,6 +52,7 @@ public final class TextureVideoPlayer extends VideoPlayer implements SurfaceProd
@NonNull VideoAsset asset, @NonNull VideoAsset asset,
@NonNull VideoPlayerOptions options) { @NonNull VideoPlayerOptions options) {
return new TextureVideoPlayer( return new TextureVideoPlayer(
context,
events, events,
surfaceProducer, surfaceProducer,
asset.getMediaItem(), asset.getMediaItem(),
@ -87,12 +88,13 @@ public final class TextureVideoPlayer extends VideoPlayer implements SurfaceProd
@UnstableApi @UnstableApi
@VisibleForTesting @VisibleForTesting
public TextureVideoPlayer( public TextureVideoPlayer(
@NonNull Context context,
@NonNull VideoPlayerCallbacks events, @NonNull VideoPlayerCallbacks events,
@NonNull SurfaceProducer surfaceProducer, @NonNull SurfaceProducer surfaceProducer,
@NonNull MediaItem mediaItem, @NonNull MediaItem mediaItem,
@NonNull VideoPlayerOptions options, @NonNull VideoPlayerOptions options,
@NonNull ExoPlayerProvider exoPlayerProvider) { @NonNull ExoPlayerProvider exoPlayerProvider) {
super(events, mediaItem, options, surfaceProducer, exoPlayerProvider); super(context, events, mediaItem, options, surfaceProducer, exoPlayerProvider);
surfaceProducer.setCallback(this); 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.ArgumentMatchers.any;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
import android.content.Context;
import android.view.Surface; import android.view.Surface;
import androidx.media3.common.AudioAttributes; import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C; import androidx.media3.common.C;
@ -65,7 +66,7 @@ public final class TextureVideoPlayerTest {
private TextureVideoPlayer createVideoPlayer(VideoPlayerOptions options) { private TextureVideoPlayer createVideoPlayer(VideoPlayerOptions options) {
return new TextureVideoPlayer( return new TextureVideoPlayer(
mockEvents, mockProducer, fakeVideoAsset.getMediaItem(), options, () -> mockExoPlayer); mock(Context.class), mockEvents, mockProducer, fakeVideoAsset.getMediaItem(), options, () -> mockExoPlayer);
} }
@Test @Test

View file

@ -128,6 +128,12 @@
error:(NSError **)outError { error:(NSError **)outError {
return [AVAudioSession.sharedInstance setCategory:category withOptions:options error: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 @end
#endif #endif

View file

@ -84,6 +84,7 @@ static NSDictionary<NSString *, NSValue *> *FVPGetPlayerItemObservations(void) {
self = [super init]; self = [super init];
NSAssert(self, @"super init cannot be nil"); NSAssert(self, @"super init cannot be nil");
_avFactory = avFactory;
_viewProvider = viewProvider; _viewProvider = viewProvider;
NSObject<FVPAVAsset> *asset = item.asset; NSObject<FVPAVAsset> *asset = item.asset;
@ -191,6 +192,11 @@ static NSDictionary<NSString *, NSValue *> *FVPGetPlayerItemObservations(void) {
if (_onDisposed) { if (_onDisposed) {
_onDisposed(); _onDisposed();
} }
#if TARGET_OS_IOS
if (_avFactory != nil) {
[[_avFactory sharedAudioSession] setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
}
#endif
[self.eventListener videoPlayerWasDisposed]; [self.eventListener videoPlayerWasDisposed];
} }
@ -218,6 +224,11 @@ static NSDictionary<NSString *, NSValue *> *FVPGetPlayerItemObservations(void) {
AVPlayerItem *p = [notification object]; AVPlayerItem *p = [notification object];
[p seekToTime:kCMTimeZero completionHandler:nil]; [p seekToTime:kCMTimeZero completionHandler:nil];
} else { } else {
#if TARGET_OS_IOS
if (_avFactory != nil) {
[[_avFactory sharedAudioSession] setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
}
#endif
[self.eventListener videoPlayerDidComplete]; [self.eventListener videoPlayerDidComplete];
} }
} }
@ -348,6 +359,11 @@ NS_INLINE CGFloat radiansToDegrees(CGFloat radians) {
} }
} else { } else {
[_player pause]; [_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 - (BOOL)setCategory:(AVAudioSessionCategory)category
withOptions:(AVAudioSessionCategoryOptions)options withOptions:(AVAudioSessionCategoryOptions)options
error:(NSError **)outError; error:(NSError **)outError;
/// Wraps the AVAudioSession method of the same name.
- (BOOL)setActive:(BOOL)active
withOptions:(AVAudioSessionSetActiveOptions)options
error:(NSError **)outError;
@end @end
#endif #endif

View file

@ -17,6 +17,8 @@ NS_ASSUME_NONNULL_BEGIN
@property(nonatomic, readonly) NSObject<FVPPixelBufferSource> *pixelBufferSource; @property(nonatomic, readonly) NSObject<FVPPixelBufferSource> *pixelBufferSource;
/// The view provider, to obtain view information from. /// The view provider, to obtain view information from.
@property(nonatomic, readonly, nullable) NSObject<FVPViewProvider> *viewProvider; @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. /// The preferred transform for the video. It can be used to handle the rotation of the video.
@property(nonatomic) CGAffineTransform preferredTransform; @property(nonatomic) CGAffineTransform preferredTransform;
/// The target playback speed requested by the plugin client. /// The target playback speed requested by the plugin client.