expo-video 1.2.4 → 1.2.6
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 +29 -0
- package/android/build.gradle +4 -3
- package/android/src/main/java/expo/modules/video/PlayerEvent.kt +1 -1
- package/android/src/main/java/expo/modules/video/VideoManager.kt +1 -1
- package/android/src/main/java/expo/modules/video/VideoModule.kt +39 -6
- package/android/src/main/java/expo/modules/video/VideoPlayer.kt +7 -4
- package/android/src/main/java/expo/modules/video/VideoView.kt +13 -2
- package/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt +5 -2
- package/android/src/main/java/expo/modules/video/playbackService/PlaybackServiceConnection.kt +3 -3
- package/android/src/main/java/expo/modules/video/records/VideoSource.kt +35 -2
- package/android/src/main/java/expo/modules/video/utils/DataSourceUtils.kt +1 -1
- package/build/VideoPlayer.d.ts.map +1 -1
- package/build/VideoPlayer.js +19 -1
- package/build/VideoPlayer.js.map +1 -1
- package/build/VideoPlayer.types.d.ts +35 -2
- package/build/VideoPlayer.types.d.ts.map +1 -1
- package/build/VideoPlayer.types.js.map +1 -1
- package/build/VideoPlayer.web.d.ts +3 -0
- package/build/VideoPlayer.web.d.ts.map +1 -1
- package/build/VideoPlayer.web.js +11 -1
- package/build/VideoPlayer.web.js.map +1 -1
- package/build/VideoView.types.d.ts +8 -0
- package/build/VideoView.types.d.ts.map +1 -1
- package/build/VideoView.types.js.map +1 -1
- package/build/VideoView.web.d.ts.map +1 -1
- package/build/VideoView.web.js +21 -0
- package/build/VideoView.web.js.map +1 -1
- package/ios/NowPlayingManager.swift +1 -0
- package/ios/VideoModule.swift +37 -5
- package/ios/VideoView.swift +6 -0
- package/package.json +2 -2
- package/src/VideoPlayer.tsx +21 -1
- package/src/VideoPlayer.types.ts +42 -1
- package/src/VideoPlayer.web.tsx +12 -1
- package/src/VideoView.types.ts +10 -0
- package/src/VideoView.web.tsx +23 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,35 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 1.2.6 — 2024-09-13
|
|
14
|
+
|
|
15
|
+
### 🛠 Breaking changes
|
|
16
|
+
|
|
17
|
+
- `showNowPlayingNotification` property of the player now defaults to `false`. ([#31261](https://github.com/expo/expo/pull/31261) by [@behenate](https://github.com/behenate))
|
|
18
|
+
|
|
19
|
+
### 🎉 New features
|
|
20
|
+
|
|
21
|
+
- [Android][iOS] Add properties for more advanced live stream configuration. ([#30648](https://github.com/expo/expo/pull/30648) by [@justjoostnl](https://github.com/justjoostnl))
|
|
22
|
+
- [iOS] Add live indicator in the now playing info. ([#30629](https://github.com/expo/expo/pull/30629) by [@justjoostnl](https://github.com/justjoostnl))
|
|
23
|
+
- Add fullscreen enter and exit events. ([#30922](https://github.com/expo/expo/pull/30922) by [@fobos531](https://github.com/fobos531))
|
|
24
|
+
- Add support for playback of local assets imported with the `require` function. ([#30837](https://github.com/expo/expo/pull/30837) by [@behenate](https://github.com/behenate))
|
|
25
|
+
|
|
26
|
+
### 🐛 Bug fixes
|
|
27
|
+
|
|
28
|
+
- [iOS] Fixed `player.currentTime` being `NaN` when source is not provided and `player.duration` being `NaN` inside the hook callback when the source is updated. ([#31011](https://github.com/expo/expo/pull/31011) by [@AlirezaHadjar](https://github.com/AlirezaHadjar))
|
|
29
|
+
- [Android] Fix wrong event being sent when the volume is changed. ([#30891](https://github.com/expo/expo/pull/30891) by [@behenate](https://github.com/behenate))
|
|
30
|
+
|
|
31
|
+
### 💡 Others
|
|
32
|
+
|
|
33
|
+
- Bump media3 version to 1.4.0. ([#31239](https://github.com/expo/expo/pull/31239) by [@behenate](https://github.com/behenate))
|
|
34
|
+
- [Android] The media will now be buffered even when the player is unmounted to match iOS behavior. ([#30626](https://github.com/expo/expo/pull/30626) by [@behenate](https://github.com/behenate))
|
|
35
|
+
|
|
36
|
+
## 1.2.5 — 2024-08-20
|
|
37
|
+
|
|
38
|
+
### 🐛 Bug fixes
|
|
39
|
+
|
|
40
|
+
- [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))
|
|
41
|
+
|
|
13
42
|
## 1.2.4 — 2024-07-30
|
|
14
43
|
|
|
15
44
|
### 🐛 Bug fixes
|
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.6'
|
|
5
5
|
|
|
6
6
|
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
7
7
|
apply from: expoModulesCorePlugin
|
|
@@ -14,14 +14,15 @@ android {
|
|
|
14
14
|
namespace "expo.modules.video"
|
|
15
15
|
defaultConfig {
|
|
16
16
|
versionCode 1
|
|
17
|
-
versionName '1.2.
|
|
17
|
+
versionName '1.2.6'
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
dependencies {
|
|
22
22
|
implementation 'com.facebook.react:react-android'
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
// Remember to keep this in sync with the version in `expo-audio`
|
|
25
|
+
def androidxMedia3Version = "1.4.0"
|
|
25
26
|
implementation "androidx.media3:media3-session:${androidxMedia3Version}"
|
|
26
27
|
implementation "androidx.media3:media3-exoplayer:${androidxMedia3Version}"
|
|
27
28
|
implementation "androidx.media3:media3-exoplayer-dash:${androidxMedia3Version}"
|
|
@@ -23,7 +23,7 @@ sealed class PlayerEvent {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
data class VolumeChanged(val newValue: VolumeEvent, val oldValue: VolumeEvent?) : PlayerEvent() {
|
|
26
|
-
override val name = "
|
|
26
|
+
override val name = "volumeChange"
|
|
27
27
|
override val arguments = arrayOf(newValue, oldValue)
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -52,7 +52,7 @@ object VideoManager {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
if (videoPlayersToVideoViews[videoPlayer]?.size == 1) {
|
|
55
|
-
videoPlayer.serviceConnection.playbackServiceBinder?.service?.registerPlayer(videoPlayer
|
|
55
|
+
videoPlayer.serviceConnection.playbackServiceBinder?.service?.registerPlayer(videoPlayer)
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -4,9 +4,11 @@ package expo.modules.video
|
|
|
4
4
|
|
|
5
5
|
import android.app.Activity
|
|
6
6
|
import android.net.Uri
|
|
7
|
+
import androidx.media3.common.C
|
|
7
8
|
import androidx.media3.common.PlaybackParameters
|
|
8
9
|
import androidx.media3.common.Player.REPEAT_MODE_OFF
|
|
9
10
|
import androidx.media3.common.Player.REPEAT_MODE_ONE
|
|
11
|
+
import androidx.media3.common.Timeline
|
|
10
12
|
import com.facebook.react.uimanager.PixelUtil
|
|
11
13
|
import com.facebook.react.uimanager.Spacing
|
|
12
14
|
import com.facebook.react.uimanager.ViewProps
|
|
@@ -43,12 +45,13 @@ class VideoModule : Module() {
|
|
|
43
45
|
View(VideoView::class) {
|
|
44
46
|
Events(
|
|
45
47
|
"onPictureInPictureStart",
|
|
46
|
-
"onPictureInPictureStop"
|
|
48
|
+
"onPictureInPictureStop",
|
|
49
|
+
"onFullscreenEnter",
|
|
50
|
+
"onFullscreenExit"
|
|
47
51
|
)
|
|
48
52
|
|
|
49
53
|
Prop("player") { view: VideoView, player: VideoPlayer ->
|
|
50
54
|
view.videoPlayer = player
|
|
51
|
-
player.prepare()
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
Prop("nativeControls") { view: VideoView, useNativeControls: Boolean ->
|
|
@@ -149,7 +152,11 @@ class VideoModule : Module() {
|
|
|
149
152
|
|
|
150
153
|
Class(VideoPlayer::class) {
|
|
151
154
|
Constructor { source: VideoSource? ->
|
|
152
|
-
VideoPlayer(activity.applicationContext, appContext, source)
|
|
155
|
+
val player = VideoPlayer(activity.applicationContext, appContext, source)
|
|
156
|
+
appContext.mainQueue.launch {
|
|
157
|
+
player.prepare()
|
|
158
|
+
}
|
|
159
|
+
return@Constructor player
|
|
153
160
|
}
|
|
154
161
|
|
|
155
162
|
Property("playing")
|
|
@@ -193,6 +200,34 @@ class VideoModule : Module() {
|
|
|
193
200
|
}
|
|
194
201
|
}
|
|
195
202
|
|
|
203
|
+
Property("currentLiveTimestamp")
|
|
204
|
+
.get { ref: VideoPlayer ->
|
|
205
|
+
// TODO: same as `currentTime`
|
|
206
|
+
runBlocking(appContext.mainQueue.coroutineContext) {
|
|
207
|
+
val window = Timeline.Window()
|
|
208
|
+
if (!ref.player.currentTimeline.isEmpty) {
|
|
209
|
+
ref.player.currentTimeline.getWindow(ref.player.currentMediaItemIndex, window)
|
|
210
|
+
}
|
|
211
|
+
if (window.windowStartTimeMs == C.TIME_UNSET) {
|
|
212
|
+
null
|
|
213
|
+
} else {
|
|
214
|
+
window.windowStartTimeMs + ref.player.currentPosition
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
Property("currentOffsetFromLive")
|
|
220
|
+
.get { ref: VideoPlayer ->
|
|
221
|
+
// TODO: same as `currentTime`
|
|
222
|
+
runBlocking(appContext.mainQueue.coroutineContext) {
|
|
223
|
+
if (ref.player.currentLiveOffset == C.TIME_UNSET) {
|
|
224
|
+
null
|
|
225
|
+
} else {
|
|
226
|
+
ref.player.currentLiveOffset / 1000f
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
196
231
|
Property("duration")
|
|
197
232
|
.get { ref: VideoPlayer ->
|
|
198
233
|
ref.duration
|
|
@@ -284,9 +319,7 @@ class VideoModule : Module() {
|
|
|
284
319
|
|
|
285
320
|
appContext.mainQueue.launch {
|
|
286
321
|
ref.uncommittedSource = videoSource
|
|
287
|
-
|
|
288
|
-
ref.prepare()
|
|
289
|
-
}
|
|
322
|
+
ref.prepare()
|
|
290
323
|
}
|
|
291
324
|
}
|
|
292
325
|
|
|
@@ -36,7 +36,7 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
|
|
|
36
36
|
.setLooper(context.mainLooper)
|
|
37
37
|
.build()
|
|
38
38
|
|
|
39
|
-
val serviceConnection = PlaybackServiceConnection(WeakReference(
|
|
39
|
+
val serviceConnection = PlaybackServiceConnection(WeakReference(this))
|
|
40
40
|
|
|
41
41
|
var playing by IgnoreSameSet(false) { new, old ->
|
|
42
42
|
sendEvent(PlayerEvent.IsPlayingChanged(new, old))
|
|
@@ -57,7 +57,7 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
|
|
|
57
57
|
field = preservesPitch
|
|
58
58
|
playbackParameters = applyPitchCorrection(playbackParameters)
|
|
59
59
|
}
|
|
60
|
-
var showNowPlayingNotification =
|
|
60
|
+
var showNowPlayingNotification = false
|
|
61
61
|
set(value) {
|
|
62
62
|
field = value
|
|
63
63
|
serviceConnection.playbackServiceBinder?.service?.setShowNotification(value, this.player)
|
|
@@ -67,11 +67,12 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
|
|
|
67
67
|
|
|
68
68
|
var volume: Float by IgnoreSameSet(1f) { new: Float, old: Float ->
|
|
69
69
|
player.volume = if (muted) 0f else new
|
|
70
|
+
userVolume = volume
|
|
70
71
|
sendEvent(PlayerEvent.VolumeChanged(VolumeEvent(new, muted), VolumeEvent(old, muted)))
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
var muted: Boolean by IgnoreSameSet(false) { new: Boolean, old: Boolean ->
|
|
74
|
-
volume = if (new) 0f else userVolume
|
|
75
|
+
player.volume = if (new) 0f else userVolume
|
|
75
76
|
sendEvent(PlayerEvent.VolumeChanged(VolumeEvent(volume, new), VolumeEvent(volume, old)))
|
|
76
77
|
}
|
|
77
78
|
|
|
@@ -113,7 +114,9 @@ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSou
|
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
override fun onVolumeChanged(volume: Float) {
|
|
116
|
-
|
|
117
|
+
if (!muted) {
|
|
118
|
+
this@VideoPlayer.volume = volume
|
|
119
|
+
}
|
|
117
120
|
}
|
|
118
121
|
|
|
119
122
|
override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) {
|
|
@@ -36,6 +36,8 @@ class VideoView(context: Context, appContext: AppContext) : ExpoView(context, ap
|
|
|
36
36
|
val playerView: PlayerView = PlayerView(context.applicationContext)
|
|
37
37
|
val onPictureInPictureStart by EventDispatcher<Unit>()
|
|
38
38
|
val onPictureInPictureStop by EventDispatcher<Unit>()
|
|
39
|
+
val onFullscreenEnter by EventDispatcher<Unit>()
|
|
40
|
+
val onFullscreenExit by EventDispatcher<Unit>()
|
|
39
41
|
|
|
40
42
|
var willEnterPiP: Boolean = false
|
|
41
43
|
var isInFullscreen: Boolean = false
|
|
@@ -154,6 +156,7 @@ class VideoView(context: Context, appContext: AppContext) : ExpoView(context, ap
|
|
|
154
156
|
@Suppress("DEPRECATION")
|
|
155
157
|
currentActivity.overridePendingTransition(0, 0)
|
|
156
158
|
}
|
|
159
|
+
onFullscreenEnter(Unit)
|
|
157
160
|
isInFullscreen = true
|
|
158
161
|
}
|
|
159
162
|
|
|
@@ -162,6 +165,7 @@ class VideoView(context: Context, appContext: AppContext) : ExpoView(context, ap
|
|
|
162
165
|
val fullScreenButton: ImageButton = playerView.findViewById(androidx.media3.ui.R.id.exo_fullscreen)
|
|
163
166
|
fullScreenButton.setImageResource(androidx.media3.ui.R.drawable.exo_icon_fullscreen_enter)
|
|
164
167
|
videoPlayer?.changePlayerView(playerView)
|
|
168
|
+
onFullscreenExit(Unit)
|
|
165
169
|
isInFullscreen = false
|
|
166
170
|
}
|
|
167
171
|
|
|
@@ -292,14 +296,21 @@ class VideoView(context: Context, appContext: AppContext) : ExpoView(context, ap
|
|
|
292
296
|
|
|
293
297
|
// Draw borders on top of the video
|
|
294
298
|
if (borderDrawableLazyHolder.isInitialized()) {
|
|
295
|
-
val
|
|
299
|
+
val newLayoutDirection = if (I18nUtil.getInstance().isRTL(context)) {
|
|
296
300
|
LAYOUT_DIRECTION_RTL
|
|
297
301
|
} else {
|
|
298
302
|
LAYOUT_DIRECTION_LTR
|
|
299
303
|
}
|
|
300
304
|
|
|
301
305
|
borderDrawable.apply {
|
|
302
|
-
|
|
306
|
+
val setLayoutDirectionMethod = try {
|
|
307
|
+
// React Native 0.74.0 and below
|
|
308
|
+
ReactViewBackgroundDrawable::class.java.getDeclaredMethod("setResolvedLayoutDirection", Int::class.java)
|
|
309
|
+
} catch (e: NoSuchMethodException) {
|
|
310
|
+
// React Native 0.75.0 and above
|
|
311
|
+
ReactViewBackgroundDrawable::class.java.getMethod("setLayoutDirectionOverride", Int::class.java)
|
|
312
|
+
}
|
|
313
|
+
setLayoutDirectionMethod.invoke(this, newLayoutDirection)
|
|
303
314
|
setBounds(0, 0, width, height)
|
|
304
315
|
draw(canvas)
|
|
305
316
|
}
|
package/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt
CHANGED
|
@@ -20,6 +20,7 @@ import androidx.media3.session.SessionCommand
|
|
|
20
20
|
import com.google.common.collect.ImmutableList
|
|
21
21
|
import expo.modules.kotlin.AppContext
|
|
22
22
|
import expo.modules.video.R
|
|
23
|
+
import expo.modules.video.VideoPlayer
|
|
23
24
|
|
|
24
25
|
class PlaybackServiceBinder(val service: ExpoVideoPlaybackService) : Binder()
|
|
25
26
|
|
|
@@ -51,7 +52,8 @@ class ExpoVideoPlaybackService : MediaSessionService() {
|
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
fun registerPlayer(
|
|
55
|
+
fun registerPlayer(videoPlayer: VideoPlayer) {
|
|
56
|
+
val player = videoPlayer.player
|
|
55
57
|
if (mediaSessions[player] != null) {
|
|
56
58
|
return
|
|
57
59
|
}
|
|
@@ -64,6 +66,7 @@ class ExpoVideoPlaybackService : MediaSessionService() {
|
|
|
64
66
|
|
|
65
67
|
mediaSessions[player] = mediaSession
|
|
66
68
|
addSession(mediaSession)
|
|
69
|
+
setShowNotification(videoPlayer.showNowPlayingNotification, player)
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
fun unregisterPlayer(player: ExoPlayer) {
|
|
@@ -82,7 +85,7 @@ class ExpoVideoPlaybackService : MediaSessionService() {
|
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
override fun onUpdateNotification(session: MediaSession, startInForegroundRequired: Boolean) {
|
|
85
|
-
if (session.sessionExtras.getBoolean(SESSION_SHOW_NOTIFICATION,
|
|
88
|
+
if (session.sessionExtras.getBoolean(SESSION_SHOW_NOTIFICATION, false)) {
|
|
86
89
|
createNotification(session)
|
|
87
90
|
} else {
|
|
88
91
|
(session.player as? ExoPlayer)?.let {
|
package/android/src/main/java/expo/modules/video/playbackService/PlaybackServiceConnection.kt
CHANGED
|
@@ -4,14 +4,14 @@ import android.content.ComponentName
|
|
|
4
4
|
import android.content.ServiceConnection
|
|
5
5
|
import android.os.IBinder
|
|
6
6
|
import android.util.Log
|
|
7
|
-
import
|
|
7
|
+
import expo.modules.video.VideoPlayer
|
|
8
8
|
import java.lang.ref.WeakReference
|
|
9
9
|
|
|
10
|
-
class PlaybackServiceConnection(val player: WeakReference<
|
|
10
|
+
class PlaybackServiceConnection(val player: WeakReference<VideoPlayer>) : ServiceConnection {
|
|
11
11
|
var playbackServiceBinder: PlaybackServiceBinder? = null
|
|
12
12
|
|
|
13
13
|
override fun onServiceConnected(componentName: ComponentName, binder: IBinder) {
|
|
14
|
-
val player
|
|
14
|
+
val player = player.get() ?: return
|
|
15
15
|
playbackServiceBinder = binder as? PlaybackServiceBinder
|
|
16
16
|
playbackServiceBinder?.service?.registerPlayer(player) ?: run {
|
|
17
17
|
Log.w(
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
package expo.modules.video.records
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.content.ContentResolver
|
|
2
5
|
import android.content.Context
|
|
3
6
|
import android.net.Uri
|
|
7
|
+
import android.util.Log
|
|
4
8
|
import androidx.annotation.OptIn
|
|
5
9
|
import androidx.media3.common.MediaItem
|
|
6
10
|
import androidx.media3.common.MediaMetadata
|
|
7
11
|
import androidx.media3.common.util.UnstableApi
|
|
12
|
+
import androidx.media3.datasource.DataSpec
|
|
13
|
+
import androidx.media3.datasource.RawResourceDataSource
|
|
8
14
|
import androidx.media3.exoplayer.source.MediaSource
|
|
9
15
|
import expo.modules.kotlin.records.Field
|
|
10
16
|
import expo.modules.kotlin.records.Record
|
|
@@ -35,10 +41,10 @@ class VideoSource(
|
|
|
35
41
|
return buildMediaSourceWithHeaders(context, this)
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
fun toMediaItem() = MediaItem
|
|
44
|
+
fun toMediaItem(context: Context) = MediaItem
|
|
39
45
|
.Builder()
|
|
40
46
|
.apply {
|
|
41
|
-
setUri(uri)
|
|
47
|
+
setUri(parseLocalAssetId(uri, context))
|
|
42
48
|
setMediaId(toMediaId())
|
|
43
49
|
drm?.let {
|
|
44
50
|
if (it.type.isSupported()) {
|
|
@@ -57,4 +63,31 @@ class VideoSource(
|
|
|
57
63
|
)
|
|
58
64
|
}
|
|
59
65
|
.build()
|
|
66
|
+
|
|
67
|
+
// Using `resolveAssetSource` to generate a local asset URI returns a resource name for android release builds
|
|
68
|
+
// we have to get the raw resource URI to play the video
|
|
69
|
+
@SuppressLint("DiscouragedApi") // AFAIK, in this case, there's no other way to get the resource URI
|
|
70
|
+
private fun parseLocalAssetId(uri: Uri?, context: Context): Uri? {
|
|
71
|
+
if (uri == null || uri.scheme != null) {
|
|
72
|
+
return uri
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
val resourceId: Int = context.resources.getIdentifier(
|
|
76
|
+
uri.toString(),
|
|
77
|
+
"raw",
|
|
78
|
+
context.packageName
|
|
79
|
+
)
|
|
80
|
+
val parsedUri = Uri.Builder()
|
|
81
|
+
.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
|
|
82
|
+
.appendPath(resourceId.toString())
|
|
83
|
+
.build()
|
|
84
|
+
val dataSpec = DataSpec(parsedUri)
|
|
85
|
+
val rawResourceDataSource = RawResourceDataSource(context)
|
|
86
|
+
rawResourceDataSource.open(dataSpec)
|
|
87
|
+
return rawResourceDataSource.uri
|
|
88
|
+
} catch (e: RawResourceDataSource.RawResourceDataSourceException) {
|
|
89
|
+
Log.e("ExpoVideo", "Error parsing local asset id, falling back to original uri", e)
|
|
90
|
+
return uri
|
|
91
|
+
}
|
|
92
|
+
}
|
|
60
93
|
}
|
|
@@ -44,7 +44,7 @@ fun buildMediaSourceFactory(context: Context, dataSourceFactory: DataSource.Fact
|
|
|
44
44
|
fun buildMediaSourceWithHeaders(context: Context, videoSource: VideoSource): MediaSource {
|
|
45
45
|
val dataSourceFactory = buildDataSourceFactory(context, videoSource)
|
|
46
46
|
val mediaSourceFactory = buildMediaSourceFactory(context, dataSourceFactory)
|
|
47
|
-
val mediaItem = videoSource.toMediaItem()
|
|
47
|
+
val mediaItem = videoSource.toMediaItem(context)
|
|
48
48
|
return mediaSourceFactory.createMediaSource(mediaItem)
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoPlayer.d.ts","sourceRoot":"","sources":["../src/VideoPlayer.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"VideoPlayer.d.ts","sourceRoot":"","sources":["../src/VideoPlayer.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAQpE;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,WAAW,EACnB,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,GACpC,WAAW,CAQb;;AAeD,wBAA6C"}
|
package/build/VideoPlayer.js
CHANGED
|
@@ -1,17 +1,35 @@
|
|
|
1
1
|
import { useReleasingSharedObject } from 'expo-modules-core';
|
|
2
|
+
import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
|
|
2
3
|
import NativeVideoModule from './NativeVideoModule';
|
|
4
|
+
// TODO: Temporary solution until we develop a way of overriding prototypes that won't break the lazy loading of the module.
|
|
5
|
+
const replace = NativeVideoModule.VideoPlayer.prototype.replace;
|
|
6
|
+
NativeVideoModule.VideoPlayer.prototype.replace = function (source) {
|
|
7
|
+
return replace.call(this, parseSource(source));
|
|
8
|
+
};
|
|
3
9
|
/**
|
|
4
10
|
* Creates a `VideoPlayer`, which will be automatically cleaned up when the component is unmounted.
|
|
5
11
|
* @param source - A video source that is used to initialize the player.
|
|
6
12
|
* @param setup - A function that allows setting up the player. It will run after the player is created.
|
|
7
13
|
*/
|
|
8
14
|
export function useVideoPlayer(source, setup) {
|
|
9
|
-
const parsedSource =
|
|
15
|
+
const parsedSource = parseSource(source);
|
|
10
16
|
return useReleasingSharedObject(() => {
|
|
11
17
|
const player = new NativeVideoModule.VideoPlayer(parsedSource);
|
|
12
18
|
setup?.(player);
|
|
13
19
|
return player;
|
|
14
20
|
}, [JSON.stringify(parsedSource)]);
|
|
15
21
|
}
|
|
22
|
+
function parseSource(source) {
|
|
23
|
+
if (typeof source === 'number') {
|
|
24
|
+
return { uri: resolveAssetSource(source).uri };
|
|
25
|
+
}
|
|
26
|
+
else if (typeof source === 'string') {
|
|
27
|
+
return { uri: source };
|
|
28
|
+
}
|
|
29
|
+
if (typeof source?.assetId === 'number' && !source.uri) {
|
|
30
|
+
return { ...source, uri: resolveAssetSource(source.assetId).uri };
|
|
31
|
+
}
|
|
32
|
+
return source;
|
|
33
|
+
}
|
|
16
34
|
export default NativeVideoModule.VideoPlayer;
|
|
17
35
|
//# sourceMappingURL=VideoPlayer.js.map
|
package/build/VideoPlayer.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoPlayer.js","sourceRoot":"","sources":["../src/VideoPlayer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"VideoPlayer.js","sourceRoot":"","sources":["../src/VideoPlayer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,kBAAkB,MAAM,iDAAiD,CAAC;AAEjF,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AAGpD,4HAA4H;AAC5H,MAAM,OAAO,GAAG,iBAAiB,CAAC,WAAW,CAAC,SAAS,CAAC,OAAO,CAAC;AAChE,iBAAiB,CAAC,WAAW,CAAC,SAAS,CAAC,OAAO,GAAG,UAAU,MAAmB;IAC7E,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;AACjD,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,MAAmB,EACnB,KAAqC;IAErC,MAAM,YAAY,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IAEzC,OAAO,wBAAwB,CAAC,GAAG,EAAE;QACnC,MAAM,MAAM,GAAG,IAAI,iBAAiB,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC;QAC/D,KAAK,EAAE,CAAC,MAAM,CAAC,CAAC;QAChB,OAAO,MAAM,CAAC;IAChB,CAAC,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;AACrC,CAAC;AAED,SAAS,WAAW,CAAC,MAAmB;IACtC,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE;QAC9B,OAAO,EAAE,GAAG,EAAE,kBAAkB,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC;KAChD;SAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE;QACrC,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;KACxB;IAED,IAAI,OAAO,MAAM,EAAE,OAAO,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE;QACtD,OAAO,EAAE,GAAG,MAAM,EAAE,GAAG,EAAE,kBAAkB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC;KACnE;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,eAAe,iBAAiB,CAAC,WAAW,CAAC","sourcesContent":["import { useReleasingSharedObject } from 'expo-modules-core';\nimport resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';\n\nimport NativeVideoModule from './NativeVideoModule';\nimport type { VideoPlayer, VideoSource } from './VideoPlayer.types';\n\n// TODO: Temporary solution until we develop a way of overriding prototypes that won't break the lazy loading of the module.\nconst replace = NativeVideoModule.VideoPlayer.prototype.replace;\nNativeVideoModule.VideoPlayer.prototype.replace = function (source: VideoSource) {\n return replace.call(this, parseSource(source));\n};\n\n/**\n * Creates a `VideoPlayer`, which will be automatically cleaned up when the component is unmounted.\n * @param source - A video source that is used to initialize the player.\n * @param setup - A function that allows setting up the player. It will run after the player is created.\n */\nexport function useVideoPlayer(\n source: VideoSource,\n setup?: (player: VideoPlayer) => void\n): VideoPlayer {\n const parsedSource = parseSource(source);\n\n return useReleasingSharedObject(() => {\n const player = new NativeVideoModule.VideoPlayer(parsedSource);\n setup?.(player);\n return player;\n }, [JSON.stringify(parsedSource)]);\n}\n\nfunction parseSource(source: VideoSource): VideoSource {\n if (typeof source === 'number') {\n return { uri: resolveAssetSource(source).uri };\n } else if (typeof source === 'string') {\n return { uri: source };\n }\n\n if (typeof source?.assetId === 'number' && !source.uri) {\n return { ...source, uri: resolveAssetSource(source.assetId).uri };\n }\n return source;\n}\n\nexport default NativeVideoModule.VideoPlayer;\n"]}
|
|
@@ -28,6 +28,28 @@ export declare class VideoPlayer extends SharedObject<VideoPlayerEvents> {
|
|
|
28
28
|
* Setting `currentTime` to a new value seeks the player to the given time.
|
|
29
29
|
*/
|
|
30
30
|
currentTime: number;
|
|
31
|
+
/**
|
|
32
|
+
* The exact timestamp when the currently displayed video frame was sent from the server,
|
|
33
|
+
* based on the `EXT-X-PROGRAM-DATE-TIME` tag in the livestream metadata.
|
|
34
|
+
* If this metadata is missing, this property will return `null`.
|
|
35
|
+
* > This property is read-only.
|
|
36
|
+
* @platform android
|
|
37
|
+
* @platform ios
|
|
38
|
+
*/
|
|
39
|
+
readonly currentLiveTimestamp: number | null;
|
|
40
|
+
/**
|
|
41
|
+
* Float value indicating the latency of the live stream in seconds.
|
|
42
|
+
* If a livestream doesn't have the required metadata, this will return `null`.
|
|
43
|
+
* > This property is get-only
|
|
44
|
+
* @platform android
|
|
45
|
+
* @platform ios
|
|
46
|
+
*/
|
|
47
|
+
readonly currentOffsetFromLive: number | null;
|
|
48
|
+
/**
|
|
49
|
+
* Float value indicating the time offset from the live in seconds.
|
|
50
|
+
* @platform ios
|
|
51
|
+
*/
|
|
52
|
+
targetOffsetFromLive: number;
|
|
31
53
|
/**
|
|
32
54
|
* Float value indicating the duration of the current video in seconds.
|
|
33
55
|
* > This property is get-only
|
|
@@ -65,6 +87,10 @@ export declare class VideoPlayer extends SharedObject<VideoPlayerEvents> {
|
|
|
65
87
|
status: VideoPlayerStatus;
|
|
66
88
|
/**
|
|
67
89
|
* Boolean value determining whether the player should show the now playing notification.
|
|
90
|
+
*
|
|
91
|
+
* @default false
|
|
92
|
+
* @platrorm android
|
|
93
|
+
* @platform ios
|
|
68
94
|
*/
|
|
69
95
|
showNowPlayingNotification: boolean;
|
|
70
96
|
/**
|
|
@@ -137,11 +163,18 @@ export type VideoPlayerEvents = {
|
|
|
137
163
|
* - `error`: The player has encountered an error while loading or playing the video.
|
|
138
164
|
*/
|
|
139
165
|
export type VideoPlayerStatus = 'idle' | 'loading' | 'readyToPlay' | 'error';
|
|
140
|
-
export type VideoSource = string | {
|
|
166
|
+
export type VideoSource = string | number | {
|
|
141
167
|
/**
|
|
142
168
|
* The URI of the video.
|
|
169
|
+
*
|
|
170
|
+
* This property is exclusive with the `assetId` property. When both are present, the `assetId` will be ignored.
|
|
171
|
+
*/
|
|
172
|
+
uri?: string;
|
|
173
|
+
/**
|
|
174
|
+
* The asset ID of a local video asset, acquired with the `require` function.
|
|
175
|
+
* This property is exclusive with the `uri` property. When both are present, the `assetId` will be ignored.
|
|
143
176
|
*/
|
|
144
|
-
|
|
177
|
+
assetId?: number;
|
|
145
178
|
/**
|
|
146
179
|
* Specifies the DRM options which will be used by the player while loading the video.
|
|
147
180
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoPlayer.types.d.ts","sourceRoot":"","sources":["../src/VideoPlayer.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD;;GAEG;AACH,MAAM,CAAC,OAAO,OAAO,WAAY,SAAQ,YAAY,CAAC,iBAAiB,CAAC;IACtE;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,IAAI,EAAE,OAAO,CAAC;IAEd;;;;OAIG;IACH,KAAK,EAAE,OAAO,CAAC;IAEf;;;;;;;OAOG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;;;OAKG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;;;;;OAMG;IACH,cAAc,EAAE,OAAO,CAAC;IAExB;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;;OAGG;IACH,MAAM,EAAE,iBAAiB,CAAC;IAE1B
|
|
1
|
+
{"version":3,"file":"VideoPlayer.types.d.ts","sourceRoot":"","sources":["../src/VideoPlayer.types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoPlayer.types.js","sourceRoot":"","sources":["../src/VideoPlayer.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { SharedObject } from 'expo-modules-core';\n\n/**\n * A class that represents an instance of the video player.\n */\nexport declare class VideoPlayer extends SharedObject<VideoPlayerEvents> {\n /**\n * Boolean value whether the player is currently playing.\n * > This property is get-only, use `play` and `pause` methods to control the playback.\n */\n playing: boolean;\n\n /**\n * Determines whether the player should automatically replay after reaching the end of the video.\n * @default false\n */\n loop: boolean;\n\n /**\n * Boolean value whether the player is currently muted.\n * Setting this property to `true`/`false` will mute/unmute the player.\n * @default false\n */\n muted: boolean;\n\n /**\n * Float value indicating the current playback time in seconds.\n *\n * If the player is not yet playing, this value indicates the time position\n * at which playback will begin once the `play()` method is called.\n *\n * Setting `currentTime` to a new value seeks the player to the given time.\n */\n currentTime: number;\n\n /**\n * Float value indicating the duration of the current video in seconds.\n * > This property is get-only\n */\n duration: number;\n\n /**\n * Float value between 0 and 1 representing the current volume.\n * Muting the player doesn't affect the volume. In other words, when the player is muted, the volume is the same as\n * when unmuted. Similarly, setting the volume doesn't unmute the player.\n * @default 1.0\n */\n volume: number;\n\n /**\n * Boolean value indicating if the player should correct audio pitch when the playback speed changes.\n * > On web, changing this property is not supported, the player will always correct the pitch.\n * @default true\n * @platform android\n * @platform ios\n */\n preservesPitch: boolean;\n\n /**\n * Float value between 0 and 16 indicating the current playback speed of the player.\n * @default 1.0\n */\n playbackRate: number;\n\n /**\n * Boolean value indicating whether the player is currently playing a live stream.\n * > This property is get-only\n */\n isLive: boolean;\n\n /**\n * Indicates the current status of the player.\n * > This property is get-only\n */\n status: VideoPlayerStatus;\n\n /**\n * Boolean value determining whether the player should show the now playing notification.\n */\n showNowPlayingNotification: boolean;\n\n /**\n * Determines whether the player should continue playing after the app enters the background.\n * @default false\n * @platform ios\n * @platform android\n */\n staysActiveInBackground: boolean;\n\n /**\n * Initializes a new video player instance with the given source.\n * @hidden\n */\n constructor(source: VideoSource);\n\n /**\n * Resumes the player.\n */\n play(): void;\n\n /**\n * Pauses the player.\n */\n pause(): void;\n\n /**\n * Replaces the current source with a new one.\n */\n replace(source: VideoSource): void;\n\n /**\n * Seeks the playback by the given number of seconds.\n */\n seekBy(seconds: number): void;\n\n /**\n * Seeks the playback to the beginning.\n */\n replay(): void;\n}\n\n/**\n * Handlers for events which can be emitted by the player.\n */\nexport type VideoPlayerEvents = {\n /**\n * Handler for an event emitted when the status of the player changes.\n */\n statusChange(\n newStatus: VideoPlayerStatus,\n oldStatus: VideoPlayerStatus,\n error?: PlayerError\n ): void;\n /**\n * Handler for an event emitted when the player starts or stops playback.\n */\n playingChange(newIsPlaying: boolean, oldIsPlaying: boolean): void;\n /**\n * Handler for an event emitted when the `playbackRate` property of the player changes.\n */\n playbackRateChange(newPlaybackRate: number, oldPlaybackRate: number): void;\n /**\n * Handler for an event emitted when the `volume` property of the player changes.\n */\n volumeChange(newVolume: VolumeEvent, oldVolume: VolumeEvent): void;\n /**\n * Handler for an event emitted when the player plays to the end of the current source.\n */\n playToEnd(): void;\n /**\n * Handler for an event emitted when the current media source of the player changes.\n */\n sourceChange(newSource: VideoSource, previousSource: VideoSource): void;\n};\n\n/**\n * Describes the current status of the player.\n * - `idle`: The player is not playing or loading any videos.\n * - `loading`: The player is loading video data from the provided source\n * - `readyToPlay`: The player has loaded enough data to start playing or to continue playback.\n * - `error`: The player has encountered an error while loading or playing the video.\n */\nexport type VideoPlayerStatus = 'idle' | 'loading' | 'readyToPlay' | 'error';\n\nexport type VideoSource =\n | string\n | {\n /**\n * The URI of the video.\n */\n uri
|
|
1
|
+
{"version":3,"file":"VideoPlayer.types.js","sourceRoot":"","sources":["../src/VideoPlayer.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { SharedObject } from 'expo-modules-core';\n\n/**\n * A class that represents an instance of the video player.\n */\nexport declare class VideoPlayer extends SharedObject<VideoPlayerEvents> {\n /**\n * Boolean value whether the player is currently playing.\n * > This property is get-only, use `play` and `pause` methods to control the playback.\n */\n playing: boolean;\n\n /**\n * Determines whether the player should automatically replay after reaching the end of the video.\n * @default false\n */\n loop: boolean;\n\n /**\n * Boolean value whether the player is currently muted.\n * Setting this property to `true`/`false` will mute/unmute the player.\n * @default false\n */\n muted: boolean;\n\n /**\n * Float value indicating the current playback time in seconds.\n *\n * If the player is not yet playing, this value indicates the time position\n * at which playback will begin once the `play()` method is called.\n *\n * Setting `currentTime` to a new value seeks the player to the given time.\n */\n currentTime: number;\n\n /**\n * The exact timestamp when the currently displayed video frame was sent from the server,\n * based on the `EXT-X-PROGRAM-DATE-TIME` tag in the livestream metadata.\n * If this metadata is missing, this property will return `null`.\n * > This property is read-only.\n * @platform android\n * @platform ios\n */\n readonly currentLiveTimestamp: number | null;\n\n /**\n * Float value indicating the latency of the live stream in seconds.\n * If a livestream doesn't have the required metadata, this will return `null`.\n * > This property is get-only\n * @platform android\n * @platform ios\n */\n readonly currentOffsetFromLive: number | null;\n\n /**\n * Float value indicating the time offset from the live in seconds.\n * @platform ios\n */\n targetOffsetFromLive: number;\n\n /**\n * Float value indicating the duration of the current video in seconds.\n * > This property is get-only\n */\n duration: number;\n\n /**\n * Float value between 0 and 1 representing the current volume.\n * Muting the player doesn't affect the volume. In other words, when the player is muted, the volume is the same as\n * when unmuted. Similarly, setting the volume doesn't unmute the player.\n * @default 1.0\n */\n volume: number;\n\n /**\n * Boolean value indicating if the player should correct audio pitch when the playback speed changes.\n * > On web, changing this property is not supported, the player will always correct the pitch.\n * @default true\n * @platform android\n * @platform ios\n */\n preservesPitch: boolean;\n\n /**\n * Float value between 0 and 16 indicating the current playback speed of the player.\n * @default 1.0\n */\n playbackRate: number;\n\n /**\n * Boolean value indicating whether the player is currently playing a live stream.\n * > This property is get-only\n */\n isLive: boolean;\n\n /**\n * Indicates the current status of the player.\n * > This property is get-only\n */\n status: VideoPlayerStatus;\n\n /**\n * Boolean value determining whether the player should show the now playing notification.\n *\n * @default false\n * @platrorm android\n * @platform ios\n */\n showNowPlayingNotification: boolean;\n\n /**\n * Determines whether the player should continue playing after the app enters the background.\n * @default false\n * @platform ios\n * @platform android\n */\n staysActiveInBackground: boolean;\n\n /**\n * Initializes a new video player instance with the given source.\n * @hidden\n */\n constructor(source: VideoSource);\n\n /**\n * Resumes the player.\n */\n play(): void;\n\n /**\n * Pauses the player.\n */\n pause(): void;\n\n /**\n * Replaces the current source with a new one.\n */\n replace(source: VideoSource): void;\n\n /**\n * Seeks the playback by the given number of seconds.\n */\n seekBy(seconds: number): void;\n\n /**\n * Seeks the playback to the beginning.\n */\n replay(): void;\n}\n\n/**\n * Handlers for events which can be emitted by the player.\n */\nexport type VideoPlayerEvents = {\n /**\n * Handler for an event emitted when the status of the player changes.\n */\n statusChange(\n newStatus: VideoPlayerStatus,\n oldStatus: VideoPlayerStatus,\n error?: PlayerError\n ): void;\n /**\n * Handler for an event emitted when the player starts or stops playback.\n */\n playingChange(newIsPlaying: boolean, oldIsPlaying: boolean): void;\n /**\n * Handler for an event emitted when the `playbackRate` property of the player changes.\n */\n playbackRateChange(newPlaybackRate: number, oldPlaybackRate: number): void;\n /**\n * Handler for an event emitted when the `volume` property of the player changes.\n */\n volumeChange(newVolume: VolumeEvent, oldVolume: VolumeEvent): void;\n /**\n * Handler for an event emitted when the player plays to the end of the current source.\n */\n playToEnd(): void;\n /**\n * Handler for an event emitted when the current media source of the player changes.\n */\n sourceChange(newSource: VideoSource, previousSource: VideoSource): void;\n};\n\n/**\n * Describes the current status of the player.\n * - `idle`: The player is not playing or loading any videos.\n * - `loading`: The player is loading video data from the provided source\n * - `readyToPlay`: The player has loaded enough data to start playing or to continue playback.\n * - `error`: The player has encountered an error while loading or playing the video.\n */\nexport type VideoPlayerStatus = 'idle' | 'loading' | 'readyToPlay' | 'error';\n\nexport type VideoSource =\n | string\n | number\n | {\n /**\n * The URI of the video.\n *\n * This property is exclusive with the `assetId` property. When both are present, the `assetId` will be ignored.\n */\n uri?: string;\n\n /**\n * The asset ID of a local video asset, acquired with the `require` function.\n * This property is exclusive with the `uri` property. When both are present, the `assetId` will be ignored.\n */\n assetId?: number;\n\n /**\n * Specifies the DRM options which will be used by the player while loading the video.\n */\n drm?: DRMOptions;\n\n /**\n * Specifies information which will be displayed in the now playing notification.\n * When undefined the player will display information contained in the video metadata.\n */\n metadata?: VideoMetadata;\n\n /**\n * Specifies headers sent with the video request.\n * > For DRM license headers use the `headers` field of [`DRMOptions`](#drmoptions).\n * @platform android\n * @platform ios\n */\n headers?: Record<string, string>;\n }\n | null;\n\n/**\n * Contains information about any errors that the player encountered during the playback\n */\nexport type PlayerError = {\n message: string;\n};\n\n/**\n * Contains information about the current volume and whether the player is muted.\n */\nexport type VolumeEvent = {\n volume: number;\n isMuted: boolean;\n};\n\n/**\n * Contains information that will be displayed in the now playing notification when the video is playing.\n */\nexport type VideoMetadata = {\n /**\n * The title of the video.\n */\n title?: string;\n /**\n * Secondary text that will be displayed under the title.\n */\n artist?: string;\n};\n\n/**\n * Specifies which type of DRM to use. Android supports Widevine, PlayReady and ClearKey, iOS supports FairPlay.\n */\nexport type DRMType = 'clearkey' | 'fairplay' | 'playready' | 'widevine';\n\n/**\n * Specifies DRM options which will be used by the player while loading the video.\n */\nexport type DRMOptions = {\n /**\n * Determines which type of DRM to use.\n */\n type: DRMType;\n\n /**\n * Determines the license server URL.\n */\n licenseServer: string;\n\n /**\n * Determines headers sent to the license server on license requests.\n */\n headers?: { [key: string]: string };\n\n /**\n * Specifies whether the DRM is a multi-key DRM.\n * @platform android\n */\n multiKey?: boolean;\n\n /**\n * Specifies the content ID of the stream.\n * @platform ios\n */\n contentId?: string;\n\n /**\n * Specifies the certificate URL for the FairPlay DRM.\n * @platform ios\n */\n certificateUrl?: string;\n\n /**\n * Specifies the base64 encoded certificate data for the FairPlay DRM.\n * When this property is set, the `certificateUrl` property is ignored.\n * @platform ios\n */\n base64CertificateData?: string;\n};\n"]}
|
|
@@ -17,6 +17,9 @@ export default class VideoPlayerWeb extends globalThis.expo.SharedObject<VideoPl
|
|
|
17
17
|
_error: PlayerError | null;
|
|
18
18
|
staysActiveInBackground: boolean;
|
|
19
19
|
showNowPlayingNotification: boolean;
|
|
20
|
+
currentLiveTimestamp: number | null;
|
|
21
|
+
currentOffsetFromLive: number | null;
|
|
22
|
+
targetOffsetFromLive: number;
|
|
20
23
|
set muted(value: boolean);
|
|
21
24
|
get muted(): boolean;
|
|
22
25
|
set playbackRate(value: number);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoPlayer.web.d.ts","sourceRoot":"","sources":["../src/VideoPlayer.web.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"VideoPlayer.web.d.ts","sourceRoot":"","sources":["../src/VideoPlayer.web.tsx"],"names":[],"mappings":"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"}
|
package/build/VideoPlayer.web.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
|
+
import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
|
|
2
3
|
export function useVideoPlayer(source, setup) {
|
|
3
4
|
const parsedSource = typeof source === 'string' ? { uri: source } : source;
|
|
4
5
|
return useMemo(() => {
|
|
@@ -8,9 +9,15 @@ export function useVideoPlayer(source, setup) {
|
|
|
8
9
|
}, [JSON.stringify(source)]);
|
|
9
10
|
}
|
|
10
11
|
export function getSourceUri(source) {
|
|
11
|
-
if (typeof source
|
|
12
|
+
if (typeof source === 'string') {
|
|
12
13
|
return source;
|
|
13
14
|
}
|
|
15
|
+
if (typeof source === 'number') {
|
|
16
|
+
return resolveAssetSource(source)?.uri ?? null;
|
|
17
|
+
}
|
|
18
|
+
if (typeof source?.assetId === 'number' && !source?.uri) {
|
|
19
|
+
return resolveAssetSource(source.assetId)?.uri ?? null;
|
|
20
|
+
}
|
|
14
21
|
return source?.uri ?? null;
|
|
15
22
|
}
|
|
16
23
|
export default class VideoPlayerWeb extends globalThis.expo.SharedObject {
|
|
@@ -32,6 +39,9 @@ export default class VideoPlayerWeb extends globalThis.expo.SharedObject {
|
|
|
32
39
|
_error = null;
|
|
33
40
|
staysActiveInBackground = false; // Not supported on web. Dummy to match the interface.
|
|
34
41
|
showNowPlayingNotification = false; // Not supported on web. Dummy to match the interface.
|
|
42
|
+
currentLiveTimestamp = null; // Not supported on web. Dummy to match the interface.
|
|
43
|
+
currentOffsetFromLive = null; // Not supported on web. Dummy to match the interface.
|
|
44
|
+
targetOffsetFromLive = 0; // Not supported on web. Dummy to match the interface.
|
|
35
45
|
set muted(value) {
|
|
36
46
|
this._mountedVideos.forEach((video) => {
|
|
37
47
|
video.muted = value;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoPlayer.web.js","sourceRoot":"","sources":["../src/VideoPlayer.web.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo } from 'react';\n\nimport type {\n PlayerError,\n VideoPlayer,\n VideoPlayerEvents,\n VideoPlayerStatus,\n VideoSource,\n} from './VideoPlayer.types';\n\nexport function useVideoPlayer(\n source: VideoSource,\n setup?: (player: VideoPlayer) => void\n): VideoPlayer {\n const parsedSource = typeof source === 'string' ? { uri: source } : source;\n\n return useMemo(() => {\n const player = new VideoPlayerWeb(parsedSource);\n setup?.(player);\n return player;\n }, [JSON.stringify(source)]);\n}\n\nexport function getSourceUri(source: VideoSource): string | null {\n if (typeof source == 'string') {\n return source;\n }\n return source?.uri ?? null;\n}\n\nexport default class VideoPlayerWeb\n extends globalThis.expo.SharedObject<VideoPlayerEvents>\n implements VideoPlayer\n{\n constructor(source: VideoSource) {\n super();\n this.src = source;\n }\n\n src: VideoSource = null;\n previousSrc: VideoSource = null;\n _mountedVideos: Set<HTMLVideoElement> = new Set();\n _audioNodes: Set<MediaElementAudioSourceNode> = new Set();\n playing: boolean = false;\n _muted: boolean = false;\n _volume: number = 1;\n _loop: boolean = false;\n _playbackRate: number = 1.0;\n _preservesPitch: boolean = true;\n _status: VideoPlayerStatus = 'idle';\n _error: PlayerError | null = null;\n staysActiveInBackground: boolean = false; // Not supported on web. Dummy to match the interface.\n showNowPlayingNotification: boolean = false; // Not supported on web. Dummy to match the interface.\n\n set muted(value: boolean) {\n this._mountedVideos.forEach((video) => {\n video.muted = value;\n });\n this._muted = value;\n }\n\n get muted(): boolean {\n return this._muted;\n }\n\n set playbackRate(value: number) {\n this._mountedVideos.forEach((video) => {\n video.playbackRate = value;\n });\n }\n\n get playbackRate(): number {\n return this._playbackRate;\n }\n\n get isLive(): boolean {\n return [...this._mountedVideos][0].duration === Infinity;\n }\n\n set volume(value: number) {\n this._mountedVideos.forEach((video) => {\n video.volume = value;\n });\n this._volume = value;\n }\n\n get volume(): number {\n return this._volume;\n }\n\n set loop(value: boolean) {\n this._mountedVideos.forEach((video) => {\n video.loop = value;\n });\n this._loop = value;\n }\n\n get loop(): boolean {\n return this._loop;\n }\n\n get currentTime(): number {\n // All videos should be synchronized, so we return the position of the first video.\n return [...this._mountedVideos][0].currentTime;\n }\n\n set currentTime(value: number) {\n this._mountedVideos.forEach((video) => {\n video.currentTime = value;\n });\n }\n\n get duration(): number {\n // All videos should have the same duration, so we return the duration of the first video.\n return [...this._mountedVideos][0].duration;\n }\n\n get preservesPitch(): boolean {\n return this._preservesPitch;\n }\n\n set preservesPitch(value: boolean) {\n this._mountedVideos.forEach((video) => {\n video.preservesPitch = value;\n });\n this._preservesPitch = value;\n }\n\n get status(): VideoPlayerStatus {\n return this._status;\n }\n\n private set status(value: VideoPlayerStatus) {\n if (this._status === value) return;\n\n if (value === 'error' && this._error) {\n this.emit('statusChange', value, this._status, this._error);\n } else {\n this.emit('statusChange', value, this._status);\n this._error = null;\n }\n this._status = value;\n }\n\n mountVideoView(video: HTMLVideoElement) {\n // The video will be the first video, it should inherit the properties set in the setup() function\n if (this._mountedVideos.size === 0) {\n video.preservesPitch = this._preservesPitch;\n video.loop = this._loop;\n video.volume = this._volume;\n video.muted = this._muted;\n video.playbackRate = this._playbackRate;\n }\n this._mountedVideos.add(video);\n this._addListeners(video);\n this._synchronizeWithFirstVideo(video);\n }\n\n unmountVideoView(video: HTMLVideoElement) {\n this._mountedVideos.delete(video);\n }\n\n mountAudioNode(\n audioContext: AudioContext,\n zeroGainNode: GainNode,\n audioSourceNode: MediaElementAudioSourceNode\n ): void {\n if (!audioContext || !zeroGainNode) return;\n\n this._audioNodes.add(audioSourceNode);\n // First mounted video should be connected to the audio context. All other videos have to be muted.\n if (this._audioNodes.size === 1) {\n audioSourceNode.connect(audioContext.destination);\n } else {\n audioSourceNode.connect(zeroGainNode);\n }\n }\n\n unmountAudioNode(\n video: HTMLVideoElement,\n audioContext: AudioContext,\n audioSourceNode: MediaElementAudioSourceNode\n ) {\n const mountedVideos = [...this._mountedVideos];\n const videoPlayingAudio = mountedVideos[0];\n this._audioNodes.delete(audioSourceNode);\n audioSourceNode.disconnect();\n\n // If video playing audio has been removed, select a new video to be the audio player by disconnecting it from the mute node.\n if (videoPlayingAudio === video && this._audioNodes.size > 0 && audioContext) {\n const newMainAudioSource = [...this._audioNodes][0];\n newMainAudioSource.disconnect();\n newMainAudioSource.connect(audioContext.destination);\n }\n }\n\n play(): void {\n this._mountedVideos.forEach((video) => {\n video.play();\n });\n }\n\n pause(): void {\n this._mountedVideos.forEach((video) => {\n video.pause();\n });\n }\n\n replace(source: VideoSource): void {\n this._mountedVideos.forEach((video) => {\n const uri = getSourceUri(source);\n video.pause();\n if (uri) {\n video.setAttribute('src', uri);\n video.load();\n video.play();\n } else {\n video.removeAttribute('src');\n video.load();\n }\n });\n // TODO @behenate: this won't work when we add support for playlists\n this.previousSrc = this.src;\n this.src = source;\n this.playing = true;\n }\n\n seekBy(seconds: number): void {\n this._mountedVideos.forEach((video) => {\n video.currentTime += seconds;\n });\n }\n\n replay(): void {\n this._mountedVideos.forEach((video) => {\n video.currentTime = 0;\n video.play();\n });\n this.playing = true;\n }\n\n _synchronizeWithFirstVideo(video: HTMLVideoElement): void {\n const firstVideo = [...this._mountedVideos][0];\n if (!firstVideo) return;\n\n if (firstVideo.paused) {\n video.pause();\n } else {\n video.play();\n }\n video.currentTime = firstVideo.currentTime;\n video.volume = firstVideo.volume;\n video.muted = firstVideo.muted;\n video.playbackRate = firstVideo.playbackRate;\n }\n\n /**\n * If there are multiple mounted videos, all of them will emit an event, as they are synchronised.\n * We want to avoid this, so we only emit the event if it came from the first video.\n */\n _emitOnce<EventName extends keyof VideoPlayerEvents>(\n eventSource: HTMLVideoElement,\n eventName: EventName,\n ...args: Parameters<VideoPlayerEvents[EventName]>\n ): void {\n const mountedVideos = [...this._mountedVideos];\n if (mountedVideos[0] === eventSource) {\n this.emit(eventName, ...args);\n }\n }\n\n _addListeners(video: HTMLVideoElement): void {\n video.onplay = () => {\n this._emitOnce(video, 'playingChange', true, this.playing);\n this.playing = true;\n this._mountedVideos.forEach((mountedVideo) => {\n mountedVideo.play();\n });\n };\n\n video.onpause = () => {\n this._emitOnce(video, 'playingChange', false, this.playing);\n this.playing = false;\n this._mountedVideos.forEach((mountedVideo) => {\n mountedVideo.pause();\n });\n };\n\n video.onvolumechange = () => {\n this._emitOnce(\n video,\n 'volumeChange',\n { volume: video.volume, isMuted: video.muted },\n { volume: this.volume, isMuted: this.muted }\n );\n this.volume = video.volume;\n this.muted = video.muted;\n };\n\n video.onseeking = () => {\n this._mountedVideos.forEach((mountedVideo) => {\n if (mountedVideo === video || mountedVideo.currentTime === video.currentTime) return;\n mountedVideo.currentTime = video.currentTime;\n });\n };\n\n video.onseeked = () => {\n this._mountedVideos.forEach((mountedVideo) => {\n if (mountedVideo === video || mountedVideo.currentTime === video.currentTime) return;\n mountedVideo.currentTime = video.currentTime;\n });\n };\n\n video.onratechange = () => {\n this._emitOnce(video, 'playbackRateChange', video.playbackRate, this.playbackRate);\n this._mountedVideos.forEach((mountedVideo) => {\n if (mountedVideo.playbackRate === video.playbackRate) return;\n this._playbackRate = video.playbackRate;\n mountedVideo.playbackRate = video.playbackRate;\n });\n this._playbackRate = video.playbackRate;\n };\n\n video.onerror = () => {\n this._error = {\n message: video.error?.message ?? 'Unknown player error',\n };\n this.status = 'error';\n };\n\n video.oncanplay = () => {\n const allCanPlay = [...this._mountedVideos].reduce((previousValue, video) => {\n return previousValue && video.readyState >= 3;\n }, true);\n if (!allCanPlay) return;\n\n this.status = 'readyToPlay';\n };\n\n video.onwaiting = () => {\n if (this._status === 'loading') return;\n this.status = 'loading';\n };\n\n video.onended = () => {\n this._emitOnce(video, 'playToEnd');\n };\n\n video.onloadstart = () => {\n this._emitOnce(video, 'sourceChange', this.src, this.previousSrc);\n };\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"VideoPlayer.web.js","sourceRoot":"","sources":["../src/VideoPlayer.web.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo } from 'react';\nimport resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';\n\nimport type {\n PlayerError,\n VideoPlayer,\n VideoPlayerEvents,\n VideoPlayerStatus,\n VideoSource,\n} from './VideoPlayer.types';\n\nexport function useVideoPlayer(\n source: VideoSource,\n setup?: (player: VideoPlayer) => void\n): VideoPlayer {\n const parsedSource = typeof source === 'string' ? { uri: source } : source;\n\n return useMemo(() => {\n const player = new VideoPlayerWeb(parsedSource);\n setup?.(player);\n return player;\n }, [JSON.stringify(source)]);\n}\n\nexport function getSourceUri(source: VideoSource): string | null {\n if (typeof source === 'string') {\n return source;\n }\n if (typeof source === 'number') {\n return resolveAssetSource(source)?.uri ?? null;\n }\n if (typeof source?.assetId === 'number' && !source?.uri) {\n return resolveAssetSource(source.assetId)?.uri ?? null;\n }\n\n return source?.uri ?? null;\n}\n\nexport default class VideoPlayerWeb\n extends globalThis.expo.SharedObject<VideoPlayerEvents>\n implements VideoPlayer\n{\n constructor(source: VideoSource) {\n super();\n this.src = source;\n }\n\n src: VideoSource = null;\n previousSrc: VideoSource = null;\n _mountedVideos: Set<HTMLVideoElement> = new Set();\n _audioNodes: Set<MediaElementAudioSourceNode> = new Set();\n playing: boolean = false;\n _muted: boolean = false;\n _volume: number = 1;\n _loop: boolean = false;\n _playbackRate: number = 1.0;\n _preservesPitch: boolean = true;\n _status: VideoPlayerStatus = 'idle';\n _error: PlayerError | null = null;\n staysActiveInBackground: boolean = false; // Not supported on web. Dummy to match the interface.\n showNowPlayingNotification: boolean = false; // Not supported on web. Dummy to match the interface.\n currentLiveTimestamp: number | null = null; // Not supported on web. Dummy to match the interface.\n currentOffsetFromLive: number | null = null; // Not supported on web. Dummy to match the interface.\n targetOffsetFromLive: number = 0; // Not supported on web. Dummy to match the interface.\n\n set muted(value: boolean) {\n this._mountedVideos.forEach((video) => {\n video.muted = value;\n });\n this._muted = value;\n }\n\n get muted(): boolean {\n return this._muted;\n }\n\n set playbackRate(value: number) {\n this._mountedVideos.forEach((video) => {\n video.playbackRate = value;\n });\n }\n\n get playbackRate(): number {\n return this._playbackRate;\n }\n\n get isLive(): boolean {\n return [...this._mountedVideos][0].duration === Infinity;\n }\n\n set volume(value: number) {\n this._mountedVideos.forEach((video) => {\n video.volume = value;\n });\n this._volume = value;\n }\n\n get volume(): number {\n return this._volume;\n }\n\n set loop(value: boolean) {\n this._mountedVideos.forEach((video) => {\n video.loop = value;\n });\n this._loop = value;\n }\n\n get loop(): boolean {\n return this._loop;\n }\n\n get currentTime(): number {\n // All videos should be synchronized, so we return the position of the first video.\n return [...this._mountedVideos][0].currentTime;\n }\n\n set currentTime(value: number) {\n this._mountedVideos.forEach((video) => {\n video.currentTime = value;\n });\n }\n\n get duration(): number {\n // All videos should have the same duration, so we return the duration of the first video.\n return [...this._mountedVideos][0].duration;\n }\n\n get preservesPitch(): boolean {\n return this._preservesPitch;\n }\n\n set preservesPitch(value: boolean) {\n this._mountedVideos.forEach((video) => {\n video.preservesPitch = value;\n });\n this._preservesPitch = value;\n }\n\n get status(): VideoPlayerStatus {\n return this._status;\n }\n\n private set status(value: VideoPlayerStatus) {\n if (this._status === value) return;\n\n if (value === 'error' && this._error) {\n this.emit('statusChange', value, this._status, this._error);\n } else {\n this.emit('statusChange', value, this._status);\n this._error = null;\n }\n this._status = value;\n }\n\n mountVideoView(video: HTMLVideoElement) {\n // The video will be the first video, it should inherit the properties set in the setup() function\n if (this._mountedVideos.size === 0) {\n video.preservesPitch = this._preservesPitch;\n video.loop = this._loop;\n video.volume = this._volume;\n video.muted = this._muted;\n video.playbackRate = this._playbackRate;\n }\n this._mountedVideos.add(video);\n this._addListeners(video);\n this._synchronizeWithFirstVideo(video);\n }\n\n unmountVideoView(video: HTMLVideoElement) {\n this._mountedVideos.delete(video);\n }\n\n mountAudioNode(\n audioContext: AudioContext,\n zeroGainNode: GainNode,\n audioSourceNode: MediaElementAudioSourceNode\n ): void {\n if (!audioContext || !zeroGainNode) return;\n\n this._audioNodes.add(audioSourceNode);\n // First mounted video should be connected to the audio context. All other videos have to be muted.\n if (this._audioNodes.size === 1) {\n audioSourceNode.connect(audioContext.destination);\n } else {\n audioSourceNode.connect(zeroGainNode);\n }\n }\n\n unmountAudioNode(\n video: HTMLVideoElement,\n audioContext: AudioContext,\n audioSourceNode: MediaElementAudioSourceNode\n ) {\n const mountedVideos = [...this._mountedVideos];\n const videoPlayingAudio = mountedVideos[0];\n this._audioNodes.delete(audioSourceNode);\n audioSourceNode.disconnect();\n\n // If video playing audio has been removed, select a new video to be the audio player by disconnecting it from the mute node.\n if (videoPlayingAudio === video && this._audioNodes.size > 0 && audioContext) {\n const newMainAudioSource = [...this._audioNodes][0];\n newMainAudioSource.disconnect();\n newMainAudioSource.connect(audioContext.destination);\n }\n }\n\n play(): void {\n this._mountedVideos.forEach((video) => {\n video.play();\n });\n }\n\n pause(): void {\n this._mountedVideos.forEach((video) => {\n video.pause();\n });\n }\n\n replace(source: VideoSource): void {\n this._mountedVideos.forEach((video) => {\n const uri = getSourceUri(source);\n video.pause();\n if (uri) {\n video.setAttribute('src', uri);\n video.load();\n video.play();\n } else {\n video.removeAttribute('src');\n video.load();\n }\n });\n // TODO @behenate: this won't work when we add support for playlists\n this.previousSrc = this.src;\n this.src = source;\n this.playing = true;\n }\n\n seekBy(seconds: number): void {\n this._mountedVideos.forEach((video) => {\n video.currentTime += seconds;\n });\n }\n\n replay(): void {\n this._mountedVideos.forEach((video) => {\n video.currentTime = 0;\n video.play();\n });\n this.playing = true;\n }\n\n _synchronizeWithFirstVideo(video: HTMLVideoElement): void {\n const firstVideo = [...this._mountedVideos][0];\n if (!firstVideo) return;\n\n if (firstVideo.paused) {\n video.pause();\n } else {\n video.play();\n }\n video.currentTime = firstVideo.currentTime;\n video.volume = firstVideo.volume;\n video.muted = firstVideo.muted;\n video.playbackRate = firstVideo.playbackRate;\n }\n\n /**\n * If there are multiple mounted videos, all of them will emit an event, as they are synchronised.\n * We want to avoid this, so we only emit the event if it came from the first video.\n */\n _emitOnce<EventName extends keyof VideoPlayerEvents>(\n eventSource: HTMLVideoElement,\n eventName: EventName,\n ...args: Parameters<VideoPlayerEvents[EventName]>\n ): void {\n const mountedVideos = [...this._mountedVideos];\n if (mountedVideos[0] === eventSource) {\n this.emit(eventName, ...args);\n }\n }\n\n _addListeners(video: HTMLVideoElement): void {\n video.onplay = () => {\n this._emitOnce(video, 'playingChange', true, this.playing);\n this.playing = true;\n this._mountedVideos.forEach((mountedVideo) => {\n mountedVideo.play();\n });\n };\n\n video.onpause = () => {\n this._emitOnce(video, 'playingChange', false, this.playing);\n this.playing = false;\n this._mountedVideos.forEach((mountedVideo) => {\n mountedVideo.pause();\n });\n };\n\n video.onvolumechange = () => {\n this._emitOnce(\n video,\n 'volumeChange',\n { volume: video.volume, isMuted: video.muted },\n { volume: this.volume, isMuted: this.muted }\n );\n this.volume = video.volume;\n this.muted = video.muted;\n };\n\n video.onseeking = () => {\n this._mountedVideos.forEach((mountedVideo) => {\n if (mountedVideo === video || mountedVideo.currentTime === video.currentTime) return;\n mountedVideo.currentTime = video.currentTime;\n });\n };\n\n video.onseeked = () => {\n this._mountedVideos.forEach((mountedVideo) => {\n if (mountedVideo === video || mountedVideo.currentTime === video.currentTime) return;\n mountedVideo.currentTime = video.currentTime;\n });\n };\n\n video.onratechange = () => {\n this._emitOnce(video, 'playbackRateChange', video.playbackRate, this.playbackRate);\n this._mountedVideos.forEach((mountedVideo) => {\n if (mountedVideo.playbackRate === video.playbackRate) return;\n this._playbackRate = video.playbackRate;\n mountedVideo.playbackRate = video.playbackRate;\n });\n this._playbackRate = video.playbackRate;\n };\n\n video.onerror = () => {\n this._error = {\n message: video.error?.message ?? 'Unknown player error',\n };\n this.status = 'error';\n };\n\n video.oncanplay = () => {\n const allCanPlay = [...this._mountedVideos].reduce((previousValue, video) => {\n return previousValue && video.readyState >= 3;\n }, true);\n if (!allCanPlay) return;\n\n this.status = 'readyToPlay';\n };\n\n video.onwaiting = () => {\n if (this._status === 'loading') return;\n this.status = 'loading';\n };\n\n video.onended = () => {\n this._emitOnce(video, 'playToEnd');\n };\n\n video.onloadstart = () => {\n this._emitOnce(video, 'sourceChange', this.src, this.previousSrc);\n };\n }\n}\n"]}
|
|
@@ -89,5 +89,13 @@ export interface VideoViewProps extends ViewProps {
|
|
|
89
89
|
* @platform ios 16.0+
|
|
90
90
|
*/
|
|
91
91
|
allowsVideoFrameAnalysis?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* A callback to call after the video player enters fullscreen mode.
|
|
94
|
+
*/
|
|
95
|
+
onFullscreenEnter?: () => void;
|
|
96
|
+
/**
|
|
97
|
+
* A callback to call after the video player exits fullscreen mode.
|
|
98
|
+
*/
|
|
99
|
+
onFullscreenExit?: () => void;
|
|
92
100
|
}
|
|
93
101
|
//# sourceMappingURL=VideoView.types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoView.types.d.ts","sourceRoot":"","sources":["../src/VideoView.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;AAE3D,MAAM,WAAW,cAAe,SAAQ,SAAS;IAC/C;;OAEG;IACH,MAAM,EAAE,WAAW,CAAC;IAEpB;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB;;;;OAIG;IACH,UAAU,CAAC,EAAE,eAAe,CAAC;IAE7B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAEjC;;;;OAIG;IACH,eAAe,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAE/C;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,MAAM,IAAI,CAAC;IAErC;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;IAEpC;;;;;;;OAOG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAEjC;;;;;;;;;;OAUG;IACH,mCAAmC,CAAC,EAAE,OAAO,CAAC;IAE9C;;;;OAIG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"VideoView.types.d.ts","sourceRoot":"","sources":["../src/VideoView.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;AAE3D,MAAM,WAAW,cAAe,SAAQ,SAAS;IAC/C;;OAEG;IACH,MAAM,EAAE,WAAW,CAAC;IAEpB;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB;;;;OAIG;IACH,UAAU,CAAC,EAAE,eAAe,CAAC;IAE7B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAEjC;;;;OAIG;IACH,eAAe,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAE/C;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,MAAM,IAAI,CAAC;IAErC;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;IAEpC;;;;;;;OAOG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAEjC;;;;;;;;;;OAUG;IACH,mCAAmC,CAAC,EAAE,OAAO,CAAC;IAE9C;;;;OAIG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;IAEnC;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;IAE/B;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;CAC/B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoView.types.js","sourceRoot":"","sources":["../src/VideoView.types.ts"],"names":[],"mappings":"","sourcesContent":["import { ViewProps } from 'react-native';\n\nimport type { VideoPlayer } from './VideoPlayer.types';\n\n/**\n * Describes how a video should be scaled to fit in a container.\n * - `contain`: The video maintains its aspect ratio and fits inside the container, with possible letterboxing/pillarboxing.\n * - `cover`: The video maintains its aspect ratio and covers the entire container, potentially cropping some portions.\n * - `fill`: The video stretches/squeezes to completely fill the container, potentially causing distortion.\n */\nexport type VideoContentFit = 'contain' | 'cover' | 'fill';\n\nexport interface VideoViewProps extends ViewProps {\n /**\n * A player instance – use `useVideoPlayer()` to create one.\n */\n player: VideoPlayer;\n\n /**\n * Determines whether native controls should be displayed or not.\n * @default true\n */\n nativeControls?: boolean;\n\n /**\n * Describes how the video should be scaled to fit in the container.\n * Options are 'contain', 'cover', and 'fill'.\n * @default 'contain'\n */\n contentFit?: VideoContentFit;\n\n /**\n * Determines whether fullscreen mode is allowed or not.\n * @default true\n */\n allowsFullscreen?: boolean;\n\n /**\n * Determines whether the timecodes should be displayed or not.\n * @default true\n * @platform ios\n */\n showsTimecodes?: boolean;\n\n /**\n * Determines whether the player allows the user to skip media content.\n * @default false\n * @platform android\n * @platform ios\n */\n requiresLinearPlayback?: boolean;\n\n /**\n * Determines the position offset of the video inside the container.\n * @default { dx: 0, dy: 0 }\n * @platform ios\n */\n contentPosition?: { dx?: number; dy?: number };\n\n /**\n * A callback to call after the video player enters Picture in Picture (PiP) mode.\n * @platform android\n * @platform ios 14+\n */\n onPictureInPictureStart?: () => void;\n\n /**\n * A callback to call after the video player exits Picture in Picture (PiP) mode.\n * @platform android\n * @platform ios 14+\n */\n onPictureInPictureStop?: () => void;\n\n /**\n * Determines whether the player allows Picture in Picture (PiP) mode.\n * > **Note:** The `supportsPictureInPicture` property of the [config plugin](#configuration-in-appjsonappconfigjs)\n * > has to be configured for the PiP to work.\n *\n * @default false\n * @platform ios 14+\n */\n allowsPictureInPicture?: boolean;\n\n /**\n * Determines whether the player should start Picture in Picture (PiP) automatically when the app is in the background.\n * > **Note:** Only one player can be in Picture in Picture (PiP) mode at a time.\n *\n * > **Note:** The `supportsPictureInPicture` property of the [config plugin](#configuration-in-appjsonappconfigjs)\n * > has to be configured for the PiP to work.\n *\n * @default false\n * @platform android 12+\n * @platform ios 14.2+\n */\n startsPictureInPictureAutomatically?: boolean;\n\n /**\n * Specifies whether to perform video frame analysis (Live Text in videos). Check official [Apple documentation](https://developer.apple.com/documentation/avkit/avplayerviewcontroller/allowsvideoframeanalysis) for more details.\n * @default true\n * @platform ios 16.0+\n */\n allowsVideoFrameAnalysis?: boolean;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"VideoView.types.js","sourceRoot":"","sources":["../src/VideoView.types.ts"],"names":[],"mappings":"","sourcesContent":["import { ViewProps } from 'react-native';\n\nimport type { VideoPlayer } from './VideoPlayer.types';\n\n/**\n * Describes how a video should be scaled to fit in a container.\n * - `contain`: The video maintains its aspect ratio and fits inside the container, with possible letterboxing/pillarboxing.\n * - `cover`: The video maintains its aspect ratio and covers the entire container, potentially cropping some portions.\n * - `fill`: The video stretches/squeezes to completely fill the container, potentially causing distortion.\n */\nexport type VideoContentFit = 'contain' | 'cover' | 'fill';\n\nexport interface VideoViewProps extends ViewProps {\n /**\n * A player instance – use `useVideoPlayer()` to create one.\n */\n player: VideoPlayer;\n\n /**\n * Determines whether native controls should be displayed or not.\n * @default true\n */\n nativeControls?: boolean;\n\n /**\n * Describes how the video should be scaled to fit in the container.\n * Options are 'contain', 'cover', and 'fill'.\n * @default 'contain'\n */\n contentFit?: VideoContentFit;\n\n /**\n * Determines whether fullscreen mode is allowed or not.\n * @default true\n */\n allowsFullscreen?: boolean;\n\n /**\n * Determines whether the timecodes should be displayed or not.\n * @default true\n * @platform ios\n */\n showsTimecodes?: boolean;\n\n /**\n * Determines whether the player allows the user to skip media content.\n * @default false\n * @platform android\n * @platform ios\n */\n requiresLinearPlayback?: boolean;\n\n /**\n * Determines the position offset of the video inside the container.\n * @default { dx: 0, dy: 0 }\n * @platform ios\n */\n contentPosition?: { dx?: number; dy?: number };\n\n /**\n * A callback to call after the video player enters Picture in Picture (PiP) mode.\n * @platform android\n * @platform ios 14+\n */\n onPictureInPictureStart?: () => void;\n\n /**\n * A callback to call after the video player exits Picture in Picture (PiP) mode.\n * @platform android\n * @platform ios 14+\n */\n onPictureInPictureStop?: () => void;\n\n /**\n * Determines whether the player allows Picture in Picture (PiP) mode.\n * > **Note:** The `supportsPictureInPicture` property of the [config plugin](#configuration-in-appjsonappconfigjs)\n * > has to be configured for the PiP to work.\n *\n * @default false\n * @platform ios 14+\n */\n allowsPictureInPicture?: boolean;\n\n /**\n * Determines whether the player should start Picture in Picture (PiP) automatically when the app is in the background.\n * > **Note:** Only one player can be in Picture in Picture (PiP) mode at a time.\n *\n * > **Note:** The `supportsPictureInPicture` property of the [config plugin](#configuration-in-appjsonappconfigjs)\n * > has to be configured for the PiP to work.\n *\n * @default false\n * @platform android 12+\n * @platform ios 14.2+\n */\n startsPictureInPictureAutomatically?: boolean;\n\n /**\n * Specifies whether to perform video frame analysis (Live Text in videos). Check official [Apple documentation](https://developer.apple.com/documentation/avkit/avplayerviewcontroller/allowsvideoframeanalysis) for more details.\n * @default true\n * @platform ios 16.0+\n */\n allowsVideoFrameAnalysis?: boolean;\n\n /**\n * A callback to call after the video player enters fullscreen mode.\n */\n onFullscreenEnter?: () => void;\n\n /**\n * A callback to call after the video player exits fullscreen mode.\n */\n onFullscreenExit?: () => void;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoView.web.d.ts","sourceRoot":"","sources":["../src/VideoView.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA6D,MAAM,OAAO,CAAC;AAGlF,OAAO,WAA6B,MAAM,mBAAmB,CAAC;AAC9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAsBxD,eAAO,MAAM,SAAS;;
|
|
1
|
+
{"version":3,"file":"VideoView.web.d.ts","sourceRoot":"","sources":["../src/VideoView.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA6D,MAAM,OAAO,CAAC;AAGlF,OAAO,WAA6B,MAAM,mBAAmB,CAAC;AAC9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAsBxD,eAAO,MAAM,SAAS;;kDAuIpB,CAAC;AAEH,eAAe,SAAS,CAAC"}
|
package/build/VideoView.web.js
CHANGED
|
@@ -21,6 +21,7 @@ export const VideoView = forwardRef((props, ref) => {
|
|
|
21
21
|
const videoRef = useRef(null);
|
|
22
22
|
const mediaNodeRef = useRef(null);
|
|
23
23
|
const hasToSetupAudioContext = useRef(false);
|
|
24
|
+
const fullscreenChangeListener = useRef(null);
|
|
24
25
|
/**
|
|
25
26
|
* Audio context is used to mute all but one video when multiple video views are playing from one player simultaneously.
|
|
26
27
|
* Using audio context nodes allows muting videos without displaying the mute icon in the video player.
|
|
@@ -76,15 +77,35 @@ export const VideoView = forwardRef((props, ref) => {
|
|
|
76
77
|
attachAudioNodes();
|
|
77
78
|
hasToSetupAudioContext.current = false;
|
|
78
79
|
}
|
|
80
|
+
function fullscreenListener() {
|
|
81
|
+
if (document.fullscreenElement === videoRef.current) {
|
|
82
|
+
props.onFullscreenEnter?.();
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
props.onFullscreenExit?.();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function setupFullscreenListener() {
|
|
89
|
+
fullscreenChangeListener.current = fullscreenListener;
|
|
90
|
+
videoRef.current?.addEventListener('fullscreenchange', fullscreenChangeListener.current);
|
|
91
|
+
}
|
|
92
|
+
function cleanupFullscreenListener() {
|
|
93
|
+
if (fullscreenChangeListener.current) {
|
|
94
|
+
videoRef.current?.removeEventListener('fullscreenchange', fullscreenChangeListener.current);
|
|
95
|
+
fullscreenChangeListener.current = null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
79
98
|
useEffect(() => {
|
|
80
99
|
if (videoRef.current) {
|
|
81
100
|
props.player?.mountVideoView(videoRef.current);
|
|
82
101
|
}
|
|
102
|
+
setupFullscreenListener();
|
|
83
103
|
attachAudioNodes();
|
|
84
104
|
return () => {
|
|
85
105
|
if (videoRef.current) {
|
|
86
106
|
props.player?.unmountVideoView(videoRef.current);
|
|
87
107
|
}
|
|
108
|
+
cleanupFullscreenListener();
|
|
88
109
|
detachAudioNodes();
|
|
89
110
|
};
|
|
90
111
|
}, [props.player]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoView.web.js","sourceRoot":"","sources":["../src/VideoView.web.tsx"],"names":[],"mappings":"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","sourcesContent":["import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react';\nimport { StyleSheet } from 'react-native';\n\nimport VideoPlayer, { getSourceUri } from './VideoPlayer.web';\nimport type { VideoViewProps } from './VideoView.types';\n\nfunction createAudioContext(): AudioContext | null {\n return typeof window !== 'undefined' ? new window.AudioContext() : null;\n}\n\nfunction createZeroGainNode(audioContext: AudioContext | null): GainNode | null {\n const zeroGainNode = audioContext?.createGain() ?? null;\n\n if (audioContext && zeroGainNode) {\n zeroGainNode.gain.value = 0;\n zeroGainNode.connect(audioContext.destination);\n }\n return zeroGainNode;\n}\n\nfunction mapStyles(style: VideoViewProps['style']): React.CSSProperties {\n const flattenedStyles = StyleSheet.flatten(style);\n // Looking through react-native-web source code they also just pass styles directly without further conversions, so it's just a cast.\n return flattenedStyles as React.CSSProperties;\n}\n\nexport const VideoView = forwardRef((props: { player?: VideoPlayer } & VideoViewProps, ref) => {\n const videoRef = useRef<null | HTMLVideoElement>(null);\n const mediaNodeRef = useRef<null | MediaElementAudioSourceNode>(null);\n const hasToSetupAudioContext = useRef(false);\n\n /**\n * Audio context is used to mute all but one video when multiple video views are playing from one player simultaneously.\n * Using audio context nodes allows muting videos without displaying the mute icon in the video player.\n * We have to keep the context that called createMediaElementSource(videoRef), as the method can't be called\n * for the second time with another context and there is no way to unbind the video and audio context afterward.\n */\n const audioContextRef = useRef<null | AudioContext>(null);\n const zeroGainNodeRef = useRef<null | GainNode>(null);\n\n useImperativeHandle(ref, () => ({\n enterFullscreen: () => {\n if (!props.allowsFullscreen) {\n return;\n }\n videoRef.current?.requestFullscreen();\n },\n exitFullscreen: () => {\n document.exitFullscreen();\n },\n }));\n\n // Adds the video view as a candidate for being the audio source for the player (when multiple views play from one\n // player only one will emit audio).\n function attachAudioNodes() {\n const audioContext = audioContextRef.current;\n const zeroGainNode = zeroGainNodeRef.current;\n const mediaNode = mediaNodeRef.current;\n\n if (audioContext && zeroGainNode && mediaNode) {\n props.player.mountAudioNode(audioContext, zeroGainNode, mediaNode);\n } else {\n console.warn(\n \"Couldn't mount audio node, this might affect the audio playback when using multiple video views with the same player.\"\n );\n }\n }\n\n function detachAudioNodes() {\n const audioContext = audioContextRef.current;\n const mediaNode = mediaNodeRef.current;\n if (audioContext && mediaNode && videoRef.current) {\n props.player.unmountAudioNode(videoRef.current, audioContext, mediaNode);\n }\n }\n\n function maybeSetupAudioContext() {\n if (\n !hasToSetupAudioContext.current ||\n !navigator.userActivation.hasBeenActive ||\n !videoRef.current\n ) {\n return;\n }\n const audioContext = createAudioContext();\n\n detachAudioNodes();\n audioContextRef.current = audioContext;\n zeroGainNodeRef.current = createZeroGainNode(audioContextRef.current);\n mediaNodeRef.current = audioContext\n ? audioContext.createMediaElementSource(videoRef.current)\n : null;\n attachAudioNodes();\n hasToSetupAudioContext.current = false;\n }\n\n useEffect(() => {\n if (videoRef.current) {\n props.player?.mountVideoView(videoRef.current);\n }\n attachAudioNodes();\n\n return () => {\n if (videoRef.current) {\n props.player?.unmountVideoView(videoRef.current);\n }\n detachAudioNodes();\n };\n }, [props.player]);\n\n return (\n <video\n controls={props.nativeControls ?? true}\n controlsList={props.allowsFullscreen ? undefined : 'nofullscreen'}\n crossOrigin=\"anonymous\"\n style={{\n ...mapStyles(props.style),\n objectFit: props.contentFit,\n }}\n onPlay={() => {\n maybeSetupAudioContext();\n }}\n // The player can autoplay when muted, unmuting by a user should create the audio context\n onVolumeChange={() => {\n maybeSetupAudioContext();\n }}\n ref={(newRef) => {\n // This is called with a null value before `player.unmountVideoView` is called,\n // we can't assign null to videoRef if we want to unmount it from the player.\n if (newRef && !newRef.isEqualNode(videoRef.current)) {\n videoRef.current = newRef;\n hasToSetupAudioContext.current = true;\n maybeSetupAudioContext();\n }\n }}\n src={getSourceUri(props.player?.src) ?? ''}\n />\n );\n});\n\nexport default VideoView;\n"]}
|
|
1
|
+
{"version":3,"file":"VideoView.web.js","sourceRoot":"","sources":["../src/VideoView.web.tsx"],"names":[],"mappings":"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","sourcesContent":["import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react';\nimport { StyleSheet } from 'react-native';\n\nimport VideoPlayer, { getSourceUri } from './VideoPlayer.web';\nimport type { VideoViewProps } from './VideoView.types';\n\nfunction createAudioContext(): AudioContext | null {\n return typeof window !== 'undefined' ? new window.AudioContext() : null;\n}\n\nfunction createZeroGainNode(audioContext: AudioContext | null): GainNode | null {\n const zeroGainNode = audioContext?.createGain() ?? null;\n\n if (audioContext && zeroGainNode) {\n zeroGainNode.gain.value = 0;\n zeroGainNode.connect(audioContext.destination);\n }\n return zeroGainNode;\n}\n\nfunction mapStyles(style: VideoViewProps['style']): React.CSSProperties {\n const flattenedStyles = StyleSheet.flatten(style);\n // Looking through react-native-web source code they also just pass styles directly without further conversions, so it's just a cast.\n return flattenedStyles as React.CSSProperties;\n}\n\nexport const VideoView = forwardRef((props: { player?: VideoPlayer } & VideoViewProps, ref) => {\n const videoRef = useRef<null | HTMLVideoElement>(null);\n const mediaNodeRef = useRef<null | MediaElementAudioSourceNode>(null);\n const hasToSetupAudioContext = useRef(false);\n const fullscreenChangeListener = useRef<null | (() => void)>(null);\n\n /**\n * Audio context is used to mute all but one video when multiple video views are playing from one player simultaneously.\n * Using audio context nodes allows muting videos without displaying the mute icon in the video player.\n * We have to keep the context that called createMediaElementSource(videoRef), as the method can't be called\n * for the second time with another context and there is no way to unbind the video and audio context afterward.\n */\n const audioContextRef = useRef<null | AudioContext>(null);\n const zeroGainNodeRef = useRef<null | GainNode>(null);\n\n useImperativeHandle(ref, () => ({\n enterFullscreen: () => {\n if (!props.allowsFullscreen) {\n return;\n }\n videoRef.current?.requestFullscreen();\n },\n exitFullscreen: () => {\n document.exitFullscreen();\n },\n }));\n\n // Adds the video view as a candidate for being the audio source for the player (when multiple views play from one\n // player only one will emit audio).\n function attachAudioNodes() {\n const audioContext = audioContextRef.current;\n const zeroGainNode = zeroGainNodeRef.current;\n const mediaNode = mediaNodeRef.current;\n\n if (audioContext && zeroGainNode && mediaNode) {\n props.player.mountAudioNode(audioContext, zeroGainNode, mediaNode);\n } else {\n console.warn(\n \"Couldn't mount audio node, this might affect the audio playback when using multiple video views with the same player.\"\n );\n }\n }\n\n function detachAudioNodes() {\n const audioContext = audioContextRef.current;\n const mediaNode = mediaNodeRef.current;\n if (audioContext && mediaNode && videoRef.current) {\n props.player.unmountAudioNode(videoRef.current, audioContext, mediaNode);\n }\n }\n\n function maybeSetupAudioContext() {\n if (\n !hasToSetupAudioContext.current ||\n !navigator.userActivation.hasBeenActive ||\n !videoRef.current\n ) {\n return;\n }\n const audioContext = createAudioContext();\n\n detachAudioNodes();\n audioContextRef.current = audioContext;\n zeroGainNodeRef.current = createZeroGainNode(audioContextRef.current);\n mediaNodeRef.current = audioContext\n ? audioContext.createMediaElementSource(videoRef.current)\n : null;\n attachAudioNodes();\n hasToSetupAudioContext.current = false;\n }\n\n function fullscreenListener() {\n if (document.fullscreenElement === videoRef.current) {\n props.onFullscreenEnter?.();\n } else {\n props.onFullscreenExit?.();\n }\n }\n\n function setupFullscreenListener() {\n fullscreenChangeListener.current = fullscreenListener;\n videoRef.current?.addEventListener('fullscreenchange', fullscreenChangeListener.current);\n }\n\n function cleanupFullscreenListener() {\n if (fullscreenChangeListener.current) {\n videoRef.current?.removeEventListener('fullscreenchange', fullscreenChangeListener.current);\n fullscreenChangeListener.current = null;\n }\n }\n\n useEffect(() => {\n if (videoRef.current) {\n props.player?.mountVideoView(videoRef.current);\n }\n setupFullscreenListener();\n attachAudioNodes();\n\n return () => {\n if (videoRef.current) {\n props.player?.unmountVideoView(videoRef.current);\n }\n cleanupFullscreenListener();\n detachAudioNodes();\n };\n }, [props.player]);\n\n return (\n <video\n controls={props.nativeControls ?? true}\n controlsList={props.allowsFullscreen ? undefined : 'nofullscreen'}\n crossOrigin=\"anonymous\"\n style={{\n ...mapStyles(props.style),\n objectFit: props.contentFit,\n }}\n onPlay={() => {\n maybeSetupAudioContext();\n }}\n // The player can autoplay when muted, unmuting by a user should create the audio context\n onVolumeChange={() => {\n maybeSetupAudioContext();\n }}\n ref={(newRef) => {\n // This is called with a null value before `player.unmountVideoView` is called,\n // we can't assign null to videoRef if we want to unmount it from the player.\n if (newRef && !newRef.isEqualNode(videoRef.current)) {\n videoRef.current = newRef;\n hasToSetupAudioContext.current = true;\n maybeSetupAudioContext();\n }\n }}\n src={getSourceUri(props.player?.src) ?? ''}\n />\n );\n});\n\nexport default VideoView;\n"]}
|
|
@@ -168,6 +168,7 @@ class NowPlayingManager: VideoPlayerObserverDelegate {
|
|
|
168
168
|
nowPlayingInfo[MPMediaItemPropertyArtist] = userMetadata?.artist ?? artist
|
|
169
169
|
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = currentItem.duration.seconds
|
|
170
170
|
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentItem.currentTime().seconds
|
|
171
|
+
nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = currentItem.duration.isIndefinite
|
|
171
172
|
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = await player.rate
|
|
172
173
|
nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaType.video.rawValue // Using MPNowPlayingInfoMediaType.video causes a crash
|
|
173
174
|
nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
|
package/ios/VideoModule.swift
CHANGED
|
@@ -16,7 +16,9 @@ public final class VideoModule: Module {
|
|
|
16
16
|
View(VideoView.self) {
|
|
17
17
|
Events(
|
|
18
18
|
"onPictureInPictureStart",
|
|
19
|
-
"onPictureInPictureStop"
|
|
19
|
+
"onPictureInPictureStop",
|
|
20
|
+
"onFullscreenEnter",
|
|
21
|
+
"onFullscreenExit"
|
|
20
22
|
)
|
|
21
23
|
|
|
22
24
|
Prop("player") { (view, player: VideoPlayer?) in
|
|
@@ -129,8 +131,11 @@ public final class VideoModule: Module {
|
|
|
129
131
|
player.isMuted = muted
|
|
130
132
|
}
|
|
131
133
|
|
|
132
|
-
Property("
|
|
133
|
-
return player.pointer.
|
|
134
|
+
Property("allowsExternalPlayback") { player -> Bool in
|
|
135
|
+
return player.pointer.allowsExternalPlayback
|
|
136
|
+
}
|
|
137
|
+
.set { (player, allowsExternalPlayback: Bool) in
|
|
138
|
+
player.pointer.allowsExternalPlayback = allowsExternalPlayback
|
|
134
139
|
}
|
|
135
140
|
|
|
136
141
|
Property("staysActiveInBackground") { player -> Bool in
|
|
@@ -148,7 +153,8 @@ public final class VideoModule: Module {
|
|
|
148
153
|
}
|
|
149
154
|
|
|
150
155
|
Property("currentTime") { player -> Double in
|
|
151
|
-
|
|
156
|
+
let currentTime = player.pointer.currentTime().seconds
|
|
157
|
+
return currentTime.isNaN ? 0 : currentTime
|
|
152
158
|
}
|
|
153
159
|
.set { (player, time: Double) in
|
|
154
160
|
// Only clamp the lower limit, AVPlayer automatically clamps the upper limit.
|
|
@@ -157,8 +163,34 @@ public final class VideoModule: Module {
|
|
|
157
163
|
player.pointer.seek(to: timeToSeek, toleranceBefore: .zero, toleranceAfter: .zero)
|
|
158
164
|
}
|
|
159
165
|
|
|
166
|
+
Property("currentLiveTimestamp") { player -> Double? in
|
|
167
|
+
guard let currentDate = player.pointer.currentItem?.currentDate() else {
|
|
168
|
+
return nil
|
|
169
|
+
}
|
|
170
|
+
let timeIntervalSince = currentDate.timeIntervalSince1970
|
|
171
|
+
return Double(timeIntervalSince * 1000)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
Property("currentOffsetFromLive") { player -> Double? in
|
|
175
|
+
guard let currentDate = player.pointer.currentItem?.currentDate() else {
|
|
176
|
+
return nil
|
|
177
|
+
}
|
|
178
|
+
let timeIntervalSince = currentDate.timeIntervalSince1970
|
|
179
|
+
let unixTime = Date().timeIntervalSince1970
|
|
180
|
+
return unixTime - timeIntervalSince
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
Property("targetOffsetFromLive") { player -> Double in
|
|
184
|
+
return player.pointer.currentItem?.configuredTimeOffsetFromLive.seconds ?? 0
|
|
185
|
+
}
|
|
186
|
+
.set { (player, timeOffset: Double) in
|
|
187
|
+
let timeOffset = CMTime(seconds: timeOffset, preferredTimescale: .max)
|
|
188
|
+
player.pointer.currentItem?.configuredTimeOffsetFromLive = timeOffset
|
|
189
|
+
}
|
|
190
|
+
|
|
160
191
|
Property("duration") { player -> Double in
|
|
161
|
-
|
|
192
|
+
let duration = player.pointer.currentItem?.duration.seconds ?? 0
|
|
193
|
+
return duration.isNaN ? 0 : duration
|
|
162
194
|
}
|
|
163
195
|
|
|
164
196
|
Property("playbackRate") { player -> Float in
|
package/ios/VideoView.swift
CHANGED
|
@@ -41,6 +41,8 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
|
|
41
41
|
|
|
42
42
|
let onPictureInPictureStart = EventDispatcher()
|
|
43
43
|
let onPictureInPictureStop = EventDispatcher()
|
|
44
|
+
let onFullscreenEnter = EventDispatcher()
|
|
45
|
+
let onFullscreenExit = EventDispatcher()
|
|
44
46
|
|
|
45
47
|
public override var bounds: CGRect {
|
|
46
48
|
didSet {
|
|
@@ -93,6 +95,7 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
|
|
93
95
|
wasPlaying = player?.isPlaying == true
|
|
94
96
|
self.playerViewController.view.removeFromSuperview()
|
|
95
97
|
self.reactViewController().present(self.playerViewController, animated: true)
|
|
98
|
+
onFullscreenEnter()
|
|
96
99
|
isFullscreen = true
|
|
97
100
|
#endif
|
|
98
101
|
}
|
|
@@ -143,6 +146,7 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
|
|
143
146
|
}
|
|
144
147
|
|
|
145
148
|
public func playerViewControllerDidEndDismissalTransition(_ playerViewController: AVPlayerViewController) {
|
|
149
|
+
self.onFullscreenExit()
|
|
146
150
|
self.isFullscreen = false
|
|
147
151
|
// Reset the bounds of the view controller and add it back to our view
|
|
148
152
|
self.playerViewController.view.frame = self.bounds
|
|
@@ -163,6 +167,7 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
|
|
163
167
|
_ playerViewController: AVPlayerViewController,
|
|
164
168
|
willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator
|
|
165
169
|
) {
|
|
170
|
+
onFullscreenEnter()
|
|
166
171
|
isFullscreen = true
|
|
167
172
|
}
|
|
168
173
|
|
|
@@ -179,6 +184,7 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
|
|
179
184
|
if wasPlaying {
|
|
180
185
|
self.player?.pointer.play()
|
|
181
186
|
}
|
|
187
|
+
self.onFullscreenExit()
|
|
182
188
|
self.isFullscreen = false
|
|
183
189
|
}
|
|
184
190
|
}
|
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.6",
|
|
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": "5ea2101954073640f715c183b322de8f722b60d3"
|
|
40
40
|
}
|
package/src/VideoPlayer.tsx
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { useReleasingSharedObject } from 'expo-modules-core';
|
|
2
|
+
import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
|
|
2
3
|
|
|
3
4
|
import NativeVideoModule from './NativeVideoModule';
|
|
4
5
|
import type { VideoPlayer, VideoSource } from './VideoPlayer.types';
|
|
5
6
|
|
|
7
|
+
// TODO: Temporary solution until we develop a way of overriding prototypes that won't break the lazy loading of the module.
|
|
8
|
+
const replace = NativeVideoModule.VideoPlayer.prototype.replace;
|
|
9
|
+
NativeVideoModule.VideoPlayer.prototype.replace = function (source: VideoSource) {
|
|
10
|
+
return replace.call(this, parseSource(source));
|
|
11
|
+
};
|
|
12
|
+
|
|
6
13
|
/**
|
|
7
14
|
* Creates a `VideoPlayer`, which will be automatically cleaned up when the component is unmounted.
|
|
8
15
|
* @param source - A video source that is used to initialize the player.
|
|
@@ -12,7 +19,7 @@ export function useVideoPlayer(
|
|
|
12
19
|
source: VideoSource,
|
|
13
20
|
setup?: (player: VideoPlayer) => void
|
|
14
21
|
): VideoPlayer {
|
|
15
|
-
const parsedSource =
|
|
22
|
+
const parsedSource = parseSource(source);
|
|
16
23
|
|
|
17
24
|
return useReleasingSharedObject(() => {
|
|
18
25
|
const player = new NativeVideoModule.VideoPlayer(parsedSource);
|
|
@@ -21,4 +28,17 @@ export function useVideoPlayer(
|
|
|
21
28
|
}, [JSON.stringify(parsedSource)]);
|
|
22
29
|
}
|
|
23
30
|
|
|
31
|
+
function parseSource(source: VideoSource): VideoSource {
|
|
32
|
+
if (typeof source === 'number') {
|
|
33
|
+
return { uri: resolveAssetSource(source).uri };
|
|
34
|
+
} else if (typeof source === 'string') {
|
|
35
|
+
return { uri: source };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (typeof source?.assetId === 'number' && !source.uri) {
|
|
39
|
+
return { ...source, uri: resolveAssetSource(source.assetId).uri };
|
|
40
|
+
}
|
|
41
|
+
return source;
|
|
42
|
+
}
|
|
43
|
+
|
|
24
44
|
export default NativeVideoModule.VideoPlayer;
|
package/src/VideoPlayer.types.ts
CHANGED
|
@@ -33,6 +33,31 @@ export declare class VideoPlayer extends SharedObject<VideoPlayerEvents> {
|
|
|
33
33
|
*/
|
|
34
34
|
currentTime: number;
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* The exact timestamp when the currently displayed video frame was sent from the server,
|
|
38
|
+
* based on the `EXT-X-PROGRAM-DATE-TIME` tag in the livestream metadata.
|
|
39
|
+
* If this metadata is missing, this property will return `null`.
|
|
40
|
+
* > This property is read-only.
|
|
41
|
+
* @platform android
|
|
42
|
+
* @platform ios
|
|
43
|
+
*/
|
|
44
|
+
readonly currentLiveTimestamp: number | null;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Float value indicating the latency of the live stream in seconds.
|
|
48
|
+
* If a livestream doesn't have the required metadata, this will return `null`.
|
|
49
|
+
* > This property is get-only
|
|
50
|
+
* @platform android
|
|
51
|
+
* @platform ios
|
|
52
|
+
*/
|
|
53
|
+
readonly currentOffsetFromLive: number | null;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Float value indicating the time offset from the live in seconds.
|
|
57
|
+
* @platform ios
|
|
58
|
+
*/
|
|
59
|
+
targetOffsetFromLive: number;
|
|
60
|
+
|
|
36
61
|
/**
|
|
37
62
|
* Float value indicating the duration of the current video in seconds.
|
|
38
63
|
* > This property is get-only
|
|
@@ -76,6 +101,10 @@ export declare class VideoPlayer extends SharedObject<VideoPlayerEvents> {
|
|
|
76
101
|
|
|
77
102
|
/**
|
|
78
103
|
* Boolean value determining whether the player should show the now playing notification.
|
|
104
|
+
*
|
|
105
|
+
* @default false
|
|
106
|
+
* @platrorm android
|
|
107
|
+
* @platform ios
|
|
79
108
|
*/
|
|
80
109
|
showNowPlayingNotification: boolean;
|
|
81
110
|
|
|
@@ -164,20 +193,32 @@ export type VideoPlayerStatus = 'idle' | 'loading' | 'readyToPlay' | 'error';
|
|
|
164
193
|
|
|
165
194
|
export type VideoSource =
|
|
166
195
|
| string
|
|
196
|
+
| number
|
|
167
197
|
| {
|
|
168
198
|
/**
|
|
169
199
|
* The URI of the video.
|
|
200
|
+
*
|
|
201
|
+
* This property is exclusive with the `assetId` property. When both are present, the `assetId` will be ignored.
|
|
170
202
|
*/
|
|
171
|
-
uri
|
|
203
|
+
uri?: string;
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The asset ID of a local video asset, acquired with the `require` function.
|
|
207
|
+
* This property is exclusive with the `uri` property. When both are present, the `assetId` will be ignored.
|
|
208
|
+
*/
|
|
209
|
+
assetId?: number;
|
|
210
|
+
|
|
172
211
|
/**
|
|
173
212
|
* Specifies the DRM options which will be used by the player while loading the video.
|
|
174
213
|
*/
|
|
175
214
|
drm?: DRMOptions;
|
|
215
|
+
|
|
176
216
|
/**
|
|
177
217
|
* Specifies information which will be displayed in the now playing notification.
|
|
178
218
|
* When undefined the player will display information contained in the video metadata.
|
|
179
219
|
*/
|
|
180
220
|
metadata?: VideoMetadata;
|
|
221
|
+
|
|
181
222
|
/**
|
|
182
223
|
* Specifies headers sent with the video request.
|
|
183
224
|
* > For DRM license headers use the `headers` field of [`DRMOptions`](#drmoptions).
|
package/src/VideoPlayer.web.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
|
+
import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
|
|
2
3
|
|
|
3
4
|
import type {
|
|
4
5
|
PlayerError,
|
|
@@ -22,9 +23,16 @@ export function useVideoPlayer(
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export function getSourceUri(source: VideoSource): string | null {
|
|
25
|
-
if (typeof source
|
|
26
|
+
if (typeof source === 'string') {
|
|
26
27
|
return source;
|
|
27
28
|
}
|
|
29
|
+
if (typeof source === 'number') {
|
|
30
|
+
return resolveAssetSource(source)?.uri ?? null;
|
|
31
|
+
}
|
|
32
|
+
if (typeof source?.assetId === 'number' && !source?.uri) {
|
|
33
|
+
return resolveAssetSource(source.assetId)?.uri ?? null;
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
return source?.uri ?? null;
|
|
29
37
|
}
|
|
30
38
|
|
|
@@ -51,6 +59,9 @@ export default class VideoPlayerWeb
|
|
|
51
59
|
_error: PlayerError | null = null;
|
|
52
60
|
staysActiveInBackground: boolean = false; // Not supported on web. Dummy to match the interface.
|
|
53
61
|
showNowPlayingNotification: boolean = false; // Not supported on web. Dummy to match the interface.
|
|
62
|
+
currentLiveTimestamp: number | null = null; // Not supported on web. Dummy to match the interface.
|
|
63
|
+
currentOffsetFromLive: number | null = null; // Not supported on web. Dummy to match the interface.
|
|
64
|
+
targetOffsetFromLive: number = 0; // Not supported on web. Dummy to match the interface.
|
|
54
65
|
|
|
55
66
|
set muted(value: boolean) {
|
|
56
67
|
this._mountedVideos.forEach((video) => {
|
package/src/VideoView.types.ts
CHANGED
|
@@ -100,4 +100,14 @@ export interface VideoViewProps extends ViewProps {
|
|
|
100
100
|
* @platform ios 16.0+
|
|
101
101
|
*/
|
|
102
102
|
allowsVideoFrameAnalysis?: boolean;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A callback to call after the video player enters fullscreen mode.
|
|
106
|
+
*/
|
|
107
|
+
onFullscreenEnter?: () => void;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A callback to call after the video player exits fullscreen mode.
|
|
111
|
+
*/
|
|
112
|
+
onFullscreenExit?: () => void;
|
|
103
113
|
}
|
package/src/VideoView.web.tsx
CHANGED
|
@@ -28,6 +28,7 @@ export const VideoView = forwardRef((props: { player?: VideoPlayer } & VideoView
|
|
|
28
28
|
const videoRef = useRef<null | HTMLVideoElement>(null);
|
|
29
29
|
const mediaNodeRef = useRef<null | MediaElementAudioSourceNode>(null);
|
|
30
30
|
const hasToSetupAudioContext = useRef(false);
|
|
31
|
+
const fullscreenChangeListener = useRef<null | (() => void)>(null);
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* Audio context is used to mute all but one video when multiple video views are playing from one player simultaneously.
|
|
@@ -94,16 +95,38 @@ export const VideoView = forwardRef((props: { player?: VideoPlayer } & VideoView
|
|
|
94
95
|
hasToSetupAudioContext.current = false;
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
function fullscreenListener() {
|
|
99
|
+
if (document.fullscreenElement === videoRef.current) {
|
|
100
|
+
props.onFullscreenEnter?.();
|
|
101
|
+
} else {
|
|
102
|
+
props.onFullscreenExit?.();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function setupFullscreenListener() {
|
|
107
|
+
fullscreenChangeListener.current = fullscreenListener;
|
|
108
|
+
videoRef.current?.addEventListener('fullscreenchange', fullscreenChangeListener.current);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function cleanupFullscreenListener() {
|
|
112
|
+
if (fullscreenChangeListener.current) {
|
|
113
|
+
videoRef.current?.removeEventListener('fullscreenchange', fullscreenChangeListener.current);
|
|
114
|
+
fullscreenChangeListener.current = null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
97
118
|
useEffect(() => {
|
|
98
119
|
if (videoRef.current) {
|
|
99
120
|
props.player?.mountVideoView(videoRef.current);
|
|
100
121
|
}
|
|
122
|
+
setupFullscreenListener();
|
|
101
123
|
attachAudioNodes();
|
|
102
124
|
|
|
103
125
|
return () => {
|
|
104
126
|
if (videoRef.current) {
|
|
105
127
|
props.player?.unmountVideoView(videoRef.current);
|
|
106
128
|
}
|
|
129
|
+
cleanupFullscreenListener();
|
|
107
130
|
detachAudioNodes();
|
|
108
131
|
};
|
|
109
132
|
}, [props.player]);
|