expo-video 1.2.2 → 1.2.4

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 (51) hide show
  1. package/CHANGELOG.md +28 -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/VideoExceptions.kt +3 -0
  7. package/android/src/main/java/expo/modules/video/VideoManager.kt +11 -2
  8. package/android/src/main/java/expo/modules/video/VideoModule.kt +13 -6
  9. package/android/src/main/java/expo/modules/video/VideoPlayer.kt +59 -109
  10. package/android/src/main/java/expo/modules/video/VideoPlayerListener.kt +18 -0
  11. package/android/src/main/java/expo/modules/video/VideoView.kt +27 -5
  12. package/android/src/main/java/expo/modules/video/delegates/IgnoreSameSet.kt +24 -0
  13. package/android/src/main/java/expo/modules/video/drawing/OutlineProvider.kt +1 -1
  14. package/android/src/main/java/expo/modules/video/{ContentFit.kt → enums/ContentFit.kt} +1 -1
  15. package/android/src/main/java/expo/modules/video/{ExpoVideoPlaybackService.kt → playbackService/ExpoVideoPlaybackService.kt} +20 -1
  16. package/android/src/main/java/expo/modules/video/playbackService/PlaybackServiceConnection.kt +36 -0
  17. package/android/src/main/java/expo/modules/video/{VideoMediaSessionCallback.kt → playbackService/VideoMediaSessionCallback.kt} +1 -1
  18. package/android/src/main/java/expo/modules/video/records/VideoSource.kt +3 -2
  19. package/android/src/main/java/expo/modules/video/records/VolumeEvent.kt +1 -1
  20. package/android/src/main/java/expo/modules/video/{DataSourceUtils.kt → utils/DataSourceUtils.kt} +13 -2
  21. package/android/src/main/java/expo/modules/video/{YogaUtils.kt → utils/YogaUtils.kt} +1 -1
  22. package/build/VideoPlayer.types.d.ts +1 -1
  23. package/build/VideoPlayer.types.d.ts.map +1 -1
  24. package/build/VideoPlayer.types.js.map +1 -1
  25. package/build/VideoPlayer.web.d.ts +9 -1
  26. package/build/VideoPlayer.web.d.ts.map +1 -1
  27. package/build/VideoPlayer.web.js +61 -13
  28. package/build/VideoPlayer.web.js.map +1 -1
  29. package/build/VideoView.d.ts +3 -0
  30. package/build/VideoView.d.ts.map +1 -1
  31. package/build/VideoView.js +3 -0
  32. package/build/VideoView.js.map +1 -1
  33. package/build/VideoView.types.d.ts +13 -0
  34. package/build/VideoView.types.d.ts.map +1 -1
  35. package/build/VideoView.types.js.map +1 -1
  36. package/build/VideoView.web.d.ts.map +1 -1
  37. package/build/VideoView.web.js +42 -13
  38. package/build/VideoView.web.js.map +1 -1
  39. package/ios/NowPlayingManager.swift +6 -10
  40. package/ios/VideoModule.swift +19 -0
  41. package/ios/VideoPlayer.swift +7 -1
  42. package/package.json +2 -2
  43. package/plugin/build/withExpoVideo.d.ts +5 -1
  44. package/plugin/build/withExpoVideo.js +21 -3
  45. package/plugin/src/withExpoVideo.ts +35 -3
  46. package/src/VideoPlayer.types.ts +1 -1
  47. package/src/VideoPlayer.web.tsx +72 -13
  48. package/src/VideoView.tsx +3 -0
  49. package/src/VideoView.types.ts +14 -0
  50. package/src/VideoView.web.tsx +49 -14
  51. package/android/src/main/java/expo/modules/video/VideoPlayerAudioFocusManager.kt +0 -105
package/CHANGELOG.md CHANGED
@@ -10,6 +10,34 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 1.2.4 — 2024-07-30
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - [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))
18
+ - [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))
19
+
20
+ ### 💡 Others
21
+
22
+ - [Android] Refactor `VideoPlayer.kt`, organize files ([#30452](https://github.com/expo/expo/pull/30452) by [@behenate](https://github.com/behenate))
23
+
24
+ ## 1.2.3 — 2024-07-11
25
+
26
+ ### 🛠 Breaking changes
27
+
28
+ - [Android][iOS] Now Picture in Picture has to be enabled via the config plugin to work. ([#30068](https://github.com/expo/expo/pull/30068) by [@behenate](https://github.com/behenate))
29
+
30
+ ### 🎉 New features
31
+
32
+ - [Web] Add support for events. ([#29742](https://github.com/expo/expo/pull/29742) by [@behenate](https://github.com/behenate))
33
+ - [iOS] Add ability to disable live text interaction. ([#30093](https://github.com/expo/expo/pull/30093) by [@fobos531](https://github.com/fobos531))
34
+
35
+ ### 🐛 Bug fixes
36
+
37
+ - [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))
38
+ - [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))
39
+ - 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))
40
+
13
41
  ## 1.2.2 — 2024-07-03
14
42
 
15
43
  ### 🐛 Bug fixes
@@ -1,7 +1,7 @@
1
1
  apply plugin: 'com.android.library'
2
2
 
3
3
  group = 'host.exp.exponent'
4
- version = '1.2.2'
4
+ version = '1.2.4'
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.2'
17
+ versionName '1.2.4'
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
+ }
@@ -15,6 +15,9 @@ internal class MethodUnsupportedException(methodName: String) :
15
15
  internal class PictureInPictureEnterException(message: String?) :
16
16
  CodedException("Failed to enter Picture in Picture mode${message?.let { ". $message" } ?: ""}")
17
17
 
18
+ internal class PictureInPictureConfigurationException :
19
+ CodedException("Current activity does not support picture-in-picture. Make sure you have configured the `expo-video` config plugin correctly.")
20
+
18
21
  internal class PictureInPictureUnsupportedException :
19
22
  CodedException("Picture in Picture mode is not supported on this device")
20
23
 
@@ -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
  }
@@ -128,7 +133,9 @@ class VideoModule : Module() {
128
133
  }
129
134
 
130
135
  AsyncFunction("startPictureInPicture") { view: VideoView ->
131
- view.enterPictureInPicture()
136
+ view.runWithPiPMisconfigurationSoftHandling(true) {
137
+ view.enterPictureInPicture()
138
+ }
132
139
  }
133
140
 
134
141
  AsyncFunction("stopPictureInPicture") {
@@ -266,12 +273,12 @@ class VideoModule : Module() {
266
273
  }
267
274
  }
268
275
 
269
- Function("replace") { ref: VideoPlayer, source: Either<String, VideoSource>? ->
276
+ Function("replace") { ref: VideoPlayer, source: Either<Uri, VideoSource>? ->
270
277
  val videoSource = source?.let {
271
278
  if (it.`is`(VideoSource::class)) {
272
279
  it.get(VideoSource::class)
273
280
  } else {
274
- VideoSource(it.get(String::class))
281
+ VideoSource(it.get(Uri::class))
275
282
  }
276
283
  }
277
284