From b7e6d6f8418cf57dfa7f50cf010b86d01258530a Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Sun, 15 Sep 2024 21:57:14 +0200 Subject: [PATCH 1/4] chore(android): refactor audio focus Move audio focus management in a dedicated kotlin file create RNVPlayerInterface to be able to handle generic code Add lazy init of audioFocusDelegate (will not be instanciated if not necessary) --- .../common/api/RNVPlayerInterface.kt | 26 +++ .../common/toolbox/AudioManagerDelegate.kt | 105 ++++++++++++ .../exoplayer/ReactExoplayerView.java | 149 ++++++++---------- 3 files changed, 195 insertions(+), 85 deletions(-) create mode 100644 android/src/main/java/com/brentvatne/common/api/RNVPlayerInterface.kt create mode 100644 android/src/main/java/com/brentvatne/common/toolbox/AudioManagerDelegate.kt diff --git a/android/src/main/java/com/brentvatne/common/api/RNVPlayerInterface.kt b/android/src/main/java/com/brentvatne/common/api/RNVPlayerInterface.kt new file mode 100644 index 0000000000..f65fae4504 --- /dev/null +++ b/android/src/main/java/com/brentvatne/common/api/RNVPlayerInterface.kt @@ -0,0 +1,26 @@ +package com.brentvatne.common.api + +import com.brentvatne.common.react.VideoEventEmitter + +// RNVPlayerInterface is an abstraction of a player implementation +// It allows to use player in a generic code +interface RNVPlayerInterface { + // return true if playback is muted + + val isMuted: Boolean + + // return true if playback is ongoing and false when paused + val isPlaying: Boolean + + // return the eventEmitter associated to the player + val eventEmitter: VideoEventEmitter + + // pause player + fun pausePlayback() + + // decrease audio volume internally to handle audio ducking request + fun audioDuck() + + // decrease audio volume internally from ducking request + fun audioRestoreFromDuck() +} diff --git a/android/src/main/java/com/brentvatne/common/toolbox/AudioManagerDelegate.kt b/android/src/main/java/com/brentvatne/common/toolbox/AudioManagerDelegate.kt new file mode 100644 index 0000000000..114f4a790f --- /dev/null +++ b/android/src/main/java/com/brentvatne/common/toolbox/AudioManagerDelegate.kt @@ -0,0 +1,105 @@ +package com.brentvatne.common.toolbox + +import android.content.Context +import android.media.AudioManager +import android.media.AudioManager.OnAudioFocusChangeListener +import com.brentvatne.common.api.RNVPlayerInterface +import com.facebook.react.uimanager.ThemedReactContext + +/** + * Delegate audio management to this class + * This is an helper to group all generic android code which do not depend on player implementation + */ +class AudioManagerDelegate(player: RNVPlayerInterface, themedReactContext: ThemedReactContext) { + + companion object { + const val TAG = "AudioFocusDelegate" + } + + // indicates if audio focus shall be handled + var disableFocus: Boolean = false + // indicates app currently have audio focus + var hasAudioFocus = false + + val audioManager: AudioManager = themedReactContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager + + private val audioFocusChangeListener = OnAudioFocusChangedListener(player, themedReactContext, this) + + /** implementation of OnAudioFocusChangedListener + * It reports audio events to the app and request volume change to the player + **/ + private class OnAudioFocusChangedListener( + private val player: RNVPlayerInterface, + private val themedReactContext: ThemedReactContext, + private val audioFocusDelegate: AudioManagerDelegate + ) : + OnAudioFocusChangeListener { + override fun onAudioFocusChange(focusChange: Int) { + when (focusChange) { + AudioManager.AUDIOFOCUS_LOSS -> { + audioFocusDelegate.hasAudioFocus = false + player.eventEmitter.onAudioFocusChanged.invoke(false) + // FIXME this pause can cause issue if content doesn't have pause capability (can happen on live channel) + themedReactContext.currentActivity?.runOnUiThread(player::pausePlayback) + audioFocusDelegate.audioManager.abandonAudioFocus(this) + } + + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> player.eventEmitter.onAudioFocusChanged.invoke(false) + AudioManager.AUDIOFOCUS_GAIN -> { + audioFocusDelegate.hasAudioFocus = true + player.eventEmitter.onAudioFocusChanged.invoke(true) + } + + else -> { + DebugLog.e(TAG, "unhandled audioFocusChange $focusChange") + } + } + if (player.isPlaying) { + if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { + // Lower the volume + if (!player.isMuted) { + player.audioDuck() + } + } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { + // Raise it back to normal + if (!player.isMuted) { + player.audioRestoreFromDuck() + } + } + } + } + } + + /** + * request audio Focus + */ + fun requestAudioFocus(): Boolean { + if (disableFocus || hasAudioFocus) { + return true + } + val result: Int = audioManager.requestAudioFocus( + audioFocusChangeListener, + AudioManager.STREAM_MUSIC, + AudioManager.AUDIOFOCUS_GAIN + ) + hasAudioFocus = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED + return hasAudioFocus + } + + /** + * Abandon audio Focus + */ + fun abandonAudioFocus() { + audioManager.abandonAudioFocus(audioFocusChangeListener) + } + + /** + * change system audio output + */ + fun changeOutput(isSpeakerOutput: Boolean) { + audioManager.setMode( + if (isSpeakerOutput) AudioManager.MODE_NORMAL else AudioManager.MODE_IN_COMMUNICATION + ) + audioManager.setSpeakerphoneOn(isSpeakerOutput) + } +} diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index d9e94f4a0c..8deb320ed9 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -14,7 +14,6 @@ import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; -import android.media.AudioManager; import android.net.Uri; import android.os.Build; import android.os.Handler; @@ -106,6 +105,7 @@ import com.brentvatne.common.api.BufferingStrategy; import com.brentvatne.common.api.ControlsConfig; import com.brentvatne.common.api.DRMProps; +import com.brentvatne.common.api.RNVPlayerInterface; import com.brentvatne.common.api.ResizeMode; import com.brentvatne.common.api.SideLoadedTextTrack; import com.brentvatne.common.api.Source; @@ -114,6 +114,7 @@ import com.brentvatne.common.api.Track; import com.brentvatne.common.api.VideoTrack; import com.brentvatne.common.react.VideoEventEmitter; +import com.brentvatne.common.toolbox.AudioManagerDelegate; import com.brentvatne.common.toolbox.DebugLog; import com.brentvatne.common.toolbox.ReactBridgeUtils; import com.brentvatne.react.BuildConfig; @@ -139,7 +140,6 @@ import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Objects; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; @@ -155,7 +155,8 @@ public class ReactExoplayerView extends FrameLayout implements BecomingNoisyListener, DrmSessionEventListener, AdEvent.AdEventListener, - AdErrorEvent.AdErrorListener { + AdErrorEvent.AdErrorListener, + RNVPlayerInterface { public static final double DEFAULT_MAX_HEAP_ALLOCATION_PERCENT = 1; public static final double DEFAULT_MIN_BUFFER_MEMORY_RESERVE = 0; @@ -170,7 +171,8 @@ public class ReactExoplayerView extends FrameLayout implements DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); } - protected final VideoEventEmitter eventEmitter; + public final VideoEventEmitter eventEmitter = new VideoEventEmitter(); + private final ReactExoplayerConfig config; private final DefaultBandwidthMeter bandwidthMeter; private LegacyPlayerControlView playerControlView; @@ -201,7 +203,6 @@ public class ReactExoplayerView extends FrameLayout implements private boolean isPaused; private boolean isBuffering; private boolean muted = false; - private boolean hasAudioFocus = false; private float rate = 1f; private AudioOutput audioOutput = AudioOutput.SPEAKER; private float audioVolume = 1f; @@ -249,9 +250,8 @@ public class ReactExoplayerView extends FrameLayout implements // React private final ThemedReactContext themedReactContext; - private final AudioManager audioManager; private final AudioBecomingNoisyReceiver audioBecomingNoisyReceiver; - private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener; + private AudioManagerDelegate audioFocusDelegate; // store last progress event values to avoid sending unnecessary messages private long lastPos = -1; @@ -268,6 +268,13 @@ public void setCmcdConfigurationFactory(CmcdConfiguration.Factory factory) { this.cmcdConfigurationFactory = factory; } + public boolean isPlaying() { + if (player == null || !player.getPlayWhenReady()) { + return false; + } + return true; + } + private void updateProgress() { if (player != null) { if (playerControlView != null && isPlayingAd() && controls) { @@ -313,16 +320,21 @@ public double getPositionInFirstPeriodMsForCurrentWindow(long currentPosition) { public ReactExoplayerView(ThemedReactContext context, ReactExoplayerConfig config) { super(context); this.themedReactContext = context; - this.eventEmitter = new VideoEventEmitter(); this.config = config; this.bandwidthMeter = config.getBandwidthMeter(); mainHandler = new Handler(); createViews(); - audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); themedReactContext.addLifecycleEventListener(this); audioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(themedReactContext); - audioFocusChangeListener = new OnAudioFocusChangedListener(this, themedReactContext); + } + + private AudioManagerDelegate getAudioFocusDelegate() { + if (audioFocusDelegate == null) { + audioFocusDelegate = new AudioManagerDelegate(this, themedReactContext); + audioFocusDelegate.setDisableFocus(disableFocus); + } + return audioFocusDelegate; } private boolean isPlayingAd() { @@ -587,6 +599,17 @@ public void setViewType(int viewType) { exoPlayerView.updateSurfaceView(viewType); } + @NonNull + @Override + public VideoEventEmitter getEventEmitter() { + return eventEmitter; + } + + @Override + public boolean isMuted() { + return muted; + } + private class RNVLoadControl extends DefaultLoadControl { private final int availableHeapInBytes; private final Runtime runtime; @@ -1192,78 +1215,14 @@ private void releasePlayer() { } } - private static class OnAudioFocusChangedListener implements AudioManager.OnAudioFocusChangeListener { - private final ReactExoplayerView view; - private final ThemedReactContext themedReactContext; - - private OnAudioFocusChangedListener(ReactExoplayerView view, ThemedReactContext themedReactContext) { - this.view = view; - this.themedReactContext = themedReactContext; - } - - @Override - public void onAudioFocusChange(int focusChange) { - Activity activity = themedReactContext.getCurrentActivity(); - - switch (focusChange) { - case AudioManager.AUDIOFOCUS_LOSS: - view.hasAudioFocus = false; - view.eventEmitter.onAudioFocusChanged.invoke(false); - // FIXME this pause can cause issue if content doesn't have pause capability (can happen on live channel) - if (activity != null) { - activity.runOnUiThread(view::pausePlayback); - } - view.audioManager.abandonAudioFocus(this); - break; - case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: - view.eventEmitter.onAudioFocusChanged.invoke(false); - break; - case AudioManager.AUDIOFOCUS_GAIN: - view.hasAudioFocus = true; - view.eventEmitter.onAudioFocusChanged.invoke(true); - break; - default: - break; - } - - if (view.player != null && activity != null) { - if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { - // Lower the volume - if (!view.muted) { - activity.runOnUiThread(() -> - view.player.setVolume(view.audioVolume * 0.8f) - ); - } - } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { - // Raise it back to normal - if (!view.muted) { - activity.runOnUiThread(() -> - view.player.setVolume(view.audioVolume * 1) - ); - } - } - } - } - } - - private boolean requestAudioFocus() { - if (disableFocus || source.getUri() == null || this.hasAudioFocus) { - return true; - } - int result = audioManager.requestAudioFocus(audioFocusChangeListener, - AudioManager.STREAM_MUSIC, - AudioManager.AUDIOFOCUS_GAIN); - return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED; - } - private void setPlayWhenReady(boolean playWhenReady) { if (player == null) { return; } if (playWhenReady) { - this.hasAudioFocus = requestAudioFocus(); - if (this.hasAudioFocus) { + boolean hasAudioGranted = getAudioFocusDelegate().requestAudioFocus(); + if (hasAudioGranted) { player.setPlayWhenReady(true); } } else { @@ -1280,7 +1239,7 @@ private void resumePlayback() { } } - private void pausePlayback() { + public void pausePlayback() { if (player != null) { if (player.getPlayWhenReady()) { setPlayWhenReady(false); @@ -1295,7 +1254,7 @@ private void stopPlayback() { } private void onStopPlayback() { - audioManager.abandonAudioFocus(audioFocusChangeListener); + getAudioFocusDelegate().abandonAudioFocus(); } private void updateResumePosition() { @@ -2136,12 +2095,8 @@ private void changeAudioOutput(AudioOutput output) { .setContentType(contentType) .build(); player.setAudioAttributes(audioAttributes, false); - AudioManager audioManager = (AudioManager) themedReactContext.getSystemService(Context.AUDIO_SERVICE); boolean isSpeakerOutput = output == AudioOutput.SPEAKER; - audioManager.setMode( - isSpeakerOutput ? AudioManager.MODE_NORMAL - : AudioManager.MODE_IN_COMMUNICATION); - audioManager.setSpeakerphoneOn(isSpeakerOutput); + getAudioFocusDelegate().changeOutput(isSpeakerOutput); } } @@ -2154,7 +2109,7 @@ public void setAudioOutput(AudioOutput output) { public void setVolumeModifier(float volume) { audioVolume = volume; - if (player != null) { + if (player != null && !muted) { player.setVolume(audioVolume); } } @@ -2199,6 +2154,11 @@ public void setPlayInBackground(boolean playInBackground) { public void setDisableFocus(boolean disableFocus) { this.disableFocus = disableFocus; + // do not use getAudioFocusDelegate() + // should not be created here as we are not sure the playback is really required + if (audioFocusDelegate != null) { + audioFocusDelegate.setDisableFocus(disableFocus); + } } public void setFocusable(boolean focusable) { @@ -2380,4 +2340,23 @@ public void setControlsStyles(ControlsConfig controlsStyles) { controlsConfig = controlsStyles; refreshProgressBarVisibility(); } -} \ No newline at end of file + + public void audioDuck() { + if (themedReactContext.getCurrentActivity() != null && !muted) { + themedReactContext.getCurrentActivity().runOnUiThread(() -> { + player.setVolume(audioVolume * 0.8f); + }); + } + } + + public void audioRestoreFromDuck() { + if (themedReactContext.getCurrentActivity() != null) { + themedReactContext.getCurrentActivity().runOnUiThread(() -> { + if (!muted) { + player.setVolume(audioVolume); + } + }); + } + } + +} From 5d5c5cefb6d90ef915b20d0a7ddbc7c3f2dba0f1 Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Sun, 15 Sep 2024 23:34:00 +0200 Subject: [PATCH 2/4] chore(android): fix linter --- .../com/brentvatne/common/toolbox/AudioManagerDelegate.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/android/src/main/java/com/brentvatne/common/toolbox/AudioManagerDelegate.kt b/android/src/main/java/com/brentvatne/common/toolbox/AudioManagerDelegate.kt index 114f4a790f..c3a5467d3d 100644 --- a/android/src/main/java/com/brentvatne/common/toolbox/AudioManagerDelegate.kt +++ b/android/src/main/java/com/brentvatne/common/toolbox/AudioManagerDelegate.kt @@ -18,6 +18,7 @@ class AudioManagerDelegate(player: RNVPlayerInterface, themedReactContext: Theme // indicates if audio focus shall be handled var disableFocus: Boolean = false + // indicates app currently have audio focus var hasAudioFocus = false @@ -26,14 +27,13 @@ class AudioManagerDelegate(player: RNVPlayerInterface, themedReactContext: Theme private val audioFocusChangeListener = OnAudioFocusChangedListener(player, themedReactContext, this) /** implementation of OnAudioFocusChangedListener - * It reports audio events to the app and request volume change to the player + * It reports audio events to the app and request volume change to the player **/ private class OnAudioFocusChangedListener( private val player: RNVPlayerInterface, private val themedReactContext: ThemedReactContext, private val audioFocusDelegate: AudioManagerDelegate - ) : - OnAudioFocusChangeListener { + ) : OnAudioFocusChangeListener { override fun onAudioFocusChange(focusChange: Int) { when (focusChange) { AudioManager.AUDIOFOCUS_LOSS -> { @@ -45,6 +45,7 @@ class AudioManagerDelegate(player: RNVPlayerInterface, themedReactContext: Theme } AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> player.eventEmitter.onAudioFocusChanged.invoke(false) + AudioManager.AUDIOFOCUS_GAIN -> { audioFocusDelegate.hasAudioFocus = true player.eventEmitter.onAudioFocusChanged.invoke(true) From 0b99c44ec7dd391681f2149f0825f719ca8e609e Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Tue, 17 Sep 2024 22:26:18 +0200 Subject: [PATCH 3/4] fix(android): deprecate disableFocus and implement mixWithOthers --- .../brentvatne/common/api/MixWithOthers.kt | 39 +++++++++++++++++++ .../common/toolbox/AudioManagerDelegate.kt | 7 +++- .../exoplayer/ReactExoplayerView.java | 11 +++--- .../exoplayer/ReactExoplayerViewManager.kt | 9 +++-- docs/pages/component/props.mdx | 11 ++++-- src/Video.tsx | 13 ++++++- src/specs/VideoNativeComponent.ts | 1 - src/types/video.ts | 3 +- 8 files changed, 76 insertions(+), 18 deletions(-) create mode 100644 android/src/main/java/com/brentvatne/common/api/MixWithOthers.kt diff --git a/android/src/main/java/com/brentvatne/common/api/MixWithOthers.kt b/android/src/main/java/com/brentvatne/common/api/MixWithOthers.kt new file mode 100644 index 0000000000..208c41038e --- /dev/null +++ b/android/src/main/java/com/brentvatne/common/api/MixWithOthers.kt @@ -0,0 +1,39 @@ +package com.brentvatne.common.api + +import androidx.annotation.IntDef +import kotlin.annotation.Retention + +internal object MixWithOthers { + /** + * Either the width or height is decreased to obtain the desired aspect ratio. + */ + const val MIX_INHERIT = 0 + + /** + * The width is fixed and the height is increased or decreased to obtain the desired aspect ratio. + */ + const val MIX_DUCK = 1 + + /** + * The height is fixed and the width is increased or decreased to obtain the desired aspect ratio. + */ + const val MIX_MIX = 2 + + @JvmStatic + @Mode + fun toMixWithOthers(ordinal: String): Int = + when (ordinal) { + "inherit" -> MIX_INHERIT + "duck" -> MIX_DUCK + "mix" -> MIX_MIX + else -> MIX_INHERIT + } + + @Retention(AnnotationRetention.SOURCE) + @IntDef( + MIX_INHERIT, + MIX_DUCK, + MIX_MIX, + ) + annotation class Mode +} diff --git a/android/src/main/java/com/brentvatne/common/toolbox/AudioManagerDelegate.kt b/android/src/main/java/com/brentvatne/common/toolbox/AudioManagerDelegate.kt index c3a5467d3d..ba03f6af1a 100644 --- a/android/src/main/java/com/brentvatne/common/toolbox/AudioManagerDelegate.kt +++ b/android/src/main/java/com/brentvatne/common/toolbox/AudioManagerDelegate.kt @@ -3,6 +3,9 @@ package com.brentvatne.common.toolbox import android.content.Context import android.media.AudioManager import android.media.AudioManager.OnAudioFocusChangeListener +import androidx.annotation.IntDef +import androidx.media3.common.FileTypes.Type +import com.brentvatne.common.api.MixWithOthers import com.brentvatne.common.api.RNVPlayerInterface import com.facebook.react.uimanager.ThemedReactContext @@ -17,7 +20,7 @@ class AudioManagerDelegate(player: RNVPlayerInterface, themedReactContext: Theme } // indicates if audio focus shall be handled - var disableFocus: Boolean = false + var mixWithOthers: Int = MixWithOthers.MIX_INHERIT // indicates app currently have audio focus var hasAudioFocus = false @@ -75,7 +78,7 @@ class AudioManagerDelegate(player: RNVPlayerInterface, themedReactContext: Theme * request audio Focus */ fun requestAudioFocus(): Boolean { - if (disableFocus || hasAudioFocus) { + if (mixWithOthers == MixWithOthers.MIX_MIX || hasAudioFocus) { return true } val result: Int = audioManager.requestAudioFocus( diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 8deb320ed9..c999499057 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -105,6 +105,7 @@ import com.brentvatne.common.api.BufferingStrategy; import com.brentvatne.common.api.ControlsConfig; import com.brentvatne.common.api.DRMProps; +import com.brentvatne.common.api.MixWithOthers; import com.brentvatne.common.api.RNVPlayerInterface; import com.brentvatne.common.api.ResizeMode; import com.brentvatne.common.api.SideLoadedTextTrack; @@ -233,7 +234,7 @@ public class ReactExoplayerView extends FrameLayout implements private String videoTrackValue; private String textTrackType = "disabled"; private String textTrackValue; - private boolean disableFocus; + private int mixWithOthers = MixWithOthers.MIX_INHERIT; private boolean focusable = true; private BufferingStrategy.BufferingStrategyEnum bufferingStrategy; private boolean disableDisconnectError; @@ -332,7 +333,7 @@ public ReactExoplayerView(ThemedReactContext context, ReactExoplayerConfig confi private AudioManagerDelegate getAudioFocusDelegate() { if (audioFocusDelegate == null) { audioFocusDelegate = new AudioManagerDelegate(this, themedReactContext); - audioFocusDelegate.setDisableFocus(disableFocus); + audioFocusDelegate.setMixWithOthers(mixWithOthers); } return audioFocusDelegate; } @@ -2152,12 +2153,12 @@ public void setPlayInBackground(boolean playInBackground) { this.playInBackground = playInBackground; } - public void setDisableFocus(boolean disableFocus) { - this.disableFocus = disableFocus; + public void setMixWithOthers(int _mixWithOthers) { + mixWithOthers = _mixWithOthers; // do not use getAudioFocusDelegate() // should not be created here as we are not sure the playback is really required if (audioFocusDelegate != null) { - audioFocusDelegate.setDisableFocus(disableFocus); + audioFocusDelegate.setMixWithOthers(mixWithOthers); } } diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.kt b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.kt index a21b29a325..658fa98e35 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.kt +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.kt @@ -7,6 +7,7 @@ import android.util.Log import com.brentvatne.common.api.BufferConfig import com.brentvatne.common.api.BufferingStrategy import com.brentvatne.common.api.ControlsConfig +import com.brentvatne.common.api.MixWithOthers import com.brentvatne.common.api.ResizeMode import com.brentvatne.common.api.Source import com.brentvatne.common.api.SubtitleStyle @@ -49,7 +50,7 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View private const val PROP_MIN_LOAD_RETRY_COUNT = "minLoadRetryCount" private const val PROP_MAXIMUM_BIT_RATE = "maxBitRate" private const val PROP_PLAY_IN_BACKGROUND = "playInBackground" - private const val PROP_DISABLE_FOCUS = "disableFocus" + private const val PROP_DISABLE_MIX_WITH_OTHERS = "mixWithOthers" private const val PROP_BUFFERING_STRATEGY = "bufferingStrategy" private const val PROP_DISABLE_DISCONNECT_ERROR = "disableDisconnectError" private const val PROP_FOCUSABLE = "focusable" @@ -226,9 +227,9 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View videoView.setPlayInBackground(playInBackground) } - @ReactProp(name = PROP_DISABLE_FOCUS, defaultBoolean = false) - fun setDisableFocus(videoView: ReactExoplayerView, disableFocus: Boolean) { - videoView.setDisableFocus(disableFocus) + @ReactProp(name = PROP_DISABLE_MIX_WITH_OTHERS) + fun setMixWithOthers(videoView: ReactExoplayerView, disableFocus: String) { + videoView.setMixWithOthers(MixWithOthers.toMixWithOthers(disableFocus)) } @ReactProp(name = PROP_FOCUSABLE, defaultBoolean = true) diff --git a/docs/pages/component/props.mdx b/docs/pages/component/props.mdx index e3069d17d2..f0ee22abcf 100644 --- a/docs/pages/component/props.mdx +++ b/docs/pages/component/props.mdx @@ -199,12 +199,15 @@ debug={{ ### `disableFocus` +> [!WARNING] +> Deprecated, use mixWithOthers instead + Determines whether video audio should override background music/audio in Android devices. -- **false (default)** - Override background audio/music -- **true** - Let background audio/music from other apps play +- **false (default)** - Override background audio/music. Equivalent to mixWithOthers={'inherit'} +- **true** - Let background audio/music from other apps play. equivalent to mixWithOthers={'duck'} Note: Allows multiple videos to play if set to `true`. If `false`, when one video is playing and another is started, the first video will be paused. @@ -383,11 +386,11 @@ minLoadRetryCount={5} // retry 5 times ### `mixWithOthers` - + Controls how Audio mix with other apps. -- **"inherit" (default)** - Use the default AVPlayer behavior +- **"inherit" (default)** - Use the default player behavior - **"mix"** - Audio from this video mixes with audio from other apps. - **"duck"** - Reduces the volume of other apps while audio from this video plays. diff --git a/src/Video.tsx b/src/Video.tsx index 66f11179a3..b5c1e30dd1 100644 --- a/src/Video.tsx +++ b/src/Video.tsx @@ -46,7 +46,7 @@ import { } from './utils'; import NativeVideoManager from './specs/NativeVideoManager'; import type {VideoSaveData} from './specs/NativeVideoManager'; -import {CmcdMode, ViewType} from './types'; +import {CmcdMode, MixWithOthersType, ViewType} from './types'; import type { OnLoadData, OnTextTracksData, @@ -87,6 +87,8 @@ const Video = forwardRef( selectedTextTrack, useTextureView, useSecureView, + disableFocus, + mixWithOthers, viewType, shutterColor, onLoadStart, @@ -722,6 +724,14 @@ const Video = forwardRef( }), [showPoster], ); + if (disableFocus != undefined && mixWithOthers) { + console.warn('disableFocus is deprecated, please use only mixWithOthers'); + } + const _mixWithOthers = mixWithOthers + ? mixWithOthers + : disableFocus + ? MixWithOthersType.DUCK + : MixWithOthersType.INHERIT; return ( @@ -738,6 +748,7 @@ const Video = forwardRef( selectedAudioTrack={_selectedAudioTrack} selectedVideoTrack={_selectedVideoTrack} shutterColor={_shutterColor} + mixWithOthers={_mixWithOthers} onGetLicense={usingExternalGetLicense ? onGetLicense : undefined} onVideoLoad={ onLoad || hasPoster diff --git a/src/specs/VideoNativeComponent.ts b/src/specs/VideoNativeComponent.ts index f210cfccfb..cc04ee3ef2 100644 --- a/src/specs/VideoNativeComponent.ts +++ b/src/specs/VideoNativeComponent.ts @@ -312,7 +312,6 @@ export interface VideoNativeProps extends ViewProps { adTagUrl?: string; adLanguage?: string; allowsExternalPlayback?: boolean; // ios, true - disableFocus?: boolean; // android maxBitRate?: Float; resizeMode?: WithDefault; repeat?: boolean; diff --git a/src/types/video.ts b/src/types/video.ts index 8aa9b8d846..9d6b247b07 100644 --- a/src/types/video.ts +++ b/src/types/video.ts @@ -270,6 +270,7 @@ export interface ReactVideoProps extends ReactVideoEvents, ViewProps { contentStartTime?: number; // Android controls?: boolean; currentPlaybackTime?: number; // Android + /** @deprecated Use mixWithOthers */ disableFocus?: boolean; disableDisconnectError?: boolean; // Android filter?: EnumValues; // iOS @@ -282,7 +283,7 @@ export interface ReactVideoProps extends ReactVideoEvents, ViewProps { ignoreSilentSwitch?: EnumValues; // iOS minLoadRetryCount?: number; // Android maxBitRate?: number; - mixWithOthers?: EnumValues; // iOS + mixWithOthers?: EnumValues; // iOS, android muted?: boolean; paused?: boolean; pictureInPicture?: boolean; // iOS From fd746890530be7b8ce4199774c945194ca8f5c83 Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Tue, 17 Sep 2024 23:11:39 +0200 Subject: [PATCH 4/4] chore: use lambda --- .../java/com/brentvatne/exoplayer/ReactExoplayerView.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index c999499057..ce12023cd7 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -2344,9 +2344,7 @@ public void setControlsStyles(ControlsConfig controlsStyles) { public void audioDuck() { if (themedReactContext.getCurrentActivity() != null && !muted) { - themedReactContext.getCurrentActivity().runOnUiThread(() -> { - player.setVolume(audioVolume * 0.8f); - }); + themedReactContext.getCurrentActivity().runOnUiThread(() -> player.setVolume(audioVolume * 0.8f)); } }