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.
- package/CHANGELOG.md +18 -0
- package/android/build.gradle +2 -2
- package/android/src/main/AndroidManifest.xml +1 -1
- package/android/src/main/java/expo/modules/video/AudioFocusManager.kt +187 -0
- package/android/src/main/java/expo/modules/video/PlayerEvent.kt +54 -0
- package/android/src/main/java/expo/modules/video/VideoManager.kt +11 -2
- package/android/src/main/java/expo/modules/video/VideoModule.kt +10 -5
- package/android/src/main/java/expo/modules/video/VideoPlayer.kt +59 -109
- package/android/src/main/java/expo/modules/video/VideoPlayerListener.kt +18 -0
- package/android/src/main/java/expo/modules/video/VideoView.kt +11 -2
- package/android/src/main/java/expo/modules/video/delegates/IgnoreSameSet.kt +24 -0
- package/android/src/main/java/expo/modules/video/drawing/OutlineProvider.kt +1 -1
- package/android/src/main/java/expo/modules/video/{ContentFit.kt → enums/ContentFit.kt} +1 -1
- package/android/src/main/java/expo/modules/video/{ExpoVideoPlaybackService.kt → playbackService/ExpoVideoPlaybackService.kt} +20 -1
- package/android/src/main/java/expo/modules/video/playbackService/PlaybackServiceConnection.kt +36 -0
- package/android/src/main/java/expo/modules/video/{VideoMediaSessionCallback.kt → playbackService/VideoMediaSessionCallback.kt} +1 -1
- package/android/src/main/java/expo/modules/video/records/VideoSource.kt +3 -2
- package/android/src/main/java/expo/modules/video/records/VolumeEvent.kt +1 -1
- package/android/src/main/java/expo/modules/video/{DataSourceUtils.kt → utils/DataSourceUtils.kt} +13 -2
- package/android/src/main/java/expo/modules/video/{YogaUtils.kt → utils/YogaUtils.kt} +1 -1
- package/package.json +2 -2
- 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
|
|
package/android/build.gradle
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
apply plugin: 'com.android.library'
|
|
2
2
|
|
|
3
3
|
group = 'host.exp.exponent'
|
|
4
|
-
version = '1.2.
|
|
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.
|
|
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.
|
|
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<
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
213
|
+
sendEvent(PlayerEvent.PlayedToEnd())
|
|
280
214
|
}
|
|
281
215
|
|
|
282
|
-
if (this.status !=
|
|
283
|
-
|
|
216
|
+
if (this.status != oldStatus) {
|
|
217
|
+
sendEvent(PlayerEvent.StatusChanged(status, oldStatus, playbackError))
|
|
284
218
|
}
|
|
285
|
-
|
|
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
|
|
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
|
-
|
|
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.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,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:
|
|
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
|
-
|
|
7
|
+
class VolumeEvent(
|
|
8
8
|
@Field var volume: Float? = null,
|
|
9
9
|
@Field var isMuted: Boolean? = null
|
|
10
10
|
) : Record, Serializable
|
package/android/src/main/java/expo/modules/video/{DataSourceUtils.kt → utils/DataSourceUtils.kt}
RENAMED
|
@@ -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):
|
|
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:
|
|
39
|
+
fun buildMediaSourceFactory(context: Context, dataSourceFactory: DataSource.Factory): MediaSource.Factory {
|
|
29
40
|
return DefaultMediaSourceFactory(context).setDataSourceFactory(dataSourceFactory)
|
|
30
41
|
}
|
|
31
42
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-video",
|
|
3
3
|
"title": "Expo Video",
|
|
4
|
-
"version": "1.2.
|
|
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": "
|
|
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
|
-
}
|