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 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue