fix audio issue
This commit is contained in:
parent
d1d70e1559
commit
b874b9cc30
8 changed files with 103 additions and 10 deletions
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue