expo-video 1.2.3 → 1.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/android/build.gradle +2 -2
  3. package/android/src/main/AndroidManifest.xml +1 -1
  4. package/android/src/main/java/expo/modules/video/AudioFocusManager.kt +187 -0
  5. package/android/src/main/java/expo/modules/video/PlayerEvent.kt +54 -0
  6. package/android/src/main/java/expo/modules/video/VideoManager.kt +11 -2
  7. package/android/src/main/java/expo/modules/video/VideoModule.kt +10 -5
  8. package/android/src/main/java/expo/modules/video/VideoPlayer.kt +59 -109
  9. package/android/src/main/java/expo/modules/video/VideoPlayerListener.kt +18 -0
  10. package/android/src/main/java/expo/modules/video/VideoView.kt +11 -2
  11. package/android/src/main/java/expo/modules/video/delegates/IgnoreSameSet.kt +24 -0
  12. package/android/src/main/java/expo/modules/video/drawing/OutlineProvider.kt +1 -1
  13. package/android/src/main/java/expo/modules/video/{ContentFit.kt → enums/ContentFit.kt} +1 -1
  14. package/android/src/main/java/expo/modules/video/{ExpoVideoPlaybackService.kt → playbackService/ExpoVideoPlaybackService.kt} +20 -1
  15. package/android/src/main/java/expo/modules/video/playbackService/PlaybackServiceConnection.kt +36 -0
  16. package/android/src/main/java/expo/modules/video/{VideoMediaSessionCallback.kt → playbackService/VideoMediaSessionCallback.kt} +1 -1
  17. package/android/src/main/java/expo/modules/video/records/VideoSource.kt +3 -2
  18. package/android/src/main/java/expo/modules/video/records/VolumeEvent.kt +1 -1
  19. package/android/src/main/java/expo/modules/video/{DataSourceUtils.kt → utils/DataSourceUtils.kt} +13 -2
  20. package/android/src/main/java/expo/modules/video/{YogaUtils.kt → utils/YogaUtils.kt} +1 -1
  21. package/package.json +2 -2
  22. package/android/src/main/java/expo/modules/video/VideoPlayerAudioFocusManager.kt +0 -105
package/CHANGELOG.md CHANGED
@@ -10,6 +10,23 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 1.2.5 — 2024-08-20
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - [Android] Fixed `resolvedLayoutDirection` building issues when using react-native 0.75.X. ([#31064](https://github.com/expo/expo/pull/31064) by [@gabrieldonadel](https://github.com/gabrieldonadel))
18
+
19
+ ## 1.2.4 — 2024-07-30
20
+
21
+ ### 🐛 Bug fixes
22
+
23
+ - [Android] Fix Audio Manager pausing player on the wrong thread and conflicts between players. ([#30453](https://github.com/expo/expo/pull/30453) by [@behenate](https://github.com/behenate))
24
+ - [Android] Fix Audio Manager pausing player on the wrong thread and conflicts between players. ([#30453](https://github.com/expo/expo/pull/30453) by [@behenate](https://github.com/behenate))
25
+
26
+ ### 💡 Others
27
+
28
+ - [Android] Refactor `VideoPlayer.kt`, organize files ([#30452](https://github.com/expo/expo/pull/30452) by [@behenate](https://github.com/behenate))
29
+
13
30
  ## 1.2.3 — 2024-07-11
14
31
 
15
32
  ### 🛠 Breaking changes
@@ -25,6 +42,7 @@
25
42
 
26
43
  - [Web] Fix `AudioContext` being created before user interaction causing playback issues. ([#29695](https://github.com/expo/expo/pull/29695) by [@behenate](https://github.com/behenate))
27
44
  - [iOS] Fix a race condition causing crashes when deallocating the player. ([#30022](https://github.com/expo/expo/pull/30022) by [@behenate](https://github.com/behenate))
45
+ - Add missing `react` and `react-native` peer dependencies for isolated modules. ([#30489](https://github.com/expo/expo/pull/30489) by [@byCedric](https://github.com/byCedric))
28
46
 
29
47
  ## 1.2.2 — 2024-07-03
30
48
 
@@ -1,7 +1,7 @@
1
1
  apply plugin: 'com.android.library'
2
2
 
3
3
  group = 'host.exp.exponent'
4
- version = '1.2.3'
4
+ version = '1.2.5'
5
5
 
6
6
  def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
7
7
  apply from: expoModulesCorePlugin
@@ -14,7 +14,7 @@ android {
14
14
  namespace "expo.modules.video"
15
15
  defaultConfig {
16
16
  versionCode 1
17
- versionName '1.2.3'
17
+ versionName '1.2.5'
18
18
  }
19
19
  }
20
20
 
@@ -6,7 +6,7 @@
6
6
  <application>
7
7
  <activity android:name=".FullscreenPlayerActivity" />
8
8
  <service
9
- android:name=".ExpoVideoPlaybackService"
9
+ android:name=".playbackService.ExpoVideoPlaybackService"
10
10
  android:exported="false"
11
11
  android:foregroundServiceType="mediaPlayback">
12
12
  <intent-filter>
@@ -0,0 +1,187 @@
1
+ package expo.modules.video
2
+
3
+ import android.content.Context
4
+ import android.media.AudioAttributes
5
+ import android.media.AudioFocusRequest
6
+ import android.media.AudioManager
7
+ import android.os.Build
8
+ import androidx.media3.common.util.UnstableApi
9
+ import expo.modules.kotlin.AppContext
10
+ import expo.modules.video.records.VolumeEvent
11
+ import kotlinx.coroutines.launch
12
+ import java.lang.ref.WeakReference
13
+
14
+ @UnstableApi
15
+ class AudioFocusManager(private val appContext: AppContext) : AudioManager.OnAudioFocusChangeListener, VideoPlayerListener {
16
+ private val audioManager by lazy {
17
+ appContext.reactContext?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager ?: run {
18
+ throw FailedToGetAudioFocusManagerException()
19
+ }
20
+ }
21
+
22
+ private var players: MutableList<WeakReference<VideoPlayer>> = mutableListOf()
23
+ private var currentFocusRequest: AudioFocusRequest? = null
24
+ private val anyPlayerRequiresFocus: Boolean
25
+ get() = players.any {
26
+ playerRequiresFocus(it)
27
+ }
28
+
29
+ private fun requestAudioFocus() {
30
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
31
+ // We already have audio focus
32
+ if (currentFocusRequest != null) {
33
+ return
34
+ }
35
+
36
+ val newFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run {
37
+ setAudioAttributes(
38
+ AudioAttributes.Builder().run {
39
+ setUsage(AudioAttributes.USAGE_MEDIA)
40
+ setContentType(AudioAttributes.CONTENT_TYPE_MOVIE)
41
+ setOnAudioFocusChangeListener(this@AudioFocusManager)
42
+ build()
43
+ }
44
+ ).build()
45
+ }
46
+ this.currentFocusRequest = newFocusRequest
47
+ audioManager.requestAudioFocus(newFocusRequest)
48
+ } else {
49
+ @Suppress("DEPRECATION")
50
+ audioManager.requestAudioFocus(
51
+ this,
52
+ AudioManager.STREAM_MUSIC,
53
+ AudioManager.AUDIOFOCUS_GAIN
54
+ )
55
+ }
56
+ }
57
+
58
+ private fun abandonAudioFocus() {
59
+ currentFocusRequest?.let {
60
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
61
+ audioManager.abandonAudioFocusRequest(it)
62
+ } else {
63
+ @Suppress("DEPRECATION")
64
+ audioManager.abandonAudioFocus(this)
65
+ }
66
+ }
67
+ currentFocusRequest = null
68
+ }
69
+
70
+ fun registerPlayer(player: VideoPlayer) {
71
+ players.find { it.get() == player } ?: run {
72
+ players.add(WeakReference(player))
73
+ }
74
+ player.addListener(this)
75
+ updateAudioFocus()
76
+ }
77
+
78
+ fun unregisterPlayer(player: VideoPlayer) {
79
+ player.removeListener(this)
80
+ players.removeAll { it.get() == player }
81
+ updateAudioFocus()
82
+ }
83
+
84
+ // VideoPlayerListener
85
+
86
+ override fun onIsPlayingChanged(player: VideoPlayer, isPlaying: Boolean, oldIsPlaying: Boolean?) {
87
+ // we can't use `updateAudioFocus`, because when losing focus the videos are paused sequentially,
88
+ // which can lead to unexpected results.
89
+ if (!isPlaying && !anyPlayerRequiresFocus) {
90
+ abandonAudioFocus()
91
+ } else if (isPlaying && anyPlayerRequiresFocus) {
92
+ requestAudioFocus()
93
+ }
94
+ }
95
+
96
+ override fun onVolumeChanged(player: VideoPlayer, newValue: VolumeEvent, oldVolume: VolumeEvent?) {
97
+ updateAudioFocus()
98
+ }
99
+
100
+ // AudioManager.OnAudioFocusChangeListener
101
+
102
+ override fun onAudioFocusChange(focusChange: Int) {
103
+ when (focusChange) {
104
+ AudioManager.AUDIOFOCUS_LOSS -> {
105
+ appContext.mainQueue.launch {
106
+ players.forEach {
107
+ pausePlayerIfUnmuted(it)
108
+ }
109
+ currentFocusRequest = null
110
+ }
111
+ }
112
+
113
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
114
+ appContext.mainQueue.launch {
115
+ players.forEach {
116
+ pausePlayerIfUnmuted(it)
117
+ }
118
+ currentFocusRequest = null
119
+ }
120
+ }
121
+
122
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
123
+ appContext.mainQueue.launch {
124
+ players.forEach {
125
+ duckPlayer(it)
126
+ }
127
+ }
128
+ }
129
+
130
+ AudioManager.AUDIOFOCUS_GAIN -> {
131
+ // TODO: For now this behaves like iOS and doesn't resume playback automatically
132
+ // In future versions we can add a prop to control this behavior.
133
+ appContext.mainQueue.launch {
134
+ players.forEach {
135
+ unduckPlayer(it)
136
+ }
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ // Utils
143
+
144
+ private fun playerRequiresFocus(weakPlayer: WeakReference<VideoPlayer>): Boolean {
145
+ return weakPlayer.get()?.let {
146
+ !it.muted && it.playing && it.volume > 0
147
+ } ?: run {
148
+ false
149
+ }
150
+ }
151
+
152
+ private fun pausePlayerIfUnmuted(weakPlayer: WeakReference<VideoPlayer>) {
153
+ weakPlayer.get()?.let { videoPlayer ->
154
+ if (!videoPlayer.muted) {
155
+ appContext.mainQueue.launch {
156
+ videoPlayer.player.pause()
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ private fun duckPlayer(weakPlayer: WeakReference<VideoPlayer>) {
163
+ weakPlayer.get()?.let { player ->
164
+ appContext.mainQueue.launch {
165
+ player.volume /= 2f
166
+ }
167
+ }
168
+ }
169
+
170
+ private fun unduckPlayer(weakPlayer: WeakReference<VideoPlayer>) {
171
+ weakPlayer.get()?.let { player ->
172
+ if (!player.muted) {
173
+ appContext.mainQueue.launch {
174
+ player.volume = player.userVolume
175
+ }
176
+ }
177
+ }
178
+ }
179
+
180
+ private fun updateAudioFocus() {
181
+ if (anyPlayerRequiresFocus) {
182
+ requestAudioFocus()
183
+ } else {
184
+ abandonAudioFocus()
185
+ }
186
+ }
187
+ }
@@ -0,0 +1,54 @@
1
+ package expo.modules.video
2
+
3
+ import androidx.annotation.OptIn
4
+ import androidx.media3.common.util.UnstableApi
5
+ import expo.modules.video.enums.PlayerStatus
6
+ import expo.modules.video.records.PlaybackError
7
+ import expo.modules.video.records.VideoSource
8
+ import expo.modules.video.records.VolumeEvent
9
+
10
+ @OptIn(UnstableApi::class)
11
+ sealed class PlayerEvent {
12
+ open val name: String = ""
13
+ open val arguments: Array<out Any?> = arrayOf()
14
+
15
+ data class StatusChanged(val status: PlayerStatus, val oldStatus: PlayerStatus?, val error: PlaybackError?) : PlayerEvent() {
16
+ override val name = "statusChange"
17
+ override val arguments = arrayOf(status, oldStatus, error)
18
+ }
19
+
20
+ data class IsPlayingChanged(val isPlaying: Boolean, val oldIsPlaying: Boolean?) : PlayerEvent() {
21
+ override val name = "playingChange"
22
+ override val arguments = arrayOf(isPlaying, oldIsPlaying)
23
+ }
24
+
25
+ data class VolumeChanged(val newValue: VolumeEvent, val oldValue: VolumeEvent?) : PlayerEvent() {
26
+ override val name = "playingChange"
27
+ override val arguments = arrayOf(newValue, oldValue)
28
+ }
29
+
30
+ data class SourceChanged(val source: VideoSource?, val oldSource: VideoSource?) : PlayerEvent() {
31
+ override val name = "sourceChange"
32
+ override val arguments = arrayOf(source, oldSource)
33
+ }
34
+
35
+ data class PlaybackRateChanged(val rate: Float, val oldRate: Float?) : PlayerEvent() {
36
+ override val name = "playbackRateChange"
37
+ override val arguments = arrayOf(rate, oldRate)
38
+ }
39
+
40
+ class PlayedToEnd : PlayerEvent() {
41
+ override val name = "playToEnd"
42
+ }
43
+
44
+ fun emit(player: VideoPlayer, listeners: List<VideoPlayerListener>) {
45
+ when (this) {
46
+ is StatusChanged -> listeners.forEach { it.onStatusChanged(player, status, oldStatus, error) }
47
+ is IsPlayingChanged -> listeners.forEach { it.onIsPlayingChanged(player, isPlaying, oldIsPlaying) }
48
+ is VolumeChanged -> listeners.forEach { it.onVolumeChanged(player, newValue, oldValue) }
49
+ is SourceChanged -> listeners.forEach { it.onSourceChanged(player, source, oldSource) }
50
+ is PlaybackRateChanged -> listeners.forEach { it.onPlaybackRateChanged(player, rate, oldRate) }
51
+ is PlayedToEnd -> listeners.forEach { it.onPlayedToEnd(player) }
52
+ }
53
+ }
54
+ }
@@ -2,6 +2,7 @@ package expo.modules.video
2
2
 
3
3
  import androidx.annotation.OptIn
4
4
  import androidx.media3.common.util.UnstableApi
5
+ import expo.modules.kotlin.AppContext
5
6
 
6
7
  // Helper class used to keep track of all existing VideoViews and VideoPlayers
7
8
  @OptIn(UnstableApi::class)
@@ -14,6 +15,12 @@ object VideoManager {
14
15
  // Keeps track of all existing VideoPlayers, and whether they are attached to a VideoView
15
16
  private var videoPlayersToVideoViews = mutableMapOf<VideoPlayer, MutableList<VideoView>>()
16
17
 
18
+ private lateinit var audioFocusManager: AudioFocusManager
19
+
20
+ fun onModuleCreated(appContext: AppContext) {
21
+ audioFocusManager = AudioFocusManager(appContext)
22
+ }
23
+
17
24
  fun registerVideoView(videoView: VideoView) {
18
25
  videoViews[videoView.id] = videoView
19
26
  }
@@ -28,10 +35,12 @@ object VideoManager {
28
35
 
29
36
  fun registerVideoPlayer(videoPlayer: VideoPlayer) {
30
37
  videoPlayersToVideoViews[videoPlayer] = videoPlayersToVideoViews[videoPlayer] ?: mutableListOf()
38
+ audioFocusManager.registerPlayer(videoPlayer)
31
39
  }
32
40
 
33
41
  fun unregisterVideoPlayer(videoPlayer: VideoPlayer) {
34
42
  videoPlayersToVideoViews.remove(videoPlayer)
43
+ audioFocusManager.unregisterPlayer(videoPlayer)
35
44
  }
36
45
 
37
46
  fun onVideoPlayerAttachedToView(videoPlayer: VideoPlayer, videoView: VideoView) {
@@ -43,7 +52,7 @@ object VideoManager {
43
52
  }
44
53
 
45
54
  if (videoPlayersToVideoViews[videoPlayer]?.size == 1) {
46
- videoPlayer.playbackServiceBinder?.service?.registerPlayer(videoPlayer.player)
55
+ videoPlayer.serviceConnection.playbackServiceBinder?.service?.registerPlayer(videoPlayer.player)
47
56
  }
48
57
  }
49
58
 
@@ -52,7 +61,7 @@ object VideoManager {
52
61
 
53
62
  // Unregister disconnected VideoPlayers from the playback service
54
63
  if (videoPlayersToVideoViews[videoPlayer] == null || videoPlayersToVideoViews[videoPlayer]?.size == 0) {
55
- videoPlayer.playbackServiceBinder?.service?.unregisterPlayer(videoPlayer.player)
64
+ videoPlayer.serviceConnection.playbackServiceBinder?.service?.unregisterPlayer(videoPlayer.player)
56
65
  }
57
66
  }
58
67
 
@@ -3,7 +3,7 @@
3
3
  package expo.modules.video
4
4
 
5
5
  import android.app.Activity
6
- import android.content.Context
6
+ import android.net.Uri
7
7
  import androidx.media3.common.PlaybackParameters
8
8
  import androidx.media3.common.Player.REPEAT_MODE_OFF
9
9
  import androidx.media3.common.Player.REPEAT_MODE_ONE
@@ -16,7 +16,10 @@ import expo.modules.kotlin.exception.Exceptions
16
16
  import expo.modules.kotlin.modules.Module
17
17
  import expo.modules.kotlin.modules.ModuleDefinition
18
18
  import expo.modules.kotlin.types.Either
19
+ import expo.modules.video.enums.ContentFit
19
20
  import expo.modules.video.records.VideoSource
21
+ import expo.modules.video.utils.ifYogaDefinedUse
22
+ import expo.modules.video.utils.makeYogaUndefinedIfNegative
20
23
  import kotlinx.coroutines.launch
21
24
  import kotlinx.coroutines.runBlocking
22
25
 
@@ -25,12 +28,14 @@ import kotlinx.coroutines.runBlocking
25
28
  class VideoModule : Module() {
26
29
  private val activity: Activity
27
30
  get() = appContext.activityProvider?.currentActivity ?: throw Exceptions.MissingActivity()
28
- private val reactContext: Context
29
- get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
30
31
 
31
32
  override fun definition() = ModuleDefinition {
32
33
  Name("ExpoVideo")
33
34
 
35
+ OnCreate {
36
+ VideoManager.onModuleCreated(appContext)
37
+ }
38
+
34
39
  Function("isPictureInPictureSupported") {
35
40
  return@Function VideoView.isPictureInPictureSupported(activity)
36
41
  }
@@ -268,12 +273,12 @@ class VideoModule : Module() {
268
273
  }
269
274
  }
270
275
 
271
- Function("replace") { ref: VideoPlayer, source: Either<String, VideoSource>? ->
276
+ Function("replace") { ref: VideoPlayer, source: Either<Uri, VideoSource>? ->
272
277
  val videoSource = source?.let {
273
278
  if (it.`is`(VideoSource::class)) {
274
279
  it.get(VideoSource::class)
275
280
  } else {
276
- VideoSource(it.get(String::class))
281
+ VideoSource(it.get(Uri::class))
277
282
  }
278
283
  }
279
284
 
@@ -1,28 +1,22 @@
1
1
  package expo.modules.video
2
2
 
3
- import android.content.ComponentName
4
3
  import android.content.Context
5
- import android.content.Context.BIND_AUTO_CREATE
6
- import android.content.Intent
7
- import android.content.ServiceConnection
8
- import android.os.Build
9
- import android.os.IBinder
10
- import android.util.Log
11
4
  import android.view.SurfaceView
12
5
  import androidx.media3.common.MediaItem
13
6
  import androidx.media3.common.PlaybackException
14
7
  import androidx.media3.common.PlaybackParameters
15
8
  import androidx.media3.common.Player
16
- import androidx.media3.common.Timeline
17
9
  import androidx.media3.common.util.UnstableApi
18
10
  import androidx.media3.exoplayer.DefaultRenderersFactory
19
11
  import androidx.media3.exoplayer.ExoPlayer
20
- import androidx.media3.session.MediaSessionService
21
12
  import androidx.media3.ui.PlayerView
22
13
  import expo.modules.kotlin.AppContext
23
14
  import expo.modules.kotlin.sharedobjects.SharedObject
15
+ import expo.modules.video.delegates.IgnoreSameSet
24
16
  import expo.modules.video.enums.PlayerStatus
25
17
  import expo.modules.video.enums.PlayerStatus.*
18
+ import expo.modules.video.playbackService.ExpoVideoPlaybackService
19
+ import expo.modules.video.playbackService.PlaybackServiceConnection
26
20
  import expo.modules.video.records.PlaybackError
27
21
  import expo.modules.video.records.VideoSource
28
22
  import expo.modules.video.records.VolumeEvent
@@ -35,29 +29,23 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
35
29
  // This improves the performance of playing DRM-protected content
36
30
  private var renderersFactory = DefaultRenderersFactory(context)
37
31
  .forceEnableMediaCodecAsynchronousQueueing()
38
- val audioFocusManager = VideoPlayerAudioFocusManager(context, WeakReference(this))
32
+ private var listeners: MutableList<WeakReference<VideoPlayerListener>> = mutableListOf()
33
+
39
34
  val player = ExoPlayer
40
35
  .Builder(context, renderersFactory)
41
36
  .setLooper(context.mainLooper)
42
37
  .build()
43
38
 
44
- // We duplicate some properties of the player, because we don't want to always use the mainQueue to access them.
45
- var playing = false
46
- set(value) {
47
- if (field != value) {
48
- sendEventOnJSThread("playingChange", value, field)
49
- }
50
- field = value
51
- }
39
+ val serviceConnection = PlaybackServiceConnection(WeakReference(player))
40
+
41
+ var playing by IgnoreSameSet(false) { new, old ->
42
+ sendEvent(PlayerEvent.IsPlayingChanged(new, old))
43
+ }
52
44
 
53
45
  var uncommittedSource: VideoSource? = source
54
- private var lastLoadedSource: VideoSource? = null
55
- set(value) {
56
- if (field != value && value != null) {
57
- sendEventOnJSThread("sourceChange", value, field)
58
- }
59
- field = value
60
- }
46
+ private var lastLoadedSource by IgnoreSameSet<VideoSource?>(null) { new, old ->
47
+ sendEvent(PlayerEvent.SourceChanged(new, old))
48
+ }
61
49
 
62
50
  // Volume of the player if there was no mute applied.
63
51
  var userVolume = 1f
@@ -66,66 +54,48 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
66
54
  var staysActiveInBackground = false
67
55
  var preservesPitch = false
68
56
  set(preservesPitch) {
69
- playbackParameters = applyPitchCorrection(playbackParameters)
70
57
  field = preservesPitch
58
+ playbackParameters = applyPitchCorrection(playbackParameters)
71
59
  }
72
60
  var showNowPlayingNotification = true
73
61
  set(value) {
74
62
  field = value
75
- playbackServiceBinder?.service?.setShowNotification(value, this.player)
63
+ serviceConnection.playbackServiceBinder?.service?.setShowNotification(value, this.player)
76
64
  }
77
65
  var duration = 0f
78
66
  var isLive = false
79
67
 
80
- private var serviceConnection: ServiceConnection
81
- internal var playbackServiceBinder: PlaybackServiceBinder? = null
82
- lateinit var timeline: Timeline
83
-
84
- var volume = 1f
85
- set(volume) {
86
- if (player.volume == volume) return
87
- player.volume = if (muted) 0f else volume
88
- sendEventOnJSThread("volumeChange", VolumeEvent(volume, muted), VolumeEvent(field, muted))
89
- field = volume
90
- }
68
+ var volume: Float by IgnoreSameSet(1f) { new: Float, old: Float ->
69
+ player.volume = if (muted) 0f else new
70
+ sendEvent(PlayerEvent.VolumeChanged(VolumeEvent(new, muted), VolumeEvent(old, muted)))
71
+ }
91
72
 
92
- var muted = false
93
- set(muted) {
94
- if (field == muted) return
95
- sendEventOnJSThread("volumeChange", VolumeEvent(volume, muted), VolumeEvent(volume, field))
96
- player.volume = if (muted) 0f else userVolume
97
- field = muted
98
- audioFocusManager.onPlayerChangedAudioFocusProperty(this@VideoPlayer)
99
- }
73
+ var muted: Boolean by IgnoreSameSet(false) { new: Boolean, old: Boolean ->
74
+ volume = if (new) 0f else userVolume
75
+ sendEvent(PlayerEvent.VolumeChanged(VolumeEvent(volume, new), VolumeEvent(volume, old)))
76
+ }
100
77
 
101
- var playbackParameters: PlaybackParameters = PlaybackParameters.DEFAULT
102
- set(newPlaybackParameters) {
103
- if (playbackParameters.speed != newPlaybackParameters.speed) {
104
- sendEventOnJSThread("playbackRateChange", newPlaybackParameters.speed, playbackParameters.speed)
105
- }
106
- val pitchCorrectedPlaybackParameters = applyPitchCorrection(newPlaybackParameters)
107
- field = pitchCorrectedPlaybackParameters
78
+ var playbackParameters by IgnoreSameSet(
79
+ PlaybackParameters.DEFAULT,
80
+ propertyMapper = { applyPitchCorrection(it) }
81
+ ) { new: PlaybackParameters, old: PlaybackParameters ->
82
+ player.playbackParameters = new
108
83
 
109
- if (player.playbackParameters != pitchCorrectedPlaybackParameters) {
110
- player.playbackParameters = pitchCorrectedPlaybackParameters
111
- }
84
+ if (old.speed != new.speed) {
85
+ sendEvent(PlayerEvent.PlaybackRateChanged(new.speed, old.speed))
112
86
  }
87
+ }
113
88
 
114
89
  private val playerListener = object : Player.Listener {
115
90
  override fun onIsPlayingChanged(isPlaying: Boolean) {
116
91
  this@VideoPlayer.playing = isPlaying
117
- audioFocusManager.onPlayerChangedAudioFocusProperty(this@VideoPlayer)
118
- }
119
-
120
- override fun onTimelineChanged(timeline: Timeline, reason: Int) {
121
- this@VideoPlayer.timeline = timeline
122
92
  }
123
93
 
124
94
  override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
125
95
  this@VideoPlayer.duration = 0f
126
96
  this@VideoPlayer.isLive = false
127
97
  if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT) {
128
- sendEventOnJSThread("playToEnd")
98
+ sendEvent(PlayerEvent.PlayedToEnd())
129
99
  }
130
100
  super.onMediaItemTransition(mediaItem, reason)
131
101
  }
@@ -144,7 +114,6 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
144
114
 
145
115
  override fun onVolumeChanged(volume: Float) {
146
116
  this@VideoPlayer.volume = volume
147
- audioFocusManager.onPlayerChangedAudioFocusProperty(this@VideoPlayer)
148
117
  }
149
118
 
150
119
  override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) {
@@ -154,9 +123,9 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
154
123
 
155
124
  override fun onPlayerErrorChanged(error: PlaybackException?) {
156
125
  error?.let {
157
- setStatus(ERROR, error)
158
126
  this@VideoPlayer.duration = 0f
159
127
  this@VideoPlayer.isLive = false
128
+ setStatus(ERROR, error)
160
129
  } ?: run {
161
130
  setStatus(playerStateToPlayerStatus(player.playbackState), null)
162
131
  }
@@ -166,53 +135,14 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
166
135
  }
167
136
 
168
137
  init {
169
- serviceConnection = object : ServiceConnection {
170
- override fun onServiceConnected(componentName: ComponentName, binder: IBinder) {
171
- playbackServiceBinder = binder as? PlaybackServiceBinder
172
- playbackServiceBinder?.service?.registerPlayer(player) ?: run {
173
- Log.w(
174
- "ExpoVideo",
175
- "Expo Video could not bind to the playback service. " +
176
- "This will cause issues with playback notifications and sustaining background playback."
177
- )
178
- }
179
- }
180
-
181
- override fun onServiceDisconnected(componentName: ComponentName) {
182
- playbackServiceBinder = null
183
- }
184
-
185
- override fun onNullBinding(componentName: ComponentName) {
186
- Log.w(
187
- "ExpoVideo",
188
- "Expo Video could not bind to the playback service. " +
189
- "This will cause issues with playback notifications and sustaining background playback."
190
- )
191
- }
192
- }
193
-
194
- appContext.reactContext?.apply {
195
- val intent = Intent(context, ExpoVideoPlaybackService::class.java)
196
- intent.action = MediaSessionService.SERVICE_INTERFACE
197
-
198
- startService(intent)
199
-
200
- val flags = if (Build.VERSION.SDK_INT >= 29) {
201
- BIND_AUTO_CREATE or Context.BIND_INCLUDE_CAPABILITIES
202
- } else {
203
- BIND_AUTO_CREATE
204
- }
205
-
206
- bindService(intent, serviceConnection, flags)
207
- }
138
+ ExpoVideoPlaybackService.startService(appContext, context, serviceConnection)
208
139
  player.addListener(playerListener)
209
140
  VideoManager.registerVideoPlayer(this)
210
141
  }
211
142
 
212
143
  override fun close() {
213
- audioFocusManager.onPlayerDestroyed()
214
144
  appContext?.reactContext?.unbindService(serviceConnection)
215
- playbackServiceBinder?.service?.unregisterPlayer(player)
145
+ serviceConnection.playbackServiceBinder?.service?.unregisterPlayer(player)
216
146
  VideoManager.unregisterVideoPlayer(this@VideoPlayer)
217
147
 
218
148
  appContext?.mainQueue?.launch {
@@ -270,19 +200,39 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
270
200
  else -> IDLE
271
201
  }
272
202
  }
203
+
273
204
  private fun setStatus(status: PlayerStatus, error: PlaybackException?) {
205
+ val oldStatus = this.status
206
+ this.status = status
207
+
274
208
  val playbackError = error?.let {
275
209
  PlaybackError(it)
276
210
  }
277
211
 
278
212
  if (playbackError == null && player.playbackState == Player.STATE_ENDED) {
279
- sendEventOnJSThread("playToEnd")
213
+ sendEvent(PlayerEvent.PlayedToEnd())
280
214
  }
281
215
 
282
- if (this.status != status) {
283
- sendEventOnJSThread("statusChange", status.value, this.status.value, playbackError)
216
+ if (this.status != oldStatus) {
217
+ sendEvent(PlayerEvent.StatusChanged(status, oldStatus, playbackError))
284
218
  }
285
- this.status = status
219
+ }
220
+
221
+ fun addListener(videoPlayerListener: VideoPlayerListener) {
222
+ if (listeners.all { it.get() != videoPlayerListener }) {
223
+ listeners.add(WeakReference(videoPlayerListener))
224
+ }
225
+ }
226
+
227
+ fun removeListener(videoPlayerListener: VideoPlayerListener) {
228
+ listeners.removeAll { it.get() == videoPlayerListener }
229
+ }
230
+
231
+ private fun sendEvent(event: PlayerEvent) {
232
+ // Emits to the native listeners
233
+ event.emit(this, listeners.mapNotNull { it.get() })
234
+ // Emits to the JS side
235
+ sendEventOnJSThread(event.name, *event.arguments)
286
236
  }
287
237
 
288
238
  private fun sendEventOnJSThread(eventName: String, vararg args: Any?) {
@@ -0,0 +1,18 @@
1
+ package expo.modules.video
2
+
3
+ import androidx.annotation.OptIn
4
+ import androidx.media3.common.util.UnstableApi
5
+ import expo.modules.video.enums.PlayerStatus
6
+ import expo.modules.video.records.PlaybackError
7
+ import expo.modules.video.records.VideoSource
8
+ import expo.modules.video.records.VolumeEvent
9
+
10
+ @OptIn(UnstableApi::class)
11
+ interface VideoPlayerListener {
12
+ fun onStatusChanged(player: VideoPlayer, status: PlayerStatus, oldStatus: PlayerStatus?, error: PlaybackError?) {}
13
+ fun onIsPlayingChanged(player: VideoPlayer, isPlaying: Boolean, oldIsPlaying: Boolean?) {}
14
+ fun onVolumeChanged(player: VideoPlayer, newValue: VolumeEvent, oldVolume: VolumeEvent?) {}
15
+ fun onSourceChanged(player: VideoPlayer, source: VideoSource?, oldSource: VideoSource?) {}
16
+ fun onPlaybackRateChanged(player: VideoPlayer, rate: Float, oldRate: Float?) {}
17
+ fun onPlayedToEnd(player: VideoPlayer) {}
18
+ }
@@ -25,6 +25,8 @@ import expo.modules.kotlin.exception.Exceptions
25
25
  import expo.modules.kotlin.viewevent.EventDispatcher
26
26
  import expo.modules.kotlin.views.ExpoView
27
27
  import expo.modules.video.drawing.OutlineProvider
28
+ import expo.modules.video.enums.ContentFit
29
+ import expo.modules.video.utils.ifYogaDefinedUse
28
30
  import java.util.UUID
29
31
 
30
32
  // https://developer.android.com/guide/topics/media/media3/getting-started/migration-guide#improvements_in_media3
@@ -290,14 +292,21 @@ class VideoView(context: Context, appContext: AppContext) : ExpoView(context, ap
290
292
 
291
293
  // Draw borders on top of the video
292
294
  if (borderDrawableLazyHolder.isInitialized()) {
293
- val layoutDirection = if (I18nUtil.getInstance().isRTL(context)) {
295
+ val newLayoutDirection = if (I18nUtil.getInstance().isRTL(context)) {
294
296
  LAYOUT_DIRECTION_RTL
295
297
  } else {
296
298
  LAYOUT_DIRECTION_LTR
297
299
  }
298
300
 
299
301
  borderDrawable.apply {
300
- resolvedLayoutDirection = layoutDirection
302
+ val setLayoutDirectionMethod = try {
303
+ // React Native 0.74.0 and below
304
+ ReactViewBackgroundDrawable::class.java.getDeclaredMethod("setResolvedLayoutDirection", Int::class.java)
305
+ } catch (e: NoSuchMethodException) {
306
+ // React Native 0.75.0 and above
307
+ ReactViewBackgroundDrawable::class.java.getMethod("setLayoutDirectionOverride", Int::class.java)
308
+ }
309
+ setLayoutDirectionMethod.invoke(this, newLayoutDirection)
301
310
  setBounds(0, 0, width, height)
302
311
  draw(canvas)
303
312
  }
@@ -0,0 +1,24 @@
1
+ package expo.modules.video.delegates
2
+
3
+ import kotlin.reflect.KProperty
4
+
5
+ /**
6
+ * Property delegate, where the set is ignored unless the value has changed.
7
+ * @param T The type of the property.
8
+ * @param value The initial value of the property.
9
+ * @param propertyMapper A function that maps the new value to the property value.
10
+ * @param didSet A function that is called when the property value has changed.
11
+ */
12
+
13
+ class IgnoreSameSet<T : Any?>(private var value: T, val propertyMapper: ((T) -> T) = { v -> v }, val didSet: ((new: T, old: T) -> Unit)? = null) {
14
+ operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
15
+ return value
16
+ }
17
+
18
+ operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
19
+ if (this.value == propertyMapper(value)) return
20
+ val oldValue = this.value
21
+ this.value = propertyMapper(value)
22
+ didSet?.invoke(this.value, oldValue)
23
+ }
24
+ }
@@ -12,7 +12,7 @@ import com.facebook.react.modules.i18nmanager.I18nUtil
12
12
  import com.facebook.react.uimanager.FloatUtil
13
13
  import com.facebook.react.uimanager.PixelUtil
14
14
  import com.facebook.yoga.YogaConstants
15
- import expo.modules.video.ifYogaUndefinedUse
15
+ import expo.modules.video.utils.ifYogaUndefinedUse
16
16
 
17
17
  class OutlineProvider(private val mContext: Context) : ViewOutlineProvider() {
18
18
  enum class BorderRadiusConfig {
@@ -1,4 +1,4 @@
1
- package expo.modules.video
1
+ package expo.modules.video.enums
2
2
 
3
3
  import androidx.media3.ui.AspectRatioFrameLayout
4
4
  import expo.modules.kotlin.types.Enumerable
@@ -1,4 +1,4 @@
1
- package expo.modules.video
1
+ package expo.modules.video.playbackService
2
2
 
3
3
  import android.app.NotificationChannel
4
4
  import android.app.NotificationManager
@@ -18,6 +18,8 @@ import androidx.media3.session.MediaSessionService
18
18
  import androidx.media3.session.MediaStyleNotificationHelper
19
19
  import androidx.media3.session.SessionCommand
20
20
  import com.google.common.collect.ImmutableList
21
+ import expo.modules.kotlin.AppContext
22
+ import expo.modules.video.R
21
23
 
22
24
  class PlaybackServiceBinder(val service: ExpoVideoPlaybackService) : Binder()
23
25
 
@@ -152,5 +154,22 @@ class ExpoVideoPlaybackService : MediaSessionService() {
152
154
  const val CHANNEL_ID = "PlaybackService"
153
155
  const val SESSION_SHOW_NOTIFICATION = "showNotification"
154
156
  const val SEEK_INTERVAL_MS = 10000L
157
+
158
+ fun startService(appContext: AppContext, context: Context, serviceConnection: PlaybackServiceConnection) {
159
+ appContext.reactContext?.apply {
160
+ val intent = Intent(context, ExpoVideoPlaybackService::class.java)
161
+ intent.action = SERVICE_INTERFACE
162
+
163
+ startService(intent)
164
+
165
+ val flags = if (Build.VERSION.SDK_INT >= 29) {
166
+ BIND_AUTO_CREATE or Context.BIND_INCLUDE_CAPABILITIES
167
+ } else {
168
+ BIND_AUTO_CREATE
169
+ }
170
+
171
+ bindService(intent, serviceConnection, flags)
172
+ }
173
+ }
155
174
  }
156
175
  }
@@ -0,0 +1,36 @@
1
+ package expo.modules.video.playbackService
2
+
3
+ import android.content.ComponentName
4
+ import android.content.ServiceConnection
5
+ import android.os.IBinder
6
+ import android.util.Log
7
+ import androidx.media3.exoplayer.ExoPlayer
8
+ import java.lang.ref.WeakReference
9
+
10
+ class PlaybackServiceConnection(val player: WeakReference<ExoPlayer>) : ServiceConnection {
11
+ var playbackServiceBinder: PlaybackServiceBinder? = null
12
+
13
+ override fun onServiceConnected(componentName: ComponentName, binder: IBinder) {
14
+ val player: ExoPlayer = player.get() ?: return
15
+ playbackServiceBinder = binder as? PlaybackServiceBinder
16
+ playbackServiceBinder?.service?.registerPlayer(player) ?: run {
17
+ Log.w(
18
+ "ExpoVideo",
19
+ "Expo Video could not bind to the playback service. " +
20
+ "This will cause issues with playback notifications and sustaining background playback."
21
+ )
22
+ }
23
+ }
24
+
25
+ override fun onServiceDisconnected(componentName: ComponentName) {
26
+ playbackServiceBinder = null
27
+ }
28
+
29
+ override fun onNullBinding(componentName: ComponentName) {
30
+ Log.w(
31
+ "ExpoVideo",
32
+ "Expo Video could not bind to the playback service. " +
33
+ "This will cause issues with playback notifications and sustaining background playback."
34
+ )
35
+ }
36
+ }
@@ -1,4 +1,4 @@
1
- package expo.modules.video
1
+ package expo.modules.video.playbackService
2
2
 
3
3
  import android.os.Bundle
4
4
  import androidx.media3.common.Player
@@ -1,5 +1,6 @@
1
1
  package expo.modules.video.records
2
2
  import android.content.Context
3
+ import android.net.Uri
3
4
  import androidx.annotation.OptIn
4
5
  import androidx.media3.common.MediaItem
5
6
  import androidx.media3.common.MediaMetadata
@@ -13,7 +14,7 @@ import java.io.Serializable
13
14
 
14
15
  @OptIn(UnstableApi::class)
15
16
  class VideoSource(
16
- @Field var uri: String? = null,
17
+ @Field var uri: Uri? = null,
17
18
  @Field var drm: DRMOptions? = null,
18
19
  @Field var metadata: VideoMetadata? = null,
19
20
  @Field var headers: Map<String, String>? = null
@@ -37,7 +38,7 @@ class VideoSource(
37
38
  fun toMediaItem() = MediaItem
38
39
  .Builder()
39
40
  .apply {
40
- setUri(uri ?: "")
41
+ setUri(uri)
41
42
  setMediaId(toMediaId())
42
43
  drm?.let {
43
44
  if (it.type.isSupported()) {
@@ -4,7 +4,7 @@ import expo.modules.kotlin.records.Field
4
4
  import expo.modules.kotlin.records.Record
5
5
  import java.io.Serializable
6
6
 
7
- internal class VolumeEvent(
7
+ class VolumeEvent(
8
8
  @Field var volume: Float? = null,
9
9
  @Field var isMuted: Boolean? = null
10
10
  ) : Record, Serializable
@@ -5,6 +5,8 @@ import android.content.pm.ApplicationInfo
5
5
  import androidx.annotation.OptIn
6
6
  import androidx.media3.common.util.UnstableApi
7
7
  import androidx.media3.common.util.Util
8
+ import androidx.media3.datasource.DataSource
9
+ import androidx.media3.datasource.DefaultDataSource
8
10
  import androidx.media3.datasource.okhttp.OkHttpDataSource
9
11
  import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
10
12
  import androidx.media3.exoplayer.source.MediaSource
@@ -12,7 +14,16 @@ import expo.modules.video.records.VideoSource
12
14
  import okhttp3.OkHttpClient
13
15
 
14
16
  @OptIn(UnstableApi::class)
15
- fun buildDataSourceFactory(context: Context, videoSource: VideoSource): OkHttpDataSource.Factory {
17
+ fun buildDataSourceFactory(context: Context, videoSource: VideoSource): DataSource.Factory {
18
+ return if (videoSource.uri?.scheme?.startsWith("http") == true) {
19
+ buildOkHttpDataSourceFactory(context, videoSource)
20
+ } else {
21
+ DefaultDataSource.Factory(context)
22
+ }
23
+ }
24
+
25
+ @OptIn(UnstableApi::class)
26
+ fun buildOkHttpDataSourceFactory(context: Context, videoSource: VideoSource): OkHttpDataSource.Factory {
16
27
  val client = OkHttpClient.Builder().build()
17
28
  val userAgent = Util.getUserAgent(context, getApplicationName(context))
18
29
  return OkHttpDataSource.Factory(client).apply {
@@ -25,7 +36,7 @@ fun buildDataSourceFactory(context: Context, videoSource: VideoSource): OkHttpDa
25
36
  }
26
37
  }
27
38
 
28
- fun buildMediaSourceFactory(context: Context, dataSourceFactory: OkHttpDataSource.Factory): MediaSource.Factory {
39
+ fun buildMediaSourceFactory(context: Context, dataSourceFactory: DataSource.Factory): MediaSource.Factory {
29
40
  return DefaultMediaSourceFactory(context).setDataSourceFactory(dataSourceFactory)
30
41
  }
31
42
 
@@ -1,4 +1,4 @@
1
- package expo.modules.video
1
+ package expo.modules.video.utils
2
2
 
3
3
  import com.facebook.yoga.YogaConstants
4
4
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "expo-video",
3
3
  "title": "Expo Video",
4
- "version": "1.2.3",
4
+ "version": "1.2.5",
5
5
  "description": "A cross-platform, performant video component for React Native and Expo with Web support",
6
6
  "main": "build/index.js",
7
7
  "types": "build/index.d.ts",
@@ -36,5 +36,5 @@
36
36
  "peerDependencies": {
37
37
  "expo": "*"
38
38
  },
39
- "gitHead": "48f1ec9d91c5c38c7684cb98a314e443cbeb2185"
39
+ "gitHead": "80d038d11d216793d00fa5b4607a2c6169cee814"
40
40
  }
@@ -1,105 +0,0 @@
1
- package expo.modules.video
2
-
3
- import android.content.Context
4
- import android.media.AudioAttributes
5
- import android.media.AudioFocusRequest
6
- import android.media.AudioManager
7
- import android.os.Build
8
- import androidx.media3.common.util.UnstableApi
9
- import java.lang.ref.WeakReference
10
-
11
- @UnstableApi
12
- class VideoPlayerAudioFocusManager(val context: Context, private val player: WeakReference<VideoPlayer>) : AudioManager.OnAudioFocusChangeListener {
13
- private val audioManager by lazy {
14
- context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager ?: run {
15
- throw FailedToGetAudioFocusManagerException()
16
- }
17
- }
18
- private var currentFocusRequest: AudioFocusRequest? = null
19
- private var volumeBeforeDuck: Float = -1f
20
-
21
- // TODO: @behenate (SDK52) Instead of calling this explicitly in the player we should add functionality
22
- // To register as a player delegate and react to player events.
23
- fun onPlayerChangedAudioFocusProperty(player: VideoPlayer) {
24
- if (player.playing && !player.muted && player.volume > 0) {
25
- requestAudioFocus()
26
- } else {
27
- abandonAudioFocus()
28
- }
29
- }
30
-
31
- fun onPlayerDestroyed() {
32
- abandonAudioFocus()
33
- }
34
-
35
- private fun requestAudioFocus() {
36
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
37
- if (currentFocusRequest != null) {
38
- abandonAudioFocus()
39
- }
40
-
41
- val newFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run {
42
- setAudioAttributes(
43
- AudioAttributes.Builder().run {
44
- setUsage(AudioAttributes.USAGE_MEDIA)
45
- setContentType(AudioAttributes.CONTENT_TYPE_MOVIE)
46
- setOnAudioFocusChangeListener(this@VideoPlayerAudioFocusManager)
47
- build()
48
- }
49
- ).build()
50
- }
51
- this.currentFocusRequest = newFocusRequest
52
- audioManager.requestAudioFocus(newFocusRequest)
53
- } else {
54
- @Suppress("DEPRECATION")
55
- audioManager.requestAudioFocus(
56
- this,
57
- AudioManager.STREAM_MUSIC,
58
- AudioManager.AUDIOFOCUS_GAIN
59
- )
60
- }
61
- }
62
-
63
- private fun abandonAudioFocus() {
64
- currentFocusRequest?.let {
65
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
66
- audioManager.abandonAudioFocusRequest(it)
67
- } else {
68
- @Suppress("DEPRECATION")
69
- audioManager.abandonAudioFocus(this)
70
- }
71
- }
72
- currentFocusRequest = null
73
- }
74
-
75
- override fun onAudioFocusChange(focusChange: Int) {
76
- when (focusChange) {
77
- AudioManager.AUDIOFOCUS_LOSS -> {
78
- player.get()?.player?.pause()
79
- }
80
-
81
- AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
82
- player.get()?.player?.pause()
83
- }
84
-
85
- AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
86
- player.get()?.let { player ->
87
- volumeBeforeDuck = player.player.volume
88
- player.volume = player.volume / 2f
89
- player.userVolume = player.volume
90
- }
91
- }
92
-
93
- AudioManager.AUDIOFOCUS_GAIN -> {
94
- // TODO: For now this behaves like iOS and doesn't resume playback automatically
95
- // In future versions we can add a prop to control this behavior.
96
- player.get()?.let { player ->
97
- if (player.playing && !player.muted && player.volume > 0) {
98
- player.volume = volumeBeforeDuck
99
- player.userVolume = player.volume
100
- }
101
- }
102
- }
103
- }
104
- }
105
- }