expo-video 1.1.10 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/android/build.gradle +4 -3
  3. package/android/src/main/AndroidManifest.xml +1 -0
  4. package/android/src/main/java/expo/modules/video/ContentFit.kt +2 -2
  5. package/android/src/main/java/expo/modules/video/DataSourceUtils.kt +44 -0
  6. package/android/src/main/java/expo/modules/video/VideoManager.kt +4 -17
  7. package/android/src/main/java/expo/modules/video/VideoModule.kt +20 -10
  8. package/android/src/main/java/expo/modules/video/VideoPlayer.kt +19 -16
  9. package/android/src/main/java/expo/modules/video/records/VideoSource.kt +15 -4
  10. package/build/VideoPlayer.types.d.ts +18 -0
  11. package/build/VideoPlayer.types.d.ts.map +1 -1
  12. package/build/VideoPlayer.types.js.map +1 -1
  13. package/build/VideoPlayer.web.d.ts +1 -0
  14. package/build/VideoPlayer.web.d.ts.map +1 -1
  15. package/build/VideoPlayer.web.js +4 -0
  16. package/build/VideoPlayer.web.js.map +1 -1
  17. package/build/VideoView.web.js +1 -1
  18. package/build/VideoView.web.js.map +1 -1
  19. package/expo-module.config.json +1 -1
  20. package/ios/ContentKeyDelegate.swift +15 -1
  21. package/ios/ExpoVideo.podspec +1 -1
  22. package/ios/NowPlayingManager.swift +36 -22
  23. package/ios/Records/DRMOptions.swift +3 -3
  24. package/ios/Records/VideoSource.swift +3 -0
  25. package/ios/VideoModule.swift +27 -6
  26. package/ios/VideoPlayer.swift +4 -4
  27. package/ios/VideoPlayerObserver.swift +46 -17
  28. package/ios/VideoView.swift +57 -3
  29. package/package.json +2 -2
  30. package/src/VideoPlayer.types.ts +20 -0
  31. package/src/VideoPlayer.web.tsx +5 -0
  32. package/src/VideoView.web.tsx +1 -1
package/CHANGELOG.md CHANGED
@@ -10,6 +10,33 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 1.2.1 — 2024-06-27
14
+
15
+ ### 🎉 New features
16
+
17
+ - [iOS] Support Apple TV. ([#29560](https://github.com/expo/expo/pull/29560) by [@douglowder](https://github.com/douglowder))
18
+
19
+ ## 1.2.0 — 2024-06-20
20
+
21
+ ### 🎉 New features
22
+
23
+ - Add `isLive` property on all platforms. ([#28903](https://github.com/expo/expo/pull/28903) by [@justjoostnl](https://github.com/justjoostnl))
24
+ - [iOS] Add base64 certificate support for FairPlay DRM. ([#28990](https://github.com/expo/expo/pull/28990) by [@behenate](https://github.com/behenate))
25
+ - [Android][iOS] Add support for request headers to in the video source. ([#29539](https://github.com/expo/expo/pull/29539) by [@behenate](https://github.com/behenate))
26
+
27
+ ### 🐛 Bug fixes
28
+
29
+ - [Android] Fix wrong content fit "fill" and "cover". ([#29364](https://github.com/expo/expo/pull/29364) by [@RRaideRR](https://github.com/RRaideRR))
30
+ - [iOS] Fix player status property always returning `undefined` on iOS. ([#29505](https://github.com/expo/expo/pull/29505) by [@behenate](https://github.com/behenate))
31
+ - [Android] Fix `VideoPlayer.replace` not working when the previous source caused an error. ([#29598](https://github.com/expo/expo/pull/29598) by [@lukmccall](https://github.com/lukmccall))
32
+ - [Web] Fix default behavior for `nativeControls` to match documentation. ([#29667](https://github.com/expo/expo/pull/29667) by [@nahn20](https://github.com/nahn20))
33
+ - [iOS] Fix crashes when creating new players. ([#29428](https://github.com/expo/expo/pull/29428) by [@behenate](https://github.com/behenate))
34
+ - Fix errors on setting a null video source. ([#29613](https://github.com/expo/expo/pull/29613) by [@behenate](https://github.com/behenate))
35
+
36
+ ### 💡 Others
37
+
38
+ - [iOS] Make appropriate references weak in `VideoPlayerObserver`. ([#29427](https://github.com/expo/expo/pull/29427) by [@behenate](https://github.com/behenate))
39
+
13
40
  ## 1.1.10 — 2024-05-29
14
41
 
15
42
  ### 💡 Others
@@ -1,7 +1,7 @@
1
1
  apply plugin: 'com.android.library'
2
2
 
3
3
  group = 'host.exp.exponent'
4
- version = '1.1.10'
4
+ version = '1.2.1'
5
5
 
6
6
  def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
7
7
  apply from: expoModulesCorePlugin
@@ -14,19 +14,20 @@ android {
14
14
  namespace "expo.modules.video"
15
15
  defaultConfig {
16
16
  versionCode 1
17
- versionName '1.1.10'
17
+ versionName '1.2.1'
18
18
  }
19
19
  }
20
20
 
21
21
  dependencies {
22
22
  implementation 'com.facebook.react:react-android'
23
23
 
24
- def androidxMedia3Version = "1.2.1"
24
+ def androidxMedia3Version = "1.3.1"
25
25
  implementation "androidx.media3:media3-session:${androidxMedia3Version}"
26
26
  implementation "androidx.media3:media3-exoplayer:${androidxMedia3Version}"
27
27
  implementation "androidx.media3:media3-exoplayer-dash:${androidxMedia3Version}"
28
28
  implementation "androidx.media3:media3-exoplayer-hls:${androidxMedia3Version}"
29
29
  implementation "androidx.media3:media3-ui:${androidxMedia3Version}"
30
+ implementation "androidx.media3:media3-datasource-okhttp:${androidxMedia3Version}"
30
31
 
31
32
  def fragment_version = "1.6.2"
32
33
  implementation "androidx.fragment:fragment:$fragment_version"
@@ -1,4 +1,5 @@
1
1
  <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-permission android:name="android.permission.INTERNET" />
2
3
  <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
3
4
  <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
4
5
 
@@ -12,8 +12,8 @@ enum class ContentFit(val value: String) : Enumerable {
12
12
  fun toResizeMode(): Int {
13
13
  return when (this) {
14
14
  CONTAIN -> AspectRatioFrameLayout.RESIZE_MODE_FIT
15
- FILL -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
16
- COVER -> AspectRatioFrameLayout.RESIZE_MODE_FILL
15
+ FILL -> AspectRatioFrameLayout.RESIZE_MODE_FILL
16
+ COVER -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
17
17
  }
18
18
  }
19
19
  }
@@ -0,0 +1,44 @@
1
+ package expo.modules.video
2
+
3
+ import android.content.Context
4
+ import android.content.pm.ApplicationInfo
5
+ import androidx.annotation.OptIn
6
+ import androidx.media3.common.util.UnstableApi
7
+ import androidx.media3.common.util.Util
8
+ import androidx.media3.datasource.okhttp.OkHttpDataSource
9
+ import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
10
+ import androidx.media3.exoplayer.source.MediaSource
11
+ import expo.modules.video.records.VideoSource
12
+ import okhttp3.OkHttpClient
13
+
14
+ @OptIn(UnstableApi::class)
15
+ fun buildDataSourceFactory(context: Context, videoSource: VideoSource): OkHttpDataSource.Factory {
16
+ val client = OkHttpClient.Builder().build()
17
+ val userAgent = Util.getUserAgent(context, getApplicationName(context))
18
+ return OkHttpDataSource.Factory(client).apply {
19
+ videoSource.headers?.let {
20
+ if (it.isNotEmpty()) {
21
+ setDefaultRequestProperties(it)
22
+ }
23
+ }
24
+ setUserAgent(userAgent)
25
+ }
26
+ }
27
+
28
+ fun buildMediaSourceFactory(context: Context, dataSourceFactory: OkHttpDataSource.Factory): MediaSource.Factory {
29
+ return DefaultMediaSourceFactory(context).setDataSourceFactory(dataSourceFactory)
30
+ }
31
+
32
+ @OptIn(UnstableApi::class)
33
+ fun buildMediaSourceWithHeaders(context: Context, videoSource: VideoSource): MediaSource {
34
+ val dataSourceFactory = buildDataSourceFactory(context, videoSource)
35
+ val mediaSourceFactory = buildMediaSourceFactory(context, dataSourceFactory)
36
+ val mediaItem = videoSource.toMediaItem()
37
+ return mediaSourceFactory.createMediaSource(mediaItem)
38
+ }
39
+
40
+ private fun getApplicationName(context: Context): String {
41
+ val applicationInfo: ApplicationInfo = context.applicationInfo
42
+ val stringId = applicationInfo.labelRes
43
+ return if (stringId == 0) applicationInfo.nonLocalizedLabel.toString() else context.getString(stringId)
44
+ }
@@ -1,10 +1,7 @@
1
1
  package expo.modules.video
2
2
 
3
3
  import androidx.annotation.OptIn
4
- import androidx.media3.common.MediaItem
5
4
  import androidx.media3.common.util.UnstableApi
6
- import expo.modules.video.records.VideoSource
7
- import java.lang.ref.WeakReference
8
5
 
9
6
  // Helper class used to keep track of all existing VideoViews and VideoPlayers
10
7
  @OptIn(UnstableApi::class)
@@ -17,9 +14,6 @@ object VideoManager {
17
14
  // Keeps track of all existing VideoPlayers, and whether they are attached to a VideoView
18
15
  private var videoPlayersToVideoViews = mutableMapOf<VideoPlayer, MutableList<VideoView>>()
19
16
 
20
- // Keeps track of all existing MediaItems and their corresponding VideoSources. Used for recognizing source of MediaItems.
21
- private var mediaItemsToVideoSources = mutableMapOf<String, WeakReference<VideoSource>>()
22
-
23
17
  fun registerVideoView(videoView: VideoView) {
24
18
  videoViews[videoView.id] = videoView
25
19
  }
@@ -40,17 +34,6 @@ object VideoManager {
40
34
  videoPlayersToVideoViews.remove(videoPlayer)
41
35
  }
42
36
 
43
- fun registerVideoSourceToMediaItem(mediaItem: MediaItem, videoSource: VideoSource) {
44
- mediaItemsToVideoSources[mediaItem.mediaId] = WeakReference(videoSource)
45
- }
46
-
47
- fun getVideoSourceFromMediaItem(mediaItem: MediaItem?): VideoSource? {
48
- if (mediaItem == null) {
49
- return null
50
- }
51
- return mediaItemsToVideoSources[mediaItem.mediaId]?.get()
52
- }
53
-
54
37
  fun onVideoPlayerAttachedToView(videoPlayer: VideoPlayer, videoView: VideoView) {
55
38
  if (videoPlayersToVideoViews[videoPlayer]?.contains(videoView) == true) {
56
39
  return
@@ -73,6 +56,10 @@ object VideoManager {
73
56
  }
74
57
  }
75
58
 
59
+ fun isVideoPlayerAttachedToView(videoPlayer: VideoPlayer): Boolean {
60
+ return videoPlayersToVideoViews[videoPlayer]?.isNotEmpty() ?: false
61
+ }
62
+
76
63
  fun onAppForegrounded() = Unit
77
64
 
78
65
  fun onAppBackgrounded() {
@@ -3,6 +3,7 @@
3
3
  package expo.modules.video
4
4
 
5
5
  import android.app.Activity
6
+ import android.content.Context
6
7
  import androidx.media3.common.PlaybackParameters
7
8
  import androidx.media3.common.Player.REPEAT_MODE_OFF
8
9
  import androidx.media3.common.Player.REPEAT_MODE_ONE
@@ -24,6 +25,8 @@ import kotlinx.coroutines.runBlocking
24
25
  class VideoModule : Module() {
25
26
  private val activity: Activity
26
27
  get() = appContext.activityProvider?.currentActivity ?: throw Exceptions.MissingActivity()
28
+ private val reactContext: Context
29
+ get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
27
30
 
28
31
  override fun definition() = ModuleDefinition {
29
32
  Name("ExpoVideo")
@@ -138,7 +141,7 @@ class VideoModule : Module() {
138
141
  }
139
142
 
140
143
  Class(VideoPlayer::class) {
141
- Constructor { source: VideoSource ->
144
+ Constructor { source: VideoSource? ->
142
145
  VideoPlayer(activity.applicationContext, appContext, source)
143
146
  }
144
147
 
@@ -199,6 +202,11 @@ class VideoModule : Module() {
199
202
  }
200
203
  }
201
204
 
205
+ Property("isLive")
206
+ .get { ref: VideoPlayer ->
207
+ ref.isLive
208
+ }
209
+
202
210
  Property("preservesPitch")
203
211
  .get { ref: VideoPlayer ->
204
212
  ref.preservesPitch
@@ -258,18 +266,20 @@ class VideoModule : Module() {
258
266
  }
259
267
  }
260
268
 
261
- Function("replace") { ref: VideoPlayer, source: Either<String, VideoSource> ->
262
- val videoSource = if (source.`is`(VideoSource::class)) {
263
- source.get(VideoSource::class)
264
- } else {
265
- VideoSource(source.get(String::class))
269
+ Function("replace") { ref: VideoPlayer, source: Either<String, VideoSource>? ->
270
+ val videoSource = source?.let {
271
+ if (it.`is`(VideoSource::class)) {
272
+ it.get(VideoSource::class)
273
+ } else {
274
+ VideoSource(it.get(String::class))
275
+ }
266
276
  }
267
- val mediaItem = videoSource.toMediaItem()
268
- VideoManager.registerVideoSourceToMediaItem(mediaItem, videoSource)
269
277
 
270
278
  appContext.mainQueue.launch {
271
- ref.videoSource = videoSource
272
- ref.player.setMediaItem(mediaItem)
279
+ ref.uncommittedSource = videoSource
280
+ if (VideoManager.isVideoPlayerAttachedToView(ref)) {
281
+ ref.prepare()
282
+ }
273
283
  }
274
284
  }
275
285
 
@@ -31,7 +31,7 @@ import java.lang.ref.WeakReference
31
31
 
32
32
  // https://developer.android.com/guide/topics/media/media3/getting-started/migration-guide#improvements_in_media3
33
33
  @UnstableApi
34
- class VideoPlayer(context: Context, appContext: AppContext, source: VideoSource?) : AutoCloseable, SharedObject(appContext) {
34
+ class VideoPlayer(val context: Context, appContext: AppContext, source: VideoSource?) : AutoCloseable, SharedObject(appContext) {
35
35
  // This improves the performance of playing DRM-protected content
36
36
  private var renderersFactory = DefaultRenderersFactory(context)
37
37
  .forceEnableMediaCodecAsynchronousQueueing()
@@ -50,14 +50,13 @@ class VideoPlayer(context: Context, appContext: AppContext, source: VideoSource?
50
50
  field = value
51
51
  }
52
52
 
53
- // This is used only for sending events and keeping the reference to the video source for the
54
- // VideoManager, which holds weak references. Changing this will not affect the player.
55
- var videoSource: VideoSource? = source
56
- set(videoSource) {
57
- if (field != videoSource) {
58
- sendEventOnJSThread("sourceChange", videoSource, field)
53
+ var uncommittedSource: VideoSource? = source
54
+ private var lastLoadedSource: VideoSource? = null
55
+ set(value) {
56
+ if (field != value && value != null) {
57
+ sendEventOnJSThread("sourceChange", value, field)
59
58
  }
60
- field = videoSource
59
+ field = value
61
60
  }
62
61
 
63
62
  // Volume of the player if there was no mute applied.
@@ -76,6 +75,7 @@ class VideoPlayer(context: Context, appContext: AppContext, source: VideoSource?
76
75
  playbackServiceBinder?.service?.setShowNotification(value, this.player)
77
76
  }
78
77
  var duration = 0f
78
+ var isLive = false
79
79
 
80
80
  private var serviceConnection: ServiceConnection
81
81
  internal var playbackServiceBinder: PlaybackServiceBinder? = null
@@ -122,9 +122,8 @@ class VideoPlayer(context: Context, appContext: AppContext, source: VideoSource?
122
122
  }
123
123
 
124
124
  override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
125
- val newVideoSource = VideoManager.getVideoSourceFromMediaItem(mediaItem)
126
- this@VideoPlayer.videoSource = newVideoSource
127
125
  this@VideoPlayer.duration = 0f
126
+ this@VideoPlayer.isLive = false
128
127
  if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT) {
129
128
  sendEventOnJSThread("playToEnd")
130
129
  }
@@ -137,6 +136,7 @@ class VideoPlayer(context: Context, appContext: AppContext, source: VideoSource?
137
136
  }
138
137
  if (playbackState == Player.STATE_READY) {
139
138
  this@VideoPlayer.duration = this@VideoPlayer.player.duration / 1000f
139
+ this@VideoPlayer.isLive = this@VideoPlayer.player.isCurrentMediaItemLive
140
140
  }
141
141
  setStatus(playerStateToPlayerStatus(playbackState), null)
142
142
  super.onPlaybackStateChanged(playbackState)
@@ -156,6 +156,7 @@ class VideoPlayer(context: Context, appContext: AppContext, source: VideoSource?
156
156
  error?.let {
157
157
  setStatus(ERROR, error)
158
158
  this@VideoPlayer.duration = 0f
159
+ this@VideoPlayer.isLive = false
159
160
  } ?: run {
160
161
  setStatus(playerStateToPlayerStatus(player.playbackState), null)
161
162
  }
@@ -218,7 +219,8 @@ class VideoPlayer(context: Context, appContext: AppContext, source: VideoSource?
218
219
  player.removeListener(playerListener)
219
220
  player.release()
220
221
  }
221
- videoSource = null
222
+ uncommittedSource = null
223
+ lastLoadedSource = null
222
224
  }
223
225
 
224
226
  override fun deallocate() {
@@ -233,13 +235,14 @@ class VideoPlayer(context: Context, appContext: AppContext, source: VideoSource?
233
235
  }
234
236
 
235
237
  fun prepare() {
236
- videoSource?.let { videoSource ->
237
- val mediaItem = videoSource.toMediaItem()
238
- VideoManager.registerVideoSourceToMediaItem(mediaItem, videoSource)
239
- player.setMediaItem(mediaItem)
238
+ uncommittedSource?.let { videoSource ->
239
+ val mediaSource = videoSource.toMediaSource(context)
240
+ player.setMediaSource(mediaSource)
240
241
  player.prepare()
242
+ lastLoadedSource = videoSource
243
+ uncommittedSource = null
241
244
  } ?: run {
242
- player.removeMediaItem(0)
245
+ player.clearMediaItems()
243
246
  player.prepare()
244
247
  }
245
248
  }
@@ -1,28 +1,39 @@
1
1
  package expo.modules.video.records
2
-
2
+ import android.content.Context
3
+ import androidx.annotation.OptIn
3
4
  import androidx.media3.common.MediaItem
4
5
  import androidx.media3.common.MediaMetadata
6
+ import androidx.media3.common.util.UnstableApi
7
+ import androidx.media3.exoplayer.source.MediaSource
5
8
  import expo.modules.kotlin.records.Field
6
9
  import expo.modules.kotlin.records.Record
7
10
  import expo.modules.video.UnsupportedDRMTypeException
11
+ import expo.modules.video.buildMediaSourceWithHeaders
8
12
  import java.io.Serializable
9
13
 
14
+ @OptIn(UnstableApi::class)
10
15
  class VideoSource(
11
16
  @Field var uri: String? = null,
12
17
  @Field var drm: DRMOptions? = null,
13
- @Field var metadata: VideoMetadata? = null
18
+ @Field var metadata: VideoMetadata? = null,
19
+ @Field var headers: Map<String, String>? = null
14
20
  ) : Record, Serializable {
15
21
  private fun toMediaId(): String {
16
22
  return "uri:${this.uri}" +
23
+ "Headers: ${this.headers}" +
17
24
  "DrmType:${this.drm?.type}" +
18
25
  "DrmLicenseServer:${this.drm?.licenseServer}" +
19
26
  "DrmMultiKey:${this.drm?.multiKey}" +
20
- "DRMHeadersKeys:${this.drm?.headers?.keys?.joinToString {it}}}" +
21
- "DRMHeadersValues:${this.drm?.headers?.values?.joinToString {it}}}" +
27
+ "DRMHeadersKeys:${this.drm?.headers?.keys?.joinToString { it }}}" +
28
+ "DRMHeadersValues:${this.drm?.headers?.values?.joinToString { it }}}" +
22
29
  "NotificationDataTitle:${this.metadata?.title}" +
23
30
  "NotificationDataSecondaryText:${this.metadata?.artist}"
24
31
  }
25
32
 
33
+ fun toMediaSource(context: Context): MediaSource {
34
+ return buildMediaSourceWithHeaders(context, this)
35
+ }
36
+
26
37
  fun toMediaItem() = MediaItem
27
38
  .Builder()
28
39
  .apply {
@@ -53,6 +53,11 @@ export declare class VideoPlayer extends SharedObject<VideoPlayerEvents> {
53
53
  * @default 1.0
54
54
  */
55
55
  playbackRate: number;
56
+ /**
57
+ * Boolean value indicating whether the player is currently playing a live stream.
58
+ * > This property is get-only
59
+ */
60
+ isLive: boolean;
56
61
  /**
57
62
  * Indicates the current status of the player.
58
63
  * > This property is get-only
@@ -146,6 +151,13 @@ export type VideoSource = string | {
146
151
  * When undefined the player will display information contained in the video metadata.
147
152
  */
148
153
  metadata?: VideoMetadata;
154
+ /**
155
+ * Specifies headers sent with the video request.
156
+ * > For DRM license headers use the `headers` field of [`DRMOptions`](#drmoptions).
157
+ * @platform android
158
+ * @platform ios
159
+ */
160
+ headers?: Record<string, string>;
149
161
  } | null;
150
162
  /**
151
163
  * Contains information about any errors that the player encountered during the playback
@@ -210,5 +222,11 @@ export type DRMOptions = {
210
222
  * @platform ios
211
223
  */
212
224
  certificateUrl?: string;
225
+ /**
226
+ * Specifies the base64 encoded certificate data for the FairPlay DRM.
227
+ * When this property is set, the `certificateUrl` property is ignored.
228
+ * @platform ios
229
+ */
230
+ base64CertificateData?: string;
213
231
  };
214
232
  //# sourceMappingURL=VideoPlayer.types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"VideoPlayer.types.d.ts","sourceRoot":"","sources":["../src/VideoPlayer.types.ts"],"names":[],"mappings":"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"}
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 * 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: string;\n /**\n * Specifies the DRM options which will be used by the player while loading the video.\n */\n drm?: DRMOptions;\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 | 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"]}
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: string;\n /**\n * Specifies the DRM options which will be used by the player while loading the video.\n */\n drm?: DRMOptions;\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 * 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"]}
@@ -19,6 +19,7 @@ export default class VideoPlayerWeb extends globalThis.expo.SharedObject<VideoPl
19
19
  get muted(): boolean;
20
20
  set playbackRate(value: number);
21
21
  get playbackRate(): number;
22
+ get isLive(): boolean;
22
23
  set volume(value: number);
23
24
  get volume(): number;
24
25
  set loop(value: boolean);
@@ -1 +1 @@
1
- {"version":3,"file":"VideoPlayer.web.d.ts","sourceRoot":"","sources":["../src/VideoPlayer.web.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,WAAW,EACX,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,EACZ,MAAM,qBAAqB,CAAC;AAE7B,wBAAgB,cAAc,CAC5B,MAAM,EAAE,WAAW,EACnB,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,GACpC,WAAW,CAQb;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI,CAK/D;AAED,MAAM,CAAC,OAAO,OAAO,cACnB,SAAQ,UAAU,CAAC,IAAI,CAAC,YAAY,CAAC,iBAAiB,CACtD,YAAW,WAAW;gBAEV,MAAM,EAAE,WAAW;IAK/B,GAAG,EAAE,WAAW,CAAQ;IACxB,cAAc,EAAE,GAAG,CAAC,gBAAgB,CAAC,CAAa;IAClD,WAAW,EAAE,GAAG,CAAC,2BAA2B,CAAC,CAAa;IAC1D,OAAO,EAAE,OAAO,CAAS;IACzB,MAAM,EAAE,OAAO,CAAS;IACxB,OAAO,EAAE,MAAM,CAAK;IACpB,KAAK,EAAE,OAAO,CAAS;IACvB,aAAa,EAAE,MAAM,CAAO;IAC5B,eAAe,EAAE,OAAO,CAAQ;IAChC,OAAO,EAAE,iBAAiB,CAAU;IACpC,uBAAuB,EAAE,OAAO,CAAS;IACzC,0BAA0B,EAAE,OAAO,CAAS;IAE5C,IAAI,KAAK,CAAC,KAAK,EAAE,OAAO,EAKvB;IAED,IAAI,KAAK,IAAI,OAAO,CAEnB;IAED,IAAI,YAAY,CAAC,KAAK,EAAE,MAAM,EAI7B;IAED,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,IAAI,MAAM,CAAC,KAAK,EAAE,MAAM,EAKvB;IAED,IAAI,MAAM,IAAI,MAAM,CAKnB;IAED,IAAI,IAAI,CAAC,KAAK,EAAE,OAAO,EAKtB;IAED,IAAI,IAAI,IAAI,OAAO,CAElB;IAED,IAAI,WAAW,IAAI,MAAM,CAGxB;IAED,IAAI,WAAW,CAAC,KAAK,EAAE,MAAM,EAI5B;IAED,IAAI,QAAQ,IAAI,MAAM,CAGrB;IAED,IAAI,cAAc,IAAI,OAAO,CAE5B;IAED,IAAI,cAAc,CAAC,KAAK,EAAE,OAAO,EAKhC;IAED,IAAI,MAAM,IAAI,iBAAiB,CAE9B;IAED,cAAc,CAAC,KAAK,EAAE,gBAAgB;IAMtC,gBAAgB,CAAC,KAAK,EAAE,gBAAgB;IAIxC,cAAc,CACZ,YAAY,EAAE,YAAY,EAC1B,YAAY,EAAE,QAAQ,EACtB,eAAe,EAAE,2BAA2B,GAC3C,IAAI;IAYP,gBAAgB,CACd,KAAK,EAAE,gBAAgB,EACvB,YAAY,EAAE,YAAY,EAC1B,eAAe,EAAE,2BAA2B;IAe9C,IAAI,IAAI,IAAI;IAOZ,KAAK,IAAI,IAAI;IAOb,OAAO,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAelC,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAM7B,MAAM,IAAI,IAAI;IAQd,0BAA0B,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAezD,aAAa,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;CA0D7C"}
1
+ {"version":3,"file":"VideoPlayer.web.d.ts","sourceRoot":"","sources":["../src/VideoPlayer.web.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,WAAW,EACX,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,EACZ,MAAM,qBAAqB,CAAC;AAE7B,wBAAgB,cAAc,CAC5B,MAAM,EAAE,WAAW,EACnB,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,GACpC,WAAW,CAQb;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI,CAK/D;AAED,MAAM,CAAC,OAAO,OAAO,cACnB,SAAQ,UAAU,CAAC,IAAI,CAAC,YAAY,CAAC,iBAAiB,CACtD,YAAW,WAAW;gBAEV,MAAM,EAAE,WAAW;IAK/B,GAAG,EAAE,WAAW,CAAQ;IACxB,cAAc,EAAE,GAAG,CAAC,gBAAgB,CAAC,CAAa;IAClD,WAAW,EAAE,GAAG,CAAC,2BAA2B,CAAC,CAAa;IAC1D,OAAO,EAAE,OAAO,CAAS;IACzB,MAAM,EAAE,OAAO,CAAS;IACxB,OAAO,EAAE,MAAM,CAAK;IACpB,KAAK,EAAE,OAAO,CAAS;IACvB,aAAa,EAAE,MAAM,CAAO;IAC5B,eAAe,EAAE,OAAO,CAAQ;IAChC,OAAO,EAAE,iBAAiB,CAAU;IACpC,uBAAuB,EAAE,OAAO,CAAS;IACzC,0BAA0B,EAAE,OAAO,CAAS;IAE5C,IAAI,KAAK,CAAC,KAAK,EAAE,OAAO,EAKvB;IAED,IAAI,KAAK,IAAI,OAAO,CAEnB;IAED,IAAI,YAAY,CAAC,KAAK,EAAE,MAAM,EAI7B;IAED,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,IAAI,MAAM,CAAC,KAAK,EAAE,MAAM,EAKvB;IAED,IAAI,MAAM,IAAI,MAAM,CAKnB;IAED,IAAI,IAAI,CAAC,KAAK,EAAE,OAAO,EAKtB;IAED,IAAI,IAAI,IAAI,OAAO,CAElB;IAED,IAAI,WAAW,IAAI,MAAM,CAGxB;IAED,IAAI,WAAW,CAAC,KAAK,EAAE,MAAM,EAI5B;IAED,IAAI,QAAQ,IAAI,MAAM,CAGrB;IAED,IAAI,cAAc,IAAI,OAAO,CAE5B;IAED,IAAI,cAAc,CAAC,KAAK,EAAE,OAAO,EAKhC;IAED,IAAI,MAAM,IAAI,iBAAiB,CAE9B;IAED,cAAc,CAAC,KAAK,EAAE,gBAAgB;IAMtC,gBAAgB,CAAC,KAAK,EAAE,gBAAgB;IAIxC,cAAc,CACZ,YAAY,EAAE,YAAY,EAC1B,YAAY,EAAE,QAAQ,EACtB,eAAe,EAAE,2BAA2B,GAC3C,IAAI;IAYP,gBAAgB,CACd,KAAK,EAAE,gBAAgB,EACvB,YAAY,EAAE,YAAY,EAC1B,eAAe,EAAE,2BAA2B;IAe9C,IAAI,IAAI,IAAI;IAOZ,KAAK,IAAI,IAAI;IAOb,OAAO,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAgBlC,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAM7B,MAAM,IAAI,IAAI;IAQd,0BAA0B,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAezD,aAAa,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;CA0D7C"}
@@ -47,6 +47,9 @@ export default class VideoPlayerWeb extends globalThis.expo.SharedObject {
47
47
  get playbackRate() {
48
48
  return this._playbackRate;
49
49
  }
50
+ get isLive() {
51
+ return [...this._mountedVideos][0].duration === Infinity;
52
+ }
50
53
  set volume(value) {
51
54
  this._mountedVideos.forEach((video) => {
52
55
  video.volume = value;
@@ -148,6 +151,7 @@ export default class VideoPlayerWeb extends globalThis.expo.SharedObject {
148
151
  }
149
152
  else {
150
153
  video.removeAttribute('src');
154
+ video.load();
151
155
  }
152
156
  });
153
157
  this.playing = true;
@@ -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 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 _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 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 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 this._mountedVideos.forEach((video) => {\n this._volume = video.volume;\n });\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 mountVideoView(video: HTMLVideoElement) {\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 this.playing = true;\n }\n\n pause(): void {\n this._mountedVideos.forEach((video) => {\n video.pause();\n });\n this.playing = false;\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 }\n });\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 _addListeners(video: HTMLVideoElement): void {\n video.onplay = () => {\n this.playing = true;\n this._mountedVideos.forEach((mountedVideo) => {\n mountedVideo.play();\n });\n };\n\n video.onpause = () => {\n this.playing = false;\n this._mountedVideos.forEach((mountedVideo) => {\n mountedVideo.pause();\n });\n };\n\n video.onvolumechange = () => {\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._mountedVideos.forEach((mountedVideo) => {\n if (mountedVideo === video || mountedVideo.playbackRate === video.playbackRate) return;\n this._playbackRate = video.playbackRate;\n mountedVideo.playbackRate = video.playbackRate;\n });\n };\n\n video.onerror = () => {\n this._status = 'error';\n };\n\n video.onloadeddata = () => {\n this._status = 'readyToPlay';\n\n if (this.playing && video.paused) {\n video.play();\n }\n };\n\n video.onwaiting = () => {\n this._status = 'loading';\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';\n\nimport type {\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 _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 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 this._mountedVideos.forEach((video) => {\n this._volume = video.volume;\n });\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 mountVideoView(video: HTMLVideoElement) {\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 this.playing = true;\n }\n\n pause(): void {\n this._mountedVideos.forEach((video) => {\n video.pause();\n });\n this.playing = false;\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 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 _addListeners(video: HTMLVideoElement): void {\n video.onplay = () => {\n this.playing = true;\n this._mountedVideos.forEach((mountedVideo) => {\n mountedVideo.play();\n });\n };\n\n video.onpause = () => {\n this.playing = false;\n this._mountedVideos.forEach((mountedVideo) => {\n mountedVideo.pause();\n });\n };\n\n video.onvolumechange = () => {\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._mountedVideos.forEach((mountedVideo) => {\n if (mountedVideo === video || mountedVideo.playbackRate === video.playbackRate) return;\n this._playbackRate = video.playbackRate;\n mountedVideo.playbackRate = video.playbackRate;\n });\n };\n\n video.onerror = () => {\n this._status = 'error';\n };\n\n video.onloadeddata = () => {\n this._status = 'readyToPlay';\n\n if (this.playing && video.paused) {\n video.play();\n }\n };\n\n video.onwaiting = () => {\n this._status = 'loading';\n };\n }\n}\n"]}
@@ -61,7 +61,7 @@ export const VideoView = forwardRef((props, ref) => {
61
61
  }
62
62
  };
63
63
  }, [props.player]);
64
- return (<video controls={props.nativeControls} controlsList={props.allowsFullscreen ? undefined : 'nofullscreen'} crossOrigin="anonymous" style={{
64
+ return (<video controls={props.nativeControls ?? true} controlsList={props.allowsFullscreen ? undefined : 'nofullscreen'} crossOrigin="anonymous" style={{
65
65
  ...mapStyles(props.style),
66
66
  objectFit: props.contentFit,
67
67
  }} ref={(newRef) => {
@@ -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\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 useEffect(() => {\n const audioContext = audioContextRef.current;\n const zeroGainNode = zeroGainNodeRef.current;\n const mediaNode = mediaNodeRef.current;\n\n if (videoRef.current) {\n props.player?.mountVideoView(videoRef.current);\n }\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 return () => {\n if (videoRef.current) {\n props.player?.unmountVideoView(videoRef.current);\n }\n if (videoRef.current && audioContext && mediaNode) {\n props.player?.unmountAudioNode(videoRef.current, audioContext, mediaNode);\n }\n };\n }, [props.player]);\n\n return (\n <video\n controls={props.nativeControls}\n controlsList={props.allowsFullscreen ? undefined : 'nofullscreen'}\n crossOrigin=\"anonymous\"\n style={{\n ...mapStyles(props.style),\n objectFit: props.contentFit,\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 const audioContext = createAudioContext();\n audioContextRef.current = audioContext;\n zeroGainNodeRef.current = createZeroGainNode(audioContextRef.current);\n mediaNodeRef.current = audioContext\n ? audioContext.createMediaElementSource(newRef)\n : null;\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\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 useEffect(() => {\n const audioContext = audioContextRef.current;\n const zeroGainNode = zeroGainNodeRef.current;\n const mediaNode = mediaNodeRef.current;\n\n if (videoRef.current) {\n props.player?.mountVideoView(videoRef.current);\n }\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 return () => {\n if (videoRef.current) {\n props.player?.unmountVideoView(videoRef.current);\n }\n if (videoRef.current && audioContext && mediaNode) {\n props.player?.unmountAudioNode(videoRef.current, audioContext, mediaNode);\n }\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 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 const audioContext = createAudioContext();\n audioContextRef.current = audioContext;\n zeroGainNodeRef.current = createZeroGainNode(audioContextRef.current);\n mediaNodeRef.current = audioContext\n ? audioContext.createMediaElementSource(newRef)\n : null;\n }\n }}\n src={getSourceUri(props.player?.src) ?? ''}\n />\n );\n});\n\nexport default VideoView;\n"]}
@@ -1,5 +1,5 @@
1
1
  {
2
- "platforms": ["ios", "android"],
2
+ "platforms": ["apple", "android"],
3
3
  "ios": {
4
4
  "modules": ["VideoModule"]
5
5
  },
@@ -95,10 +95,17 @@ internal class ContentKeyDelegate: NSObject, AVContentKeySessionDelegate {
95
95
  }
96
96
 
97
97
  private func requestApplicationCertificate(keyRequest: AVContentKeyRequest) throws -> Data {
98
+ if let certificateData = videoSource?.drm?.base64CertificateData {
99
+ return try requestCertificateFrom(base64String: certificateData)
100
+ }
101
+
98
102
  guard let url = videoSource?.drm?.certificateUrl else {
99
- throw DRMLoadException("The certificate uri is null")
103
+ throw DRMLoadException("The certificate uri and data are null")
100
104
  }
105
+ return try requestCertificateFrom(url: url)
106
+ }
101
107
 
108
+ private func requestCertificateFrom(url: URL) throws -> Data {
102
109
  let urlRequest = URLRequest(url: url)
103
110
  let (data, response, error) = URLSession.shared.synchronousDataTask(with: urlRequest)
104
111
 
@@ -123,6 +130,13 @@ internal class ContentKeyDelegate: NSObject, AVContentKeySessionDelegate {
123
130
  return data
124
131
  }
125
132
 
133
+ private func requestCertificateFrom(base64String: String) throws -> Data {
134
+ guard let certificateData = Data(base64Encoded: base64String, options: .ignoreUnknownCharacters) else {
135
+ throw DRMLoadException("Failed to load the application certificate from the provided base64 string")
136
+ }
137
+ return certificateData
138
+ }
139
+
126
140
  private func requestContentKeyFromKeySecurityModule(spcData: Data, assetID: String, keyRequest: AVContentKeyRequest) throws -> Data {
127
141
  let ckcData: Data? = nil
128
142
 
@@ -10,7 +10,7 @@ Pod::Spec.new do |s|
10
10
  s.license = package['license']
11
11
  s.author = package['author']
12
12
  s.homepage = package['homepage']
13
- s.platform = :ios, '13.4'
13
+ s.platforms = { :ios => '13.4', :tvos => '13.4' }
14
14
  s.swift_version = '5.4'
15
15
  s.source = { git: 'https://github.com/expo/expo.git' }
16
16
  s.static_framework = true
@@ -13,6 +13,7 @@ class NowPlayingManager: VideoPlayerObserverDelegate {
13
13
  static var shared = NowPlayingManager()
14
14
 
15
15
  private let skipTimeInterval = 10.0
16
+ private let fetchMetadataQueue = DispatchQueue(label: "com.expo.fetchMetadataQueue")
16
17
  private var timeObserver: Any?
17
18
  private weak var mostRecentInteractionPlayer: AVPlayer?
18
19
  private var players = NSHashTable<VideoPlayer>.weakObjects()
@@ -138,40 +139,53 @@ class NowPlayingManager: VideoPlayerObserverDelegate {
138
139
  }
139
140
 
140
141
  private func updateNowPlayingInfo() {
141
- guard let player = mostRecentInteractionPlayer, let currentItem = mostRecentInteractionPlayer?.currentItem else {
142
+ guard let player = mostRecentInteractionPlayer, let currentItem = player.currentItem else {
142
143
  return
143
144
  }
144
145
  let videoPlayerItem = currentItem as? VideoPlayerItem
145
146
 
146
- // Metadata explicily specified by the user
147
+ // Metadata explicitly specified by the user
147
148
  let userMetadata = videoPlayerItem?.videoSource.metadata
148
149
 
149
- // Metadata fetched with the video
150
- let assetMetadata = currentItem.asset.commonMetadata
150
+ Task {
151
+ let assetMetadata = await try loadMetadata(for: currentItem)
151
152
 
152
- let title = assetMetadata.first(where: {
153
- $0.commonKey == .commonKeyTitle
154
- })
153
+ let title = assetMetadata.first(where: {
154
+ $0.commonKey == .commonKeyTitle
155
+ })
155
156
 
156
- let artist = assetMetadata.first(where: {
157
- $0.commonKey == .commonKeyArtist
158
- })
157
+ let artist = assetMetadata.first(where: {
158
+ $0.commonKey == .commonKeyArtist
159
+ })
159
160
 
160
- let artwork = assetMetadata.first(where: {
161
- $0.commonKey == .commonKeyArtwork
162
- })
161
+ let artwork = assetMetadata.first(where: {
162
+ $0.commonKey == .commonKeyArtwork
163
+ })
163
164
 
164
- var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:]
165
+ var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:]
165
166
 
166
- nowPlayingInfo[MPMediaItemPropertyTitle] = userMetadata?.title ?? title
167
- nowPlayingInfo[MPMediaItemPropertyArtist] = userMetadata?.artist ?? artist
168
- nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = currentItem.duration.seconds
169
- nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentItem.currentTime().seconds
170
- nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = player.rate
171
- nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaType.video.rawValue // Using MPNowPlayingInfoMediaType.video causes a crash
172
- nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
167
+ nowPlayingInfo[MPMediaItemPropertyTitle] = userMetadata?.title ?? title
168
+ nowPlayingInfo[MPMediaItemPropertyArtist] = userMetadata?.artist ?? artist
169
+ nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = currentItem.duration.seconds
170
+ nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentItem.currentTime().seconds
171
+ nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = await player.rate
172
+ nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaType.video.rawValue // Using MPNowPlayingInfoMediaType.video causes a crash
173
+ nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
173
174
 
174
- MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
175
+ MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
176
+ }
177
+ }
178
+
179
+ private func loadMetadata(for mediaItem: AVPlayerItem) async throws -> [AVMetadataItem] {
180
+ if #available(iOS 15.0, *) {
181
+ return try await mediaItem.asset.loadMetadata(for: .iTunesMetadata)
182
+ }
183
+
184
+ return await withCheckedContinuation { continuation in
185
+ fetchMetadataQueue.async {
186
+ continuation.resume(returning: mediaItem.asset.metadata)
187
+ }
188
+ }
175
189
  }
176
190
 
177
191
  // Updates nowPlaying information that changes dynamically during playback e.g. progress
@@ -13,12 +13,12 @@ internal struct DRMOptions: Record {
13
13
  @Field
14
14
  var headers: [String: Any]?
15
15
 
16
- @Field
17
- var base64Certificate: Bool = false
18
-
19
16
  @Field
20
17
  var contentId: String?
21
18
 
22
19
  @Field
23
20
  var certificateUrl: URL?
21
+
22
+ @Field
23
+ var base64CertificateData: String?
24
24
  }
@@ -12,5 +12,8 @@ internal struct VideoSource: Record {
12
12
 
13
13
  @Field
14
14
  var metadata: VideoMetadata? = nil
15
+
16
+ @Field
17
+ var headers: [String: String]? = nil
15
18
  }
16
19
  // swiftlint:enable redundant_optional_initialization
@@ -6,8 +6,11 @@ public final class VideoModule: Module {
6
6
  public func definition() -> ModuleDefinition {
7
7
  Name("ExpoVideo")
8
8
 
9
- Function("isPictureInPictureSupported") {
10
- return AVPictureInPictureController.isPictureInPictureSupported()
9
+ Function("isPictureInPictureSupported") { () -> Bool in
10
+ if #available(iOS 13.4, tvOS 14.0, *) {
11
+ return AVPictureInPictureController.isPictureInPictureSupported()
12
+ }
13
+ return false
11
14
  }
12
15
 
13
16
  View(VideoView.self) {
@@ -22,6 +25,10 @@ public final class VideoModule: Module {
22
25
 
23
26
  Prop("nativeControls") { (view, nativeControls: Bool?) in
24
27
  view.playerViewController.showsPlaybackControls = nativeControls ?? true
28
+ #if os(tvOS)
29
+ view.playerViewController.isSkipForwardEnabled = nativeControls ?? true
30
+ view.playerViewController.isSkipBackwardEnabled = nativeControls ?? true
31
+ #endif
25
32
  }
26
33
 
27
34
  Prop("contentFit") { (view, contentFit: VideoContentFit?) in
@@ -40,11 +47,15 @@ public final class VideoModule: Module {
40
47
  }
41
48
 
42
49
  Prop("allowsFullscreen") { (view, allowsFullscreen: Bool?) in
50
+ #if !os(tvOS)
43
51
  view.playerViewController.setValue(allowsFullscreen ?? true, forKey: "allowsEnteringFullScreen")
52
+ #endif
44
53
  }
45
54
 
46
55
  Prop("showsTimecodes") { (view, showsTimecodes: Bool?) in
56
+ #if !os(tvOS)
47
57
  view.playerViewController.showsTimecodes = showsTimecodes ?? true
58
+ #endif
48
59
  }
49
60
 
50
61
  Prop("requiresLinearPlayback") { (view, requiresLinearPlayback: Bool?) in
@@ -56,7 +67,9 @@ public final class VideoModule: Module {
56
67
  }
57
68
 
58
69
  Prop("startsPictureInPictureAutomatically") { (view, startsPictureInPictureAutomatically: Bool?) in
70
+ #if !os(tvOS)
59
71
  view.startPictureInPictureAutomatically = startsPictureInPictureAutomatically ?? false
72
+ #endif
60
73
  }
61
74
 
62
75
  AsyncFunction("enterFullscreen") { view in
@@ -77,7 +90,7 @@ public final class VideoModule: Module {
77
90
  }
78
91
 
79
92
  Class(VideoPlayer.self) {
80
- Constructor { (source: VideoSource) -> VideoPlayer in
93
+ Constructor { (source: VideoSource?) -> VideoPlayer in
81
94
  let player = AVPlayer()
82
95
  let videoPlayer = VideoPlayer(player)
83
96
 
@@ -136,6 +149,10 @@ public final class VideoModule: Module {
136
149
  player.playbackRate = playbackRate
137
150
  }
138
151
 
152
+ Property("isLive") { player -> Bool in
153
+ return player.pointer.currentItem?.duration.isIndefinite ?? false
154
+ }
155
+
139
156
  Property("preservesPitch") { player -> Bool in
140
157
  return player.preservesPitch
141
158
  }
@@ -150,8 +167,8 @@ public final class VideoModule: Module {
150
167
  player.showNowPlayingNotification = showNowPlayingNotification
151
168
  }
152
169
 
153
- Property("status") { player -> PlayerStatus in
154
- return player.status
170
+ Property("status") { player in
171
+ return player.status.rawValue
155
172
  }
156
173
 
157
174
  Property("volume") { player -> Float in
@@ -169,7 +186,11 @@ public final class VideoModule: Module {
169
186
  player.pointer.pause()
170
187
  }
171
188
 
172
- Function("replace") { (player, source: Either<String, VideoSource>) in
189
+ Function("replace") { (player, source: Either<String, VideoSource>?) in
190
+ guard let source else {
191
+ try player.replaceCurrentItem(with: nil)
192
+ return
193
+ }
173
194
  var videoSource: VideoSource?
174
195
 
175
196
  if source.is(String.self), let url: String = source.get() {
@@ -16,7 +16,7 @@ internal final class VideoPlayer: SharedRef<AVPlayer>, Hashable, VideoPlayerObse
16
16
  if oldValue != playbackRate {
17
17
  safeEmit(event: "playbackRateChange", arguments: playbackRate, oldValue)
18
18
  }
19
- if #available(iOS 16.0, *) {
19
+ if #available(iOS 16.0, tvOS 16.0, *) {
20
20
  pointer.defaultRate = playbackRate
21
21
  }
22
22
  pointer.rate = playbackRate
@@ -83,9 +83,9 @@ internal final class VideoPlayer: SharedRef<AVPlayer>, Hashable, VideoPlayerObse
83
83
  }
84
84
 
85
85
  deinit {
86
+ observer.cleanup()
86
87
  NowPlayingManager.shared.unregisterPlayer(self)
87
88
  VideoManager.shared.unregister(videoPlayer: self)
88
- observer.unregisterDelegate(delegate: self)
89
89
  pointer.replaceCurrentItem(with: nil)
90
90
  }
91
91
 
@@ -98,7 +98,7 @@ internal final class VideoPlayer: SharedRef<AVPlayer>, Hashable, VideoPlayerObse
98
98
  return
99
99
  }
100
100
 
101
- let asset = AVURLAsset(url: url)
101
+ let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": videoSource.headers])
102
102
  let playerItem = VideoPlayerItem(asset: asset, videoSource: videoSource)
103
103
 
104
104
  if let drm = videoSource.drm {
@@ -143,7 +143,7 @@ internal final class VideoPlayer: SharedRef<AVPlayer>, Hashable, VideoPlayerObse
143
143
  }
144
144
 
145
145
  func onRateChanged(player: AVPlayer, oldRate: Float?, newRate: Float) {
146
- if #available(iOS 16.0, *) {
146
+ if #available(iOS 16.0, tvOS 16.0, *) {
147
147
  if player.defaultRate != playbackRate {
148
148
  // User changed the playback speed in the native controls. Update the desiredRate variable
149
149
  playbackRate = player.defaultRate
@@ -58,14 +58,13 @@ final class WeakPlayerObserverDelegate: Hashable {
58
58
  }
59
59
 
60
60
  class VideoPlayerObserver {
61
- let player: AVPlayer
61
+ weak var player: AVPlayer?
62
62
  var delegates = Set<WeakPlayerObserverDelegate>()
63
- weak var delegate: VideoPlayerObserverDelegate?
64
63
  private var currentItem: VideoPlayerItem?
65
64
 
66
65
  private var isPlaying: Bool = false {
67
66
  didSet {
68
- if oldValue != isPlaying {
67
+ if let player, oldValue != isPlaying {
69
68
  delegates.forEach { delegate in
70
69
  delegate.value?.onIsPlayingChanged(player: player, oldIsPlaying: oldValue, newIsPlaying: isPlaying)
71
70
  }
@@ -75,7 +74,7 @@ class VideoPlayerObserver {
75
74
  private var error: Exception?
76
75
  private var status: PlayerStatus = .idle {
77
76
  didSet {
78
- if oldValue != status {
77
+ if let player, oldValue != status {
79
78
  delegates.forEach { delegate in
80
79
  delegate.value?.onStatusChanged(player: player, oldStatus: oldValue, newStatus: status, error: error)
81
80
  }
@@ -104,8 +103,7 @@ class VideoPlayerObserver {
104
103
  }
105
104
 
106
105
  deinit {
107
- invalidatePlayerObservers()
108
- invalidateCurrentPlayerItemObservers()
106
+ cleanup()
109
107
  }
110
108
 
111
109
  func registerDelegate(delegate: VideoPlayerObserverDelegate) {
@@ -117,13 +115,34 @@ class VideoPlayerObserver {
117
115
  delegates.remove(WeakPlayerObserverDelegate(value: delegate))
118
116
  }
119
117
 
118
+ func cleanup() {
119
+ delegates.removeAll()
120
+ invalidatePlayerObservers()
121
+ invalidateCurrentPlayerItemObservers()
122
+ }
123
+
120
124
  private func initializePlayerObservers() {
121
- playerRateObserver = player.observe(\.rate, options: [.initial, .new, .old], changeHandler: onPlayerRateChanged)
122
- playerStatusObserver = player.observe(\.status, options: [.initial, .new, .old], changeHandler: onPlayerStatusChanged)
123
- playerTimeControlStatusObserver = player.observe(\.timeControlStatus, options: [.new, .old], changeHandler: onTimeControlStatusChanged)
124
- playerVolumeObserver = player.observe(\.volume, options: [.initial, .new, .old], changeHandler: onPlayerVolumeChanged)
125
- playerIsMutedObserver = player.observe(\.isMuted, options: [.initial, .new, .old], changeHandler: onPlayerIsMutedChanged)
126
- playerCurrentItemObserver = player.observe(\.currentItem, options: [.initial, .new], changeHandler: onPlayerCurrentItemChanged)
125
+ guard let player else {
126
+ return
127
+ }
128
+ playerRateObserver = player.observe(\.rate, options: [.initial, .new, .old]) { [weak self] player, change in
129
+ self?.onPlayerRateChanged(player, change)
130
+ }
131
+ playerStatusObserver = player.observe(\.status, options: [.initial, .new, .old]) { [weak self] player, change in
132
+ self?.onPlayerStatusChanged(player, change)
133
+ }
134
+ playerTimeControlStatusObserver = player.observe(\.timeControlStatus, options: [.new, .old]) { [weak self] player, change in
135
+ self?.onTimeControlStatusChanged(player, change)
136
+ }
137
+ playerVolumeObserver = player.observe(\.volume, options: [.initial, .new, .old]) { [weak self] player, change in
138
+ self?.onPlayerVolumeChanged(player, change)
139
+ }
140
+ playerIsMutedObserver = player.observe(\.isMuted, options: [.initial, .new, .old]) { [weak self] player, change in
141
+ self?.onPlayerIsMutedChanged(player, change)
142
+ }
143
+ playerCurrentItemObserver = player.observe(\.currentItem, options: [.initial, .new]) { [weak self] player, change in
144
+ self?.onPlayerCurrentItemChanged(player, change)
145
+ }
127
146
  }
128
147
 
129
148
  private func invalidatePlayerObservers() {
@@ -136,9 +155,17 @@ class VideoPlayerObserver {
136
155
  }
137
156
 
138
157
  private func initializeCurrentPlayerItemObservers(player: AVPlayer, playerItem: AVPlayerItem) {
139
- playbackBufferEmptyObserver = playerItem.observe(\.isPlaybackBufferEmpty, changeHandler: onIsBufferEmptyChanged)
140
- playbackLikelyToKeepUpObserver = playerItem.observe(\.isPlaybackLikelyToKeepUp, changeHandler: onPlayerLikelyToKeepUpChanged)
141
- playerItemStatusObserver = playerItem.observe(\.status, options: [.initial, .new], changeHandler: onItemStatusChanged)
158
+ playbackBufferEmptyObserver = playerItem.observe(\.isPlaybackBufferEmpty) { [weak self] item, change in
159
+ self?.onIsBufferEmptyChanged(item, change)
160
+ }
161
+
162
+ playbackLikelyToKeepUpObserver = playerItem.observe(\.isPlaybackLikelyToKeepUp) { [weak self] item, change in
163
+ self?.onPlayerLikelyToKeepUpChanged(item, change)
164
+ }
165
+
166
+ playerItemStatusObserver = playerItem.observe(\.status, options: [.initial, .new]) { [weak self] item, change in
167
+ self?.onItemStatusChanged(item, change)
168
+ }
142
169
 
143
170
  playerItemObserver = NotificationCenter.default.addObserver(
144
171
  forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
@@ -191,7 +218,7 @@ class VideoPlayerObserver {
191
218
  }
192
219
 
193
220
  private func onItemStatusChanged(_ playerItem: AVPlayerItem, _ change: NSKeyValueObservedChange<AVPlayerItem.Status>) {
194
- if player.status != .failed {
221
+ if player?.status != .failed {
195
222
  error = nil
196
223
  }
197
224
 
@@ -210,7 +237,9 @@ class VideoPlayerObserver {
210
237
  }
211
238
 
212
239
  delegates.forEach { delegate in
213
- delegate.value?.onPlayerItemStatusChanged(player: player, oldStatus: change.oldValue, newStatus: playerItem.status)
240
+ if let player {
241
+ delegate.value?.onPlayerItemStatusChanged(player: player, oldStatus: change.oldValue, newStatus: playerItem.status)
242
+ }
214
243
  }
215
244
  }
216
245
 
@@ -12,8 +12,14 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
12
12
  }
13
13
  }
14
14
 
15
+ #if os(tvOS)
16
+ var wasPlaying: Bool = false
17
+ #endif
15
18
  var isFullscreen: Bool = false
16
19
  var isInPictureInPicture = false
20
+ #if os(tvOS)
21
+ let startPictureInPictureAutomatically = false
22
+ #else
17
23
  var startPictureInPictureAutomatically = false {
18
24
  didSet {
19
25
  if #available(iOS 14.2, *) {
@@ -21,12 +27,15 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
21
27
  }
22
28
  }
23
29
  }
30
+ #endif
24
31
 
25
32
  var allowPictureInPicture: Bool = false {
26
33
  didSet {
27
34
  // PiP requires `.playback` audio session category in `.moviePlayback` mode
28
- VideoManager.shared.setAppropriateAudioSessionOrWarn()
29
- playerViewController.allowsPictureInPicturePlayback = allowPictureInPicture
35
+ if #available(iOS 13.4, tvOS 14.0, *) {
36
+ VideoManager.shared.setAppropriateAudioSessionOrWarn()
37
+ playerViewController.allowsPictureInPicturePlayback = allowPictureInPicture
38
+ }
30
39
  }
31
40
  }
32
41
 
@@ -40,7 +49,10 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
40
49
  }
41
50
 
42
51
  lazy var supportsPictureInPicture: Bool = {
43
- return AVPictureInPictureController.isPictureInPictureSupported()
52
+ if #available(iOS 13.4, tvOS 14.0, *) {
53
+ return AVPictureInPictureController.isPictureInPictureSupported()
54
+ }
55
+ return false
44
56
  }()
45
57
 
46
58
  public required init(appContext: AppContext? = nil) {
@@ -53,7 +65,9 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
53
65
  playerViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
54
66
  playerViewController.view.backgroundColor = .clear
55
67
  // Now playing is managed by the `NowPlayingManager`
68
+ #if !os(tvOS)
56
69
  playerViewController.updatesNowPlayingInfoCenter = false
70
+ #endif
57
71
 
58
72
  addSubview(playerViewController.view)
59
73
  }
@@ -71,6 +85,16 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
71
85
 
72
86
  if playerViewController.responds(to: selectorToForceFullScreenMode) {
73
87
  playerViewController.perform(selectorToForceFullScreenMode, with: true, with: nil)
88
+ } else {
89
+ #if os(tvOS)
90
+ // For TV, save the currently playing state,
91
+ // remove the view controller from its superview,
92
+ // and present the view controller normally
93
+ wasPlaying = player?.isPlaying == true
94
+ self.playerViewController.view.removeFromSuperview()
95
+ self.reactViewController().present(self.playerViewController, animated: true)
96
+ isFullscreen = true
97
+ #endif
74
98
  }
75
99
  }
76
100
 
@@ -110,6 +134,31 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
110
134
 
111
135
  // MARK: - AVPlayerViewControllerDelegate
112
136
 
137
+ #if os(tvOS)
138
+ // TV actually presents the playerViewController, so it implements the view controller
139
+ // dismissal delegate methods
140
+ public func playerViewControllerWillBeginDismissalTransition(_ playerViewController: AVPlayerViewController) {
141
+ // Start an appearance transition
142
+ self.playerViewController.beginAppearanceTransition(true, animated: true)
143
+ }
144
+
145
+ public func playerViewControllerDidEndDismissalTransition(_ playerViewController: AVPlayerViewController) {
146
+ self.isFullscreen = false
147
+ // Reset the bounds of the view controller and add it back to our view
148
+ self.playerViewController.view.frame = self.bounds
149
+ addSubview(self.playerViewController.view)
150
+ // End the appearance transition
151
+ self.playerViewController.endAppearanceTransition()
152
+ // Ensure playing state is preserved
153
+ if wasPlaying {
154
+ self.player?.pointer.play()
155
+ } else {
156
+ self.player?.pointer.pause()
157
+ }
158
+ }
159
+ #endif
160
+
161
+ #if !os(tvOS)
113
162
  public func playerViewController(
114
163
  _ playerViewController: AVPlayerViewController,
115
164
  willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator
@@ -134,6 +183,7 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
134
183
  }
135
184
  }
136
185
  }
186
+ #endif
137
187
 
138
188
  public func playerViewControllerDidStartPictureInPicture(_ playerViewController: AVPlayerViewController) {
139
189
  isInPictureInPicture = true
@@ -146,6 +196,10 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
146
196
  }
147
197
 
148
198
  public override func didMoveToWindow() {
199
+ // TV is doing a normal view controller present, so we should not execute
200
+ // this code
201
+ #if !os(tvOS)
149
202
  playerViewController.beginAppearanceTransition(self.window != nil, animated: true)
203
+ #endif
150
204
  }
151
205
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "expo-video",
3
3
  "title": "Expo Video",
4
- "version": "1.1.10",
4
+ "version": "1.2.1",
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": "979e9f1fc3cfa9c827700dcf99992e671cfdf63e"
39
+ "gitHead": "09b2d97bbc0f70f7c811ff9b6c9ad8808c5ad84b"
40
40
  }
@@ -62,6 +62,12 @@ export declare class VideoPlayer extends SharedObject<VideoPlayerEvents> {
62
62
  */
63
63
  playbackRate: number;
64
64
 
65
+ /**
66
+ * Boolean value indicating whether the player is currently playing a live stream.
67
+ * > This property is get-only
68
+ */
69
+ isLive: boolean;
70
+
65
71
  /**
66
72
  * Indicates the current status of the player.
67
73
  * > This property is get-only
@@ -172,6 +178,13 @@ export type VideoSource =
172
178
  * When undefined the player will display information contained in the video metadata.
173
179
  */
174
180
  metadata?: VideoMetadata;
181
+ /**
182
+ * Specifies headers sent with the video request.
183
+ * > For DRM license headers use the `headers` field of [`DRMOptions`](#drmoptions).
184
+ * @platform android
185
+ * @platform ios
186
+ */
187
+ headers?: Record<string, string>;
175
188
  }
176
189
  | null;
177
190
 
@@ -245,4 +258,11 @@ export type DRMOptions = {
245
258
  * @platform ios
246
259
  */
247
260
  certificateUrl?: string;
261
+
262
+ /**
263
+ * Specifies the base64 encoded certificate data for the FairPlay DRM.
264
+ * When this property is set, the `certificateUrl` property is ignored.
265
+ * @platform ios
266
+ */
267
+ base64CertificateData?: string;
248
268
  };
@@ -70,6 +70,10 @@ export default class VideoPlayerWeb
70
70
  return this._playbackRate;
71
71
  }
72
72
 
73
+ get isLive(): boolean {
74
+ return [...this._mountedVideos][0].duration === Infinity;
75
+ }
76
+
73
77
  set volume(value: number) {
74
78
  this._mountedVideos.forEach((video) => {
75
79
  video.volume = value;
@@ -194,6 +198,7 @@ export default class VideoPlayerWeb
194
198
  video.play();
195
199
  } else {
196
200
  video.removeAttribute('src');
201
+ video.load();
197
202
  }
198
203
  });
199
204
  this.playing = true;
@@ -78,7 +78,7 @@ export const VideoView = forwardRef((props: { player?: VideoPlayer } & VideoView
78
78
 
79
79
  return (
80
80
  <video
81
- controls={props.nativeControls}
81
+ controls={props.nativeControls ?? true}
82
82
  controlsList={props.allowsFullscreen ? undefined : 'nofullscreen'}
83
83
  crossOrigin="anonymous"
84
84
  style={{