diff --git a/bun.lock b/bun.lock
index 2817e54a..26a784bd 100644
--- a/bun.lock
+++ b/bun.lock
@@ -51,7 +51,7 @@
},
"example": {
"name": "react-native-video-example",
- "version": "7.0.0-alpha.3",
+ "version": "7.0.0-alpha.4",
"dependencies": {
"@react-native-community/slider": "^4.5.6",
"@twg/react-native-video-drm": "*",
@@ -107,7 +107,7 @@
},
"packages/react-native-video": {
"name": "react-native-video",
- "version": "7.0.0-alpha.3",
+ "version": "7.0.0-alpha.4",
"devDependencies": {
"@expo/config-plugins": "^10.0.2",
"@react-native/eslint-config": "^0.77.0",
diff --git a/docs/docs/events/events.md b/docs/docs/events/events.md
index 05f1d93f..68ef606c 100644
--- a/docs/docs/events/events.md
+++ b/docs/docs/events/events.md
@@ -74,6 +74,10 @@ Additionally, the `VideoPlayer` instance itself has an `onError` property:
This hook is recommended for managing event subscriptions in a declarative React style.
+### Initialization Timing and Events
+
+`onLoadStart` / `onLoad` will fire automatically after construction when `initializeOnCreation` (default `true`) is enabled. If you set `initializeOnCreation: false`, these events will not fire until you call `initialize()` or `preload()`. Attach your event handlers before invoking those methods to avoid missing early events.
+
## Subscribing to Events
You can subscribe to an event by assigning a function to the player instance's corresponding property:
diff --git a/docs/docs/player/player-lifecycle.md b/docs/docs/player/player-lifecycle.md
index b47dd8a7..dcfd5de1 100644
--- a/docs/docs/player/player-lifecycle.md
+++ b/docs/docs/player/player-lifecycle.md
@@ -9,19 +9,43 @@ Understanding the lifecycle of the `VideoPlayer` is crucial for managing resourc
## Creation and Initialization
-1. **Instantiation**: A `VideoPlayer` instance is created by calling its constructor with a video source (URL, `VideoSource`, or `VideoConfig`).
+1. **Instantiation**: A `VideoPlayer` instance is created by calling its constructor with a video source (URL, `VideoSource`, or `VideoConfig`).
```typescript
const player = new VideoPlayer('https://example.com/video.mp4');
```
-2. **Native Player Creation**: Internally this creates a native player instance tailored to the platform (iOS/Android).
+2. **Native Player Allocation**: A lightweight native player object is allocated immediately.
+3. **Asset Initialization**: By default (unless you opt out) the underlying media item is prepared **asynchronously right after creation**. You can control this with `initializeOnCreation` inside `VideoConfig`.
+
+### Deferred Initialization (Advanced)
+
+If you pass a `VideoConfig` with `{ initializeOnCreation: false }`, the player will skip preparing the media item automatically. This is useful when:
+
+- You need to batch‑create many players without incurring immediate decoding / network cost
+- You want to attach event handlers before any network requests happen
+- You want explicit control over when buffering begins (e.g. on user interaction)
+
+To initialize later, call:
+```ts
+await player.initialize();
+// or preload if you also want it prepared & ready
+await player.preload();
+```
+
+### Initialization Methods Comparison
+
+| Method | When to use | What it does |
+|--------|-------------|--------------|
+| `initialize()` | You deferred initialization and now want to create the native player item / media source | Creates & attaches the underlying player item / media source without starting playback |
+| `preload()` | You want the player item prepared (buffering kicked off) ahead of an upcoming `play()` call | Ensures the media source is set and prepared; resolves once preparation started (may already be initialized) |
+| Implicit (default) | `initializeOnCreation` not set or `true` | Automatically schedules initialization after JS construction |
:::info
-Player does not initialize asset right after JS class creation. Asset will be initialized when you call `preload()` or access any property/method of the player.
+By default, the player initializes automatically after construction. If you need to defer initialization, set `initializeOnCreation: false` in the config. You can then call `player.initialize()` or `player.preload()` later to start the player.
:::
## Playing a Video
-1. **Loading**: When `play()` is called for the first time, or after `replaceSourceAsync()`, the player starts loading the video metadata and buffering content.
+1. **Loading**: When the player (auto) initializes, `preload()` is called, or after `replaceSourceAsync()`, the player starts loading the video metadata and buffering content.
- `onLoadStart`: Fired when the video starts loading.
- `onLoad`: Fired when the video metadata is loaded and the player is ready to play (duration, dimensions, etc., are available).
- `onBuffer`: Fired when buffering starts or ends.
@@ -91,5 +115,19 @@ const MyComponent = () => {
- **Dependency Management**: If the `source` prop passed to `useVideoPlayer` changes, the hook will clean up the old player instance and create a new one with the new source.
:::tip
-Using `useVideoPlayer` is the recommended way to manage `VideoPlayer` instances in functional components to ensure proper lifecycle management and resource cleanup.
+Using `useVideoPlayer` is the recommended way to manage `VideoPlayer` instances in functional components to ensure proper lifecycle management and resource cleanup. It will also respect `initializeOnCreation` (defaults to `true`). If you need deferred initialization with the hook:
+
+```tsx
+const player = useVideoPlayer({
+ source: { uri: 'https://example.com/video.mp4' },
+ initializeOnCreation: false,
+}, (instance) => {
+ // Attach listeners first
+ instance.onLoad = () => console.log('Loaded');
+});
+
+// Later (e.g. on user tap)
+await player.initialize(); // or player.preload()
+player.play();
+```
:::
\ No newline at end of file
diff --git a/docs/docs/player/player.md b/docs/docs/player/player.md
index d28e90b6..6792cdbb 100644
--- a/docs/docs/player/player.md
+++ b/docs/docs/player/player.md
@@ -9,7 +9,7 @@ The `VideoPlayer` class is the primary way to control video playback. It provide
## Initialization
-To use the `VideoPlayer`, you first need to create an instance of it with a video source. There are two ways to do this:
+To use the `VideoPlayer`, you first need to create an instance of it with a video source. There are two ways to do this. By default the native media item is initialized asynchronously right after creation (unless you opt out with `initializeOnCreation: false`).
using `useVideoPlayer` hook
```tsx
@@ -60,7 +60,8 @@ The `VideoPlayer` class offers a comprehensive set of methods and properties to
| `seekBy(time: number)` | Seeks the video forward or backward by the specified number of seconds. |
| `seekTo(time: number)` | Seeks the video to a specific time in seconds. |
| `replaceSourceAsync(source: VideoSource \| VideoConfig \| null)` | Replaces the current video source with a new one. Pass `null` to release the current source without replacing it. |
-| `preload()` | Preloads the video content without starting playback. This can help improve the startup time when `play()` is called. |
+| `initialize()` | Manually initialize the underlying native player item when `initializeOnCreation` was set to `false`. No-op if already initialized. |
+| `preload()` | Ensures the media source is set and prepared (buffering started) without starting playback. If not yet initialized it will initialize first. |
| `release()` | Releases the player's native resources. The player is no longer usable after calling this method. **Note:** If you intend to reuse the player instance with a different source, use `replaceSourceAsync(null)` to clear resources instead of `release()`. |
### Properties
@@ -69,7 +70,7 @@ The `VideoPlayer` class offers a comprehensive set of methods and properties to
|----------|--------|------|-------------|
| `source` | Read-only | `VideoPlayerSource` | Gets the current `VideoPlayerSource` object. |
| `status` | Read-only | `VideoPlayerStatus` | Gets the current status (e.g., `playing`, `paused`, `buffering`). |
-| `duration` | Read-only | `number` | Gets the total duration of the video in seconds. |
+| `duration` | Read-only | `number` | Gets the total duration of the video in seconds. Returns `NaN` until metadata is loaded. |
| `volume` | Read/Write | `number` | Gets or sets the player volume (0.0 to 1.0). |
| `currentTime` | Read/Write | `number` | Gets or sets the current playback time in seconds. |
| `muted` | Read/Write | `boolean` | Gets or sets whether the video is muted. |
@@ -94,4 +95,5 @@ Protected content is supported via a plugin. See the full DRM guide: [DRM](./drm
Quick notes:
- Install and enable the official plugin `@twg/react-native-video-drm` and call `enable()` at app startup before creating players.
-- Pass DRM configuration on the source using the `drm` property of `VideoConfig` (see the DRM guide for platform specifics and `getLicense` examples).
\ No newline at end of file
+- Pass DRM configuration on the source using the `drm` property of `VideoConfig` (see the DRM guide for platform specifics and `getLicense` examples).
+- If you defer initialization (`initializeOnCreation: false`), be sure to call `await player.initialize()` (or `preload()`) before expecting DRM license acquisition events.
\ No newline at end of file
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
index 068364d5..0a5ee2d6 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -1565,7 +1565,7 @@ PODS:
- React-logger (= 0.77.2)
- React-perflogger (= 0.77.2)
- React-utils (= 0.77.2)
- - ReactNativeVideo (7.0.0-alpha.3):
+ - ReactNativeVideo (7.0.0-alpha.4):
- DoubleConversion
- glog
- hermes-engine
@@ -1904,7 +1904,7 @@ SPEC CHECKSUMS:
ReactAppDependencyProvider: f334cebc0beed0a72490492e978007082c03d533
ReactCodegen: 474fbb3e4bb0f1ee6c255d1955db76e13d509269
ReactCommon: 7763e59534d58e15f8f22121cdfe319040e08888
- ReactNativeVideo: 213235288864ce876c68a64cc1481fe8a3eae5d5
+ ReactNativeVideo: f365bc4f1a57ab50ddb655cda2f47bc06698a53b
ReactNativeVideoDrm: 62840ae0e184f711a2e6495c18e342a74cb598f8
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: 31a098f74c16780569aebd614a0f37a907de0189
diff --git a/example/src/App.tsx b/example/src/App.tsx
index 118e0ca6..819bf68b 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -134,11 +134,13 @@ const VideoDemo = () => {
useEvent(player, 'onVolumeChange', handleVolumeChange);
React.useEffect(() => {
- if (!settings.show) return;
-
player.volume = settings.volume;
player.muted = settings.muted;
- player.rate = settings.rate;
+
+ if (player.isPlaying) {
+ player.rate = settings.rate;
+ }
+
player.loop = settings.loop;
player.playInBackground = settings.playInBackground;
player.playWhenInactive = settings.playWhenInactive;
@@ -179,18 +181,14 @@ const VideoDemo = () => {
-
- {formatTime(settings.show ? player.duration : 0)}
-
+ {formatTime(player.duration)}
diff --git a/packages/react-native-video/android/src/main/java/com/twg/video/core/services/playback/VideoPlaybackService.kt b/packages/react-native-video/android/src/main/java/com/twg/video/core/services/playback/VideoPlaybackService.kt
index d54bd383..2f7f3f8c 100644
--- a/packages/react-native-video/android/src/main/java/com/twg/video/core/services/playback/VideoPlaybackService.kt
+++ b/packages/react-native-video/android/src/main/java/com/twg/video/core/services/playback/VideoPlaybackService.kt
@@ -60,7 +60,7 @@ class VideoPlaybackService : MediaSessionService() {
}
sourceActivity = from
- val mediaSession = MediaSession.Builder(this, player.playerPointer)
+ val mediaSession = MediaSession.Builder(this, player.player)
.setId("RNVideoPlaybackService_" + player.hashCode())
.setCallback(VideoPlaybackCallback())
.setCustomLayout(immutableListOf(seekBackwardBtn, seekForwardBtn))
@@ -75,7 +75,7 @@ class VideoPlaybackService : MediaSessionService() {
}
fun unregisterPlayer(player: HybridVideoPlayer) {
- hidePlayerNotification(player.playerPointer)
+ hidePlayerNotification(player.player)
val session = mediaSessionsList.remove(player)
session?.release()
if (mediaSessionsList.isEmpty()) {
diff --git a/packages/react-native-video/android/src/main/java/com/twg/video/hybrids/videoplayer/HybridVideoPlayer.kt b/packages/react-native-video/android/src/main/java/com/twg/video/hybrids/videoplayer/HybridVideoPlayer.kt
index 55f92e5c..65e102b0 100644
--- a/packages/react-native-video/android/src/main/java/com/twg/video/hybrids/videoplayer/HybridVideoPlayer.kt
+++ b/packages/react-native-video/android/src/main/java/com/twg/video/hybrids/videoplayer/HybridVideoPlayer.kt
@@ -1,6 +1,5 @@
package com.margelo.nitro.video
-import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
@@ -11,7 +10,6 @@ import androidx.media3.common.PlaybackParameters
import androidx.media3.common.Player
import androidx.media3.common.Tracks
import androidx.media3.common.text.CueGroup
-import androidx.media3.common.TrackSelectionOverride
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.DefaultRenderersFactory
@@ -28,9 +26,13 @@ import com.margelo.nitro.core.Promise
import com.twg.video.core.LibraryError
import com.twg.video.core.PlayerError
import com.twg.video.core.VideoManager
+import com.twg.video.core.extensions.startService
+import com.twg.video.core.extensions.stopService
import com.twg.video.core.player.OnAudioFocusChangedListener
import com.twg.video.core.recivers.AudioBecomingNoisyReceiver
import com.twg.video.core.services.playback.VideoPlaybackService
+import com.twg.video.core.services.playback.VideoPlaybackServiceConnection
+import com.twg.video.core.utils.TextTrackUtils
import com.twg.video.core.utils.Threading.mainThreadProperty
import com.twg.video.core.utils.Threading.runOnMainThread
import com.twg.video.core.utils.Threading.runOnMainThreadSync
@@ -38,10 +40,6 @@ import com.twg.video.core.utils.VideoOrientationUtils
import com.twg.video.view.VideoView
import java.lang.ref.WeakReference
import kotlin.math.max
-import com.twg.video.core.extensions.startService
-import com.twg.video.core.extensions.stopService
-import com.twg.video.core.services.playback.VideoPlaybackServiceConnection
-import com.twg.video.core.utils.TextTrackUtils
@UnstableApi
@DoNotStrip
@@ -57,14 +55,13 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
}
private var allocator: DefaultAllocator? = null
- private var context: Context = NitroModules.applicationContext
- ?.currentActivity
- ?.applicationContext
+ private var context = NitroModules.applicationContext
?: run {
throw LibraryError.ApplicationContextNotFound
}
- var player: ExoPlayer? = null
+ lateinit var player: ExoPlayer
+ var loadedWithSource = false
private var currentPlayerView: WeakReference? = null
var wasAutoPaused = false
@@ -100,84 +97,62 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
field = value
}
- var playerPointer: ExoPlayer
- get() {
- if (player == null) {
- runOnMainThreadSync {
- initializePlayer()
-
- if (player == null) {
- throw PlayerError.NotInitialized
- }
-
- if (player!!.playbackState == Player.STATE_IDLE) {
- player!!.prepare()
- }
- }
- }
-
- return player!!
- }
- private set(value) {
- player = value
- }
-
// Player Properties
override var currentTime: Double by mainThreadProperty(
- get = { playerPointer.currentPosition.toDouble() / 1000.0 },
- set = { value -> runOnMainThread { playerPointer.seekTo((value * 1000).toLong()) } }
+ get = { player.currentPosition.toDouble() / 1000.0 },
+ set = { value -> runOnMainThread { player.seekTo((value * 1000).toLong()) } }
)
// volume defined by user
var userVolume: Double = 1.0
override var volume: Double by mainThreadProperty(
- get = { playerPointer.volume.toDouble() },
+ get = { player.volume.toDouble() },
set = { value ->
userVolume = value
- playerPointer.volume = value.toFloat()
+ player.volume = value.toFloat()
}
)
override val duration: Double by mainThreadProperty(
get = {
- val duration = playerPointer.duration
+ val duration = player.duration
return@mainThreadProperty if (duration == C.TIME_UNSET) Double.NaN else duration.toDouble() / 1000.0
}
)
override var loop: Boolean by mainThreadProperty(
get = {
- playerPointer.repeatMode == Player.REPEAT_MODE_ONE
+ player.repeatMode == Player.REPEAT_MODE_ONE
},
set = { value ->
- playerPointer.repeatMode = if (value) Player.REPEAT_MODE_ONE else Player.REPEAT_MODE_OFF
+ player.repeatMode = if (value) Player.REPEAT_MODE_ONE else Player.REPEAT_MODE_OFF
}
)
override var muted: Boolean by mainThreadProperty(
get = {
- val playerVolume = playerPointer.volume.toDouble()
+ val playerVolume = player.volume.toDouble()
return@mainThreadProperty playerVolume == 0.0
},
set = { value ->
if (value) {
userVolume = volume
- playerPointer.volume = 0f
+ player.volume = 0f
} else {
- playerPointer.volume = userVolume.toFloat()
+ player.volume = userVolume.toFloat()
}
eventEmitter.onVolumeChange(onVolumeChangeData(
- volume = playerPointer.volume.toDouble(),
+ volume = player.volume.toDouble(),
muted = muted
))
}
)
override var rate: Double by mainThreadProperty(
- get = { playerPointer.playbackParameters.speed.toDouble() },
+ get = { player.playbackParameters.speed.toDouble() },
set = { value ->
- playerPointer.playbackParameters = playerPointer.playbackParameters.withSpeed(value.toFloat())
+ player.playbackParameters = player.playbackParameters.withSpeed(value.toFloat())
}
)
@@ -207,7 +182,7 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
override var playWhenInactive: Boolean = false
override var isPlaying: Boolean by mainThreadProperty(
- get = { player?.isPlaying == true }
+ get = { player.isPlaying == true }
)
private fun initializePlayer() {
@@ -216,7 +191,6 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
}
val hybridSource = source as? HybridVideoPlayerSource ?: throw PlayerError.InvalidSource
- val appContext = NitroModules.applicationContext!!
// Initialize the allocator
allocator = DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE)
@@ -233,20 +207,22 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
)
.build()
- val renderersFactory = DefaultRenderersFactory(appContext)
+ val renderersFactory = DefaultRenderersFactory(context)
.forceEnableMediaCodecAsynchronousQueueing()
.setEnableDecoderFallback(true)
// Build the player with the LoadControl
- playerPointer = ExoPlayer.Builder(NitroModules.applicationContext!!)
+ player = ExoPlayer.Builder(context)
.setLoadControl(loadControl)
.setLooper(Looper.getMainLooper())
.setRenderersFactory(renderersFactory)
.build()
- playerPointer.addListener(playerListener)
- playerPointer.addAnalyticsListener(analyticsListener)
- playerPointer.setMediaSource(hybridSource.mediaSource)
+ loadedWithSource = true
+
+ player.addListener(playerListener)
+ player.addAnalyticsListener(analyticsListener)
+ player.setMediaSource(hybridSource.mediaSource)
// Emit onLoadStart
val sourceType = if (hybridSource.uri.startsWith("http")) SourceType.NETWORK else SourceType.LOCAL
@@ -255,20 +231,39 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
startProgressUpdates()
}
+ override fun initialize(): Promise {
+ return Promise.async {
+ return@async runOnMainThreadSync {
+ initializePlayer()
+ player.prepare()
+ }
+ }
+ }
+
constructor(source: HybridVideoPlayerSource) : this() {
this.source = source
+
+ runOnMainThread {
+ if (source.config.initializeOnCreation == true) {
+ initializePlayer()
+ player.prepare()
+ } else {
+ player = ExoPlayer.Builder(context).build()
+ }
+ }
+
VideoManager.registerPlayer(this)
}
override fun play() {
runOnMainThread {
- playerPointer.play()
+ player.play()
}
}
override fun pause() {
runOnMainThread {
- playerPointer.pause()
+ player.pause()
}
}
@@ -292,10 +287,10 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
runOnMainThreadSync {
// Update source
this.source = source
- playerPointer.setMediaSource(hybridSource.mediaSource)
+ player.setMediaSource(hybridSource.mediaSource)
// Prepare player
- playerPointer.prepare()
+ player.prepare()
}
}
}
@@ -303,15 +298,15 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
override fun preload(): Promise {
return Promise.async {
runOnMainThreadSync {
- if (player == null) {
+ if (!loadedWithSource) {
initializePlayer()
}
- if (player != null && player?.playbackState != Player.STATE_IDLE) {
+ if (player.playbackState != Player.STATE_IDLE) {
return@runOnMainThreadSync
}
- player?.prepare()
+ player.prepare()
}
}
}
@@ -323,11 +318,11 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
VideoManager.unregisterPlayer(this)
stopProgressUpdates()
+ loadedWithSource = false
runOnMainThread {
- player?.removeListener(playerListener)
- player?.removeAnalyticsListener(analyticsListener)
- player?.release() // Release player
- player = null // Nullify the player
+ player.removeListener(playerListener)
+ player.removeAnalyticsListener(analyticsListener)
+ player.release() // Release player
// Clean Listeners
audioFocusChangedListener.removeEventEmitter()
@@ -342,7 +337,7 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
VideoManager.addViewToPlayer(videoView, this)
runOnMainThreadSync {
- PlayerView.switchTargetView(playerPointer, currentPlayerView?.get(), videoView.playerView)
+ PlayerView.switchTargetView(player, currentPlayerView?.get(), videoView.playerView)
currentPlayerView = WeakReference(videoView.playerView)
}
}
@@ -358,9 +353,9 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
stopProgressUpdates() // Ensure no multiple runnables
progressRunnable = object : Runnable {
override fun run() {
- if (playerPointer.playbackState != Player.STATE_IDLE && playerPointer.playbackState != Player.STATE_ENDED) {
- val currentTimeSeconds = playerPointer.currentPosition / 1000.0
- val bufferedDurationSeconds = playerPointer.bufferedPosition / 1000.0
+ if (player.playbackState != Player.STATE_IDLE && player.playbackState != Player.STATE_ENDED) {
+ val currentTimeSeconds = player.currentPosition / 1000.0
+ val bufferedDurationSeconds = player.bufferedPosition / 1000.0
// bufferDuration is the time from current time that is buffered.
val playableDurationFromNow = max(0.0, bufferedDurationSeconds - currentTimeSeconds)
@@ -389,7 +384,7 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
totalBytesLoaded: Long,
bitrateEstimate: Long
) {
- val videoFormat = playerPointer.videoFormat
+ val videoFormat = player.videoFormat
eventEmitter.onBandwidthUpdate(
BandwidthData(
bitrate = bitrateEstimate.toDouble(),
@@ -402,7 +397,7 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
private val playerListener = object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
- val isPlayingUpdate = playerPointer.isPlaying
+ val isPlayingUpdate = player.isPlaying
val isBufferingUpdate = playbackState == Player.STATE_BUFFERING
eventEmitter.onPlaybackStateChange(
@@ -425,8 +420,8 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
status = VideoPlayerStatus.READYTOPLAY
eventEmitter.onBuffer(false)
- val generalVideoFormat = playerPointer.videoFormat
- val currentTracks = playerPointer.currentTracks
+ val generalVideoFormat = player.videoFormat
+ val currentTracks = player.currentTracks
val selectedVideoTrackGroup = currentTracks.groups.find { group -> group.type == C.TRACK_TYPE_VIDEO && group.isSelected }
val selectedVideoTrackFormat = if (selectedVideoTrackGroup != null && selectedVideoTrackGroup.length > 0) {
@@ -441,15 +436,15 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
eventEmitter.onLoad(
onLoadData(
- currentTime = playerPointer.currentPosition / 1000.0,
- duration = if (playerPointer.duration == C.TIME_UNSET) Double.NaN else playerPointer.duration / 1000.0,
+ currentTime = player.currentPosition / 1000.0,
+ duration = if (player.duration == C.TIME_UNSET) Double.NaN else player.duration / 1000.0,
width = width.toDouble(),
height = height.toDouble(),
orientation = VideoOrientationUtils.fromWHR(width, height, rotationDegrees)
)
)
// If player becomes ready and is set to play, start progress updates
- if (playerPointer.playWhenReady) {
+ if (player.playWhenReady) {
startProgressUpdates()
}
@@ -469,14 +464,14 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
eventEmitter.onPlaybackStateChange(
onPlaybackStateChangeData(
isPlaying = isPlaying,
- isBuffering = playerPointer.playbackState == Player.STATE_BUFFERING
+ isBuffering = player.playbackState == Player.STATE_BUFFERING
)
)
if (isPlaying) {
VideoManager.setLastPlayedPlayer(this@HybridVideoPlayer)
startProgressUpdates()
} else {
- if (playerPointer.playbackState == Player.STATE_ENDED || playerPointer.playbackState == Player.STATE_IDLE) {
+ if (player.playbackState == Player.STATE_ENDED || player.playbackState == Player.STATE_IDLE) {
stopProgressUpdates()
}
}
@@ -497,7 +492,7 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
}
// Update progress immediately after a discontinuity if needed by your logic
val currentTimeSeconds = newPosition.positionMs / 1000.0
- val bufferedDurationSeconds = playerPointer.bufferedPosition / 1000.0
+ val bufferedDurationSeconds = player.bufferedPosition / 1000.0
eventEmitter.onProgress(
onProgressData(
currentTime = currentTimeSeconds,
@@ -564,12 +559,12 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
// MARK: - Text Track Management
override fun getAvailableTextTracks(): Array {
- return TextTrackUtils.getAvailableTextTracks(playerPointer, source)
+ return TextTrackUtils.getAvailableTextTracks(player, source)
}
override fun selectTextTrack(textTrack: TextTrack?) {
selectedExternalTrackIndex = TextTrackUtils.selectTextTrack(
- player = playerPointer,
+ player = player,
textTrack = textTrack,
source = source,
onTrackChange = { track -> eventEmitter.onTrackChange(track) }
@@ -577,5 +572,5 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
}
override val selectedTrack: TextTrack?
- get() = TextTrackUtils.getSelectedTrack(playerPointer, source)
+ get() = TextTrackUtils.getSelectedTrack(player, source)
}
diff --git a/packages/react-native-video/android/src/main/java/com/twg/video/hybrids/videoplayersource/HybridVideoPlayerSourceFactory.kt b/packages/react-native-video/android/src/main/java/com/twg/video/hybrids/videoplayersource/HybridVideoPlayerSourceFactory.kt
index 7adcd27e..4a965c73 100644
--- a/packages/react-native-video/android/src/main/java/com/twg/video/hybrids/videoplayersource/HybridVideoPlayerSourceFactory.kt
+++ b/packages/react-native-video/android/src/main/java/com/twg/video/hybrids/videoplayersource/HybridVideoPlayerSourceFactory.kt
@@ -5,7 +5,7 @@ import com.facebook.proguard.annotations.DoNotStrip
@DoNotStrip
class HybridVideoPlayerSourceFactory: HybridVideoPlayerSourceFactorySpec() {
override fun fromUri(uri: String): HybridVideoPlayerSourceSpec {
- val config = NativeVideoConfig(uri, null, null, null)
+ val config = NativeVideoConfig(uri, null, null, null, true)
return HybridVideoPlayerSource(config)
}
diff --git a/packages/react-native-video/ios/core/HLSSubtitleInjector.swift b/packages/react-native-video/ios/core/HLSSubtitleInjector.swift
index 5070b210..fa036b43 100644
--- a/packages/react-native-video/ios/core/HLSSubtitleInjector.swift
+++ b/packages/react-native-video/ios/core/HLSSubtitleInjector.swift
@@ -129,6 +129,20 @@ class HLSSubtitleInjector: NSObject {
.error()
}
+ // Post-process: ensure every variant stream references our subtitle group if we injected it
+ if hasSubtitleGroup {
+ modifiedLines = modifiedLines.map { line in
+ if line.hasPrefix("#EXT-X-STREAM-INF:") && !line.contains("SUBTITLES=") {
+ if line.hasSuffix(",") {
+ return line + "SUBTITLES=\"\(Self.subtitleGroupID)\""
+ } else {
+ return line + ",SUBTITLES=\"\(Self.subtitleGroupID)\""
+ }
+ }
+ return line
+ }
+ }
+
return modifiedLines.joined(separator: "\n")
}
diff --git a/packages/react-native-video/ios/core/Spec/NativeVideoPlayerSpec.swift b/packages/react-native-video/ios/core/Spec/NativeVideoPlayerSpec.swift
index 21dbf0b3..674ae58b 100644
--- a/packages/react-native-video/ios/core/Spec/NativeVideoPlayerSpec.swift
+++ b/packages/react-native-video/ios/core/Spec/NativeVideoPlayerSpec.swift
@@ -15,10 +15,7 @@ public protocol NativeVideoPlayerSpec {
// MARK: - Properties
/// The underlying AVPlayer instance (should not be used directly)
- var player: AVPlayer? { get set }
-
- /// The actual player that should be used for playback
- var playerPointer: AVPlayer { get set }
+ var player: AVPlayer { get set }
/// The current player item
var playerItem: AVPlayerItem? { get set }
diff --git a/packages/react-native-video/ios/core/VideoManager.swift b/packages/react-native-video/ios/core/VideoManager.swift
index 3ff16570..af3baac2 100644
--- a/packages/react-native-video/ios/core/VideoManager.swift
+++ b/packages/react-native-video/ios/core/VideoManager.swift
@@ -126,7 +126,7 @@ class VideoManager {
private func updateAudioSessionConfiguration() {
let isAnyPlayerPlaying = players.allObjects.contains { hybridPlayer in
- hybridPlayer.player?.isMuted == false && hybridPlayer.player?.rate != 0
+ hybridPlayer.player.isMuted == false && hybridPlayer.player.rate != 0
}
let anyPlayerNeedsNotMixWithOthers = players.allObjects.contains { player in
@@ -230,9 +230,9 @@ class VideoManager {
if backgroundPlayback {
players.allObjects.forEach { player in
if player.playInBackground {
- player.player?.audiovisualBackgroundPlaybackPolicy = .continuesIfPossible
+ player.player.audiovisualBackgroundPlaybackPolicy = .continuesIfPossible
} else {
- player.player?.audiovisualBackgroundPlaybackPolicy = .pauses
+ player.player.audiovisualBackgroundPlaybackPolicy = .pauses
}
}
}
@@ -261,7 +261,7 @@ class VideoManager {
func determineAudioMixingMode() -> MixAudioMode {
let activePlayers = players.allObjects.filter { player in
- player.isPlaying && player.player?.isMuted != true
+ player.isPlaying && player.player.isMuted != true
}
if activePlayers.isEmpty {
@@ -351,7 +351,7 @@ class VideoManager {
@objc func applicationWillResignActive(notification: Notification) {
// Pause all players when the app is about to become inactive
for player in players.allObjects {
- if player.playInBackground || player.playWhenInactive || !player.isPlaying || player.player?.isExternalPlaybackActive == true {
+ if player.playInBackground || player.playWhenInactive || !player.isPlaying || player.player.isExternalPlaybackActive == true {
continue
}
@@ -373,7 +373,7 @@ class VideoManager {
@objc func applicationDidEnterBackground(notification: Notification) {
// Pause all players when the app enters background
for player in players.allObjects {
- if player.playInBackground || player.player?.isExternalPlaybackActive == true || !player.isPlaying {
+ if player.playInBackground || player.player.isExternalPlaybackActive == true || !player.isPlaying {
continue
}
diff --git a/packages/react-native-video/ios/hybrids/VideoPlayer/HybridVideoPlayer+Events.swift b/packages/react-native-video/ios/hybrids/VideoPlayer/HybridVideoPlayer+Events.swift
index f1681fda..8aabaab1 100644
--- a/packages/react-native-video/ios/hybrids/VideoPlayer/HybridVideoPlayer+Events.swift
+++ b/packages/react-native-video/ios/hybrids/VideoPlayer/HybridVideoPlayer+Events.swift
@@ -44,7 +44,7 @@ extension HybridVideoPlayer: VideoPlayerObserverDelegate {
func onPlaybackLikelyToKeepUp() {
isCurrentlyBuffering = false
- if playerPointer.timeControlStatus == .playing {
+ if player.timeControlStatus == .playing {
status = .readytoplay
}
updateAndEmitPlaybackState()
@@ -55,7 +55,7 @@ extension HybridVideoPlayer: VideoPlayerObserverDelegate {
}
func onTimeControlStatusChanged(status: AVPlayer.TimeControlStatus) {
- if playerPointer.status == .failed || playerItem?.status == .failed {
+ if player.status == .failed || playerItem?.status == .failed {
self.status = .error
isCurrentlyBuffering = false
eventEmitter.onPlaybackStateChange(.init(isPlaying: false, isBuffering: false))
@@ -175,7 +175,7 @@ extension HybridVideoPlayer: VideoPlayerObserverDelegate {
}
func updateAndEmitPlaybackState() {
- let isPlaying = (player?.rate ?? 0) > 0 && !isCurrentlyBuffering
+ let isPlaying = player.rate > 0 && !isCurrentlyBuffering
eventEmitter.onPlaybackStateChange(.init(isPlaying: isPlaying, isBuffering: isCurrentlyBuffering))
eventEmitter.onBuffer(isCurrentlyBuffering)
diff --git a/packages/react-native-video/ios/hybrids/VideoPlayer/HybridVideoPlayer.swift b/packages/react-native-video/ios/hybrids/VideoPlayer/HybridVideoPlayer.swift
index a6c7da65..7f76ed92 100644
--- a/packages/react-native-video/ios/hybrids/VideoPlayer/HybridVideoPlayer.swift
+++ b/packages/react-native-video/ios/hybrids/VideoPlayer/HybridVideoPlayer.swift
@@ -11,9 +11,9 @@ import NitroModules
class HybridVideoPlayer: HybridVideoPlayerSpec, NativeVideoPlayerSpec {
/**
- * This in general should not be used directly, use `playerPointer` instead. This should be set only from within the playerQueue.
+ * Player instance for video playback
*/
- var player: AVPlayer? {
+ var player: AVPlayer {
didSet {
playerObserver?.initializePlayerObservers()
}
@@ -22,56 +22,26 @@ class HybridVideoPlayer: HybridVideoPlayerSpec, NativeVideoPlayerSpec {
}
}
- /**
- * The player queue is used to synchronize player initialization.
- */
- private let playerQueue = DispatchQueue(label: "com.nitro.hybridplayer", qos: .userInitiated)
-
- /**
- * This is the actual player that should be used for playback. It is initialized lazily when `playerPointer` is accessed.
- */
- var playerPointer: AVPlayer {
- get {
- // Synchronize access to player instance
- playerQueue.sync {
- if player != nil && playerItem != nil {
- return player!
- }
-
- do {
- if self.playerItem == nil {
- self.playerItem = try initializePlayerItemSync()
- }
-
- if player == nil {
- player = AVPlayer()
- }
-
- player?.replaceCurrentItem(with: playerItem)
- } catch {
- playerItem = nil
- player = AVPlayer()
- }
-
- return player!
- }
- }
- set {
- playerQueue.sync {
- player = newValue
- }
- }
- }
-
var playerItem: AVPlayerItem?
var playerObserver: VideoPlayerObserver?
init(source: (any HybridVideoPlayerSourceSpec)) throws {
self.source = source
self.eventEmitter = HybridVideoPlayerEventEmitter()
+
+ // Initialize AVPlayer with empty item
+ self.player = AVPlayer()
super.init()
self.playerObserver = VideoPlayerObserver(delegate: self)
+ self.playerObserver?.initializePlayerObservers()
+
+ Task {
+ if source.config.initializeOnCreation == true {
+ self.playerItem = try await initializePlayerItem()
+ self.player.replaceCurrentItem(with: self.playerItem)
+ }
+ }
VideoManager.shared.register(player: self)
}
@@ -96,54 +66,56 @@ class HybridVideoPlayer: HybridVideoPlayerSpec, NativeVideoPlayerSpec {
var volume: Double {
set {
- playerPointer.volume = Float(newValue)
+ player.volume = Float(newValue)
}
get {
- return Double(playerPointer.volume)
+ return Double(player.volume)
}
}
var muted: Bool {
set {
- playerPointer.isMuted = newValue
- eventEmitter.onVolumeChange(onVolumeChangeData(
- volume: Double(playerPointer.volume),
- muted: muted
- ))
+ player.isMuted = newValue
+ eventEmitter.onVolumeChange(
+ onVolumeChangeData(
+ volume: Double(player.volume),
+ muted: muted
+ )
+ )
}
get {
- return playerPointer.isMuted
+ return player.isMuted
}
}
var currentTime: Double {
set {
eventEmitter.onSeek(newValue)
- playerPointer.seek(
+ player.seek(
to: CMTime(seconds: newValue, preferredTimescale: 1000),
toleranceBefore: .zero,
toleranceAfter: .zero
)
}
get {
- playerPointer.currentTime().seconds
+ player.currentTime().seconds
}
}
var duration: Double {
- Double(playerPointer.currentItem?.duration.seconds ?? Double.nan)
+ Double(player.currentItem?.duration.seconds ?? Double.nan)
}
var rate: Double {
set {
if #available(iOS 16.0, tvOS 16.0, *) {
- playerPointer.defaultRate = Float(newValue)
+ player.defaultRate = Float(newValue)
}
- playerPointer.rate = Float(newValue)
+ player.rate = Float(newValue)
}
get {
- return Double(playerPointer.rate)
+ return Double(player.rate)
}
}
@@ -174,33 +146,40 @@ class HybridVideoPlayer: HybridVideoPlayerSpec, NativeVideoPlayerSpec {
// Text track selection state
private var selectedExternalTrackIndex: Int? = nil
- // MARK: - Buffering state tracking
var isCurrentlyBuffering: Bool = false
var isPlaying: Bool {
- // we are using here player as we don't want to initialize it
- // player is initialized lazily when playerPointer is accessed
- return player?.rate != 0
+ return player.rate != 0
+ }
+
+ func initialize() throws -> Promise {
+ return Promise.async { [weak self] in
+ guard let self else {
+ throw LibraryError.deallocated(objectName: "HybridVideoPlayer").error()
+ }
+
+ if self.playerItem != nil {
+ return
+ }
+
+ self.playerItem = try await self.initializePlayerItem()
+ self.player.replaceCurrentItem(with: self.playerItem)
+ }
}
func release() {
- playerQueue.async { [weak self] in
- guard let self = self else { return }
+ self.player.replaceCurrentItem(with: nil)
+ self.playerItem = nil
- self.player?.replaceCurrentItem(with: nil)
- self.player = nil
- self.playerItem = nil
-
- if let source = self.source as? HybridVideoPlayerSource {
- source.releaseAsset()
- }
-
- // Clear player observer
- self.playerObserver = nil
- status = .idle
-
- VideoManager.shared.unregister(player: self)
+ if let source = self.source as? HybridVideoPlayerSource {
+ source.releaseAsset()
}
+
+ // Clear player observer
+ self.playerObserver = nil
+ status = .idle
+
+ VideoManager.shared.unregister(player: self)
}
func preload() throws -> NitroModules.Promise {
@@ -213,7 +192,10 @@ class HybridVideoPlayer: HybridVideoPlayerSpec, NativeVideoPlayerSpec {
Task.detached(priority: .userInitiated) { [weak self] in
guard let self else {
- promise.reject(withError: LibraryError.deallocated(objectName: "HybridVideoPlayer").error())
+ promise.reject(
+ withError: LibraryError.deallocated(objectName: "HybridVideoPlayer")
+ .error()
+ )
return
}
@@ -221,11 +203,8 @@ class HybridVideoPlayer: HybridVideoPlayerSpec, NativeVideoPlayerSpec {
let playerItem = try await self.initializePlayerItem()
self.playerItem = playerItem
- self.playerQueue.sync {
- self.player = AVPlayer()
- self.player?.replaceCurrentItem(with: playerItem)
- promise.resolve(withResult: ())
- }
+ self.player.replaceCurrentItem(with: playerItem)
+ promise.resolve(withResult: ())
} catch {
promise.reject(withError: error)
}
@@ -235,15 +214,15 @@ class HybridVideoPlayer: HybridVideoPlayerSpec, NativeVideoPlayerSpec {
}
func play() throws {
- playerPointer.play()
+ player.play()
}
func pause() throws {
- playerPointer.pause()
+ player.pause()
}
func seekBy(time: Double) throws {
- guard let currentItem = playerPointer.currentItem else {
+ guard let currentItem = player.currentItem else {
throw PlayerError.notInitialized.error()
}
@@ -262,7 +241,9 @@ class HybridVideoPlayer: HybridVideoPlayerSpec, NativeVideoPlayerSpec {
currentTime = time
}
- func replaceSourceAsync(source: (any HybridVideoPlayerSourceSpec)?) throws -> Promise {
+ func replaceSourceAsync(source: (any HybridVideoPlayerSourceSpec)?) throws
+ -> Promise
+ {
let promise = Promise()
guard let source else {
@@ -273,27 +254,17 @@ class HybridVideoPlayer: HybridVideoPlayerSpec, NativeVideoPlayerSpec {
Task.detached(priority: .userInitiated) { [weak self] in
guard let self else {
- promise.reject(withError: LibraryError.deallocated(objectName: "HybridVideoPlayer").error())
+ promise.reject(
+ withError: LibraryError.deallocated(objectName: "HybridVideoPlayer")
+ .error()
+ )
return
}
self.source = source
self.playerItem = try await self.initializePlayerItem()
-
- playerQueue.sync {
- do {
- guard let player = self.player else {
- throw PlayerError.notInitialized.error()
- }
-
- player.replaceCurrentItem(with: self.playerItem)
- promise.resolve(withResult: ())
- } catch {
- self.playerItem = nil
- self.player = AVPlayer()
- promise.reject(withError: error)
- }
- }
+ self.player.replaceCurrentItem(with: self.playerItem)
+ promise.resolve(withResult: ())
}
return promise
@@ -301,60 +272,34 @@ class HybridVideoPlayer: HybridVideoPlayerSpec, NativeVideoPlayerSpec {
// MARK: - Methods
- /**
- * Initialize the player item synchronously. This is used to initialize the player item before it is set to the player.
- * This is necessary because the player item is used to initialize the player.
- * This is a blocking call and should be used with caution. prefer using `initializePlayerItem()` instead.
- */
- private func initializePlayerItemSync() throws -> AVPlayerItem {
- let semaphore = DispatchSemaphore(value: 0)
- var initializedItem: AVPlayerItem?
- var initializationError: Error?
-
- Task.detached(priority: .userInitiated) { [weak self] in
- guard let strongSelf = self else {
- semaphore.signal()
- throw LibraryError.deallocated(objectName: "HybridVideoPlayer").error()
- }
-
- do {
- initializedItem = try await strongSelf.initializePlayerItem()
- } catch {
- initializationError = error
- }
-
- semaphore.signal()
- }
-
- semaphore.wait() // Block current thread (playerQueue)
-
- if let error = initializationError, initializedItem == nil {
- throw error
- }
-
- return initializedItem!
- }
-
func initializePlayerItem() async throws -> AVPlayerItem {
// Ensure the source is a valid HybridVideoPlayerSource
guard let _hybridSource = source as? HybridVideoPlayerSource else {
status = .error
throw PlayerError.invalidSource.error()
}
-
+
// (maybe) Override source with plugins
- let _source = await PluginsRegistry.shared.overrideSource(source: _hybridSource)
+ let _source = await PluginsRegistry.shared.overrideSource(
+ source: _hybridSource
+ )
let isNetworkSource = _source.url.isFileURL == false
eventEmitter.onLoadStart(
- .init(sourceType: isNetworkSource ? .network : .local, source: _source))
-
+ .init(sourceType: isNetworkSource ? .network : .local, source: _source)
+ )
+
let asset = try await _source.getAsset()
let playerItem: AVPlayerItem
- if let externalSubtitles = source.config.externalSubtitles, externalSubtitles.isEmpty == false {
- playerItem = try await AVPlayerItem.withExternalSubtitles(for: asset, config: source.config)
+ if let externalSubtitles = source.config.externalSubtitles,
+ externalSubtitles.isEmpty == false
+ {
+ playerItem = try await AVPlayerItem.withExternalSubtitles(
+ for: asset,
+ config: source.config
+ )
} else {
playerItem = AVPlayerItem(asset: asset)
}
@@ -365,20 +310,24 @@ class HybridVideoPlayer: HybridVideoPlayerSpec, NativeVideoPlayerSpec {
// MARK: - Text Track Management
func getAvailableTextTracks() throws -> [TextTrack] {
- guard let currentItem = playerPointer.currentItem else {
+ guard let currentItem = player.currentItem else {
return []
}
var tracks: [TextTrack] = []
- if let mediaSelection = currentItem.asset.mediaSelectionGroup(forMediaCharacteristic: .legible)
- {
+ if let mediaSelection = currentItem.asset.mediaSelectionGroup(
+ forMediaCharacteristic: .legible
+ ) {
for (index, option) in mediaSelection.options.enumerated() {
let isSelected =
- currentItem.currentMediaSelection.selectedMediaOption(in: mediaSelection) == option
+ currentItem.currentMediaSelection.selectedMediaOption(
+ in: mediaSelection
+ ) == option
let name =
- option.commonMetadata.first(where: { $0.commonKey == .commonKeyTitle })?.stringValue
+ option.commonMetadata.first(where: { $0.commonKey == .commonKeyTitle }
+ )?.stringValue
?? option.displayName
let isExternal =
@@ -397,7 +346,8 @@ class HybridVideoPlayer: HybridVideoPlayerSpec, NativeVideoPlayerSpec {
label: option.displayName,
language: option.locale?.identifier,
selected: isSelected
- ))
+ )
+ )
}
}
@@ -405,16 +355,19 @@ class HybridVideoPlayer: HybridVideoPlayerSpec, NativeVideoPlayerSpec {
}
func selectTextTrack(textTrack: TextTrack?) throws {
- guard let currentItem = playerPointer.currentItem else {
+ guard let currentItem = player.currentItem else {
throw PlayerError.notInitialized.error()
}
guard
- let mediaSelection = currentItem.asset.mediaSelectionGroup(forMediaCharacteristic: .legible)
+ let mediaSelection = currentItem.asset.mediaSelectionGroup(
+ forMediaCharacteristic: .legible
+ )
else {
return
}
+ // If textTrack is nil, deselect any selected track
guard let textTrack = textTrack else {
currentItem.select(nil, in: mediaSelection)
selectedExternalTrackIndex = nil
@@ -422,6 +375,7 @@ class HybridVideoPlayer: HybridVideoPlayerSpec, NativeVideoPlayerSpec {
return
}
+ // If textTrack id is empty, deselect any selected track
if textTrack.id.isEmpty {
currentItem.select(nil, in: mediaSelection)
selectedExternalTrackIndex = nil
@@ -431,7 +385,9 @@ class HybridVideoPlayer: HybridVideoPlayerSpec, NativeVideoPlayerSpec {
if textTrack.id.hasPrefix("external-") {
let trackIndexStr = String(textTrack.id.dropFirst("external-".count))
- if let trackIndex = Int(trackIndexStr), trackIndex < mediaSelection.options.count {
+ if let trackIndex = Int(trackIndexStr),
+ trackIndex < mediaSelection.options.count
+ {
let option = mediaSelection.options[trackIndex]
currentItem.select(option, in: mediaSelection)
selectedExternalTrackIndex = trackIndex
@@ -439,7 +395,8 @@ class HybridVideoPlayer: HybridVideoPlayerSpec, NativeVideoPlayerSpec {
}
} else if textTrack.id.hasPrefix("builtin-") {
for option in mediaSelection.options {
- let optionId = "builtin-\(option.displayName)-\(option.locale?.identifier ?? "unknown")"
+ let optionId =
+ "builtin-\(option.displayName)-\(option.locale?.identifier ?? "unknown")"
if optionId == textTrack.id {
currentItem.select(option, in: mediaSelection)
selectedExternalTrackIndex = nil
@@ -451,23 +408,27 @@ class HybridVideoPlayer: HybridVideoPlayerSpec, NativeVideoPlayerSpec {
}
var selectedTrack: TextTrack? {
- guard let currentItem = playerPointer.currentItem else {
+ guard let currentItem = player.currentItem else {
return nil
}
guard
- let mediaSelection = currentItem.asset.mediaSelectionGroup(forMediaCharacteristic: .legible)
+ let mediaSelection = currentItem.asset.mediaSelectionGroup(
+ forMediaCharacteristic: .legible
+ )
else {
return nil
}
guard
- let selectedOption = currentItem.currentMediaSelection.selectedMediaOption(in: mediaSelection)
+ let selectedOption = currentItem.currentMediaSelection
+ .selectedMediaOption(in: mediaSelection)
else {
return nil
}
- guard let index = mediaSelection.options.firstIndex(of: selectedOption) else {
+ guard let index = mediaSelection.options.firstIndex(of: selectedOption)
+ else {
return nil
}
@@ -490,7 +451,7 @@ class HybridVideoPlayer: HybridVideoPlayerSpec, NativeVideoPlayerSpec {
}
// MARK: - Memory Management
-
+
func dispose() {
release()
}
diff --git a/packages/react-native-video/ios/hybrids/VideoPlayerSource/HybridVideoPlayerSourceFactory.swift b/packages/react-native-video/ios/hybrids/VideoPlayerSource/HybridVideoPlayerSourceFactory.swift
index 11629175..b21682e0 100644
--- a/packages/react-native-video/ios/hybrids/VideoPlayerSource/HybridVideoPlayerSourceFactory.swift
+++ b/packages/react-native-video/ios/hybrids/VideoPlayerSource/HybridVideoPlayerSourceFactory.swift
@@ -13,7 +13,7 @@ class HybridVideoPlayerSourceFactory: HybridVideoPlayerSourceFactorySpec {
}
func fromUri(uri: String) throws -> HybridVideoPlayerSourceSpec {
- let config = NativeVideoConfig(uri: uri, externalSubtitles: nil, drm: nil, headers: nil)
+ let config = NativeVideoConfig(uri: uri, externalSubtitles: nil, drm: nil, headers: nil, initializeOnCreation: true)
return try HybridVideoPlayerSource(config: config)
}
}
diff --git a/packages/react-native-video/ios/view/VideoComponentView.swift b/packages/react-native-video/ios/view/VideoComponentView.swift
index 97acd37e..3d7782c5 100644
--- a/packages/react-native-video/ios/view/VideoComponentView.swift
+++ b/packages/react-native-video/ios/view/VideoComponentView.swift
@@ -14,7 +14,7 @@ import AVKit
public weak var player: HybridVideoPlayerSpec? = nil {
didSet {
guard let player = player as? HybridVideoPlayer else { return }
- configureAVPlayerViewController(with: player.playerPointer)
+ configureAVPlayerViewController(with: player.player)
}
}
diff --git a/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerSpec.cpp b/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerSpec.cpp
index 37ca5a14..abcf94e6 100644
--- a/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerSpec.cpp
+++ b/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerSpec.cpp
@@ -208,6 +208,21 @@ namespace margelo::nitro::video {
static const auto method = javaClassStatic()->getMethod /* textTrack */)>("selectTextTrack");
method(_javaPart, textTrack.has_value() ? JTextTrack::fromCpp(textTrack.value()) : nullptr);
}
+ std::shared_ptr> JHybridVideoPlayerSpec::initialize() {
+ static const auto method = javaClassStatic()->getMethod()>("initialize");
+ auto __result = method(_javaPart);
+ return [&]() {
+ auto __promise = Promise::create();
+ __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& /* unit */) {
+ __promise->resolve();
+ });
+ __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) {
+ jni::JniException __jniError(__throwable);
+ __promise->reject(std::make_exception_ptr(__jniError));
+ });
+ return __promise;
+ }();
+ }
std::shared_ptr> JHybridVideoPlayerSpec::preload() {
static const auto method = javaClassStatic()->getMethod()>("preload");
auto __result = method(_javaPart);
diff --git a/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerSpec.hpp b/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerSpec.hpp
index 633262f9..5b6717ab 100644
--- a/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerSpec.hpp
+++ b/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerSpec.hpp
@@ -79,6 +79,7 @@ namespace margelo::nitro::video {
std::shared_ptr> replaceSourceAsync(const std::optional>& source) override;
std::vector getAvailableTextTracks() override;
void selectTextTrack(const std::optional& textTrack) override;
+ std::shared_ptr> initialize() override;
std::shared_ptr> preload() override;
void play() override;
void pause() override;
diff --git a/packages/react-native-video/nitrogen/generated/android/c++/JNativeVideoConfig.hpp b/packages/react-native-video/nitrogen/generated/android/c++/JNativeVideoConfig.hpp
index f5b7a3b1..2b3ef566 100644
--- a/packages/react-native-video/nitrogen/generated/android/c++/JNativeVideoConfig.hpp
+++ b/packages/react-native-video/nitrogen/generated/android/c++/JNativeVideoConfig.hpp
@@ -54,6 +54,8 @@ namespace margelo::nitro::video {
jni::local_ref drm = this->getFieldValue(fieldDrm);
static const auto fieldHeaders = clazz->getField>("headers");
jni::local_ref> headers = this->getFieldValue(fieldHeaders);
+ static const auto fieldInitializeOnCreation = clazz->getField("initializeOnCreation");
+ jni::local_ref initializeOnCreation = this->getFieldValue(fieldInitializeOnCreation);
return NativeVideoConfig(
uri->toStdString(),
externalSubtitles != nullptr ? std::make_optional([&]() {
@@ -74,7 +76,8 @@ namespace margelo::nitro::video {
__map.emplace(__entry.first->toStdString(), __entry.second->toStdString());
}
return __map;
- }()) : std::nullopt
+ }()) : std::nullopt,
+ initializeOnCreation != nullptr ? std::make_optional(static_cast(initializeOnCreation->value())) : std::nullopt
);
}
@@ -102,7 +105,8 @@ namespace margelo::nitro::video {
__map->put(jni::make_jstring(__entry.first), jni::make_jstring(__entry.second));
}
return __map;
- }() : nullptr
+ }() : nullptr,
+ value.initializeOnCreation.has_value() ? jni::JBoolean::valueOf(value.initializeOnCreation.value()) : nullptr
);
}
};
diff --git a/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/HybridVideoPlayerSpec.kt b/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/HybridVideoPlayerSpec.kt
index 7e7728aa..16fd90f6 100644
--- a/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/HybridVideoPlayerSpec.kt
+++ b/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/HybridVideoPlayerSpec.kt
@@ -128,6 +128,10 @@ abstract class HybridVideoPlayerSpec: HybridObject() {
@Keep
abstract fun selectTextTrack(textTrack: TextTrack?): Unit
+ @DoNotStrip
+ @Keep
+ abstract fun initialize(): Promise
+
@DoNotStrip
@Keep
abstract fun preload(): Promise
diff --git a/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/NativeVideoConfig.kt b/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/NativeVideoConfig.kt
index 68ae1c2c..7c685ef5 100644
--- a/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/NativeVideoConfig.kt
+++ b/packages/react-native-video/nitrogen/generated/android/kotlin/com/margelo/nitro/video/NativeVideoConfig.kt
@@ -32,7 +32,10 @@ data class NativeVideoConfig
val drm: NativeDrmParams?,
@DoNotStrip
@Keep
- val headers: Map?
+ val headers: Map?,
+ @DoNotStrip
+ @Keep
+ val initializeOnCreation: Boolean?
) {
/* main constructor */
}
diff --git a/packages/react-native-video/nitrogen/generated/ios/c++/HybridVideoPlayerSpecSwift.hpp b/packages/react-native-video/nitrogen/generated/ios/c++/HybridVideoPlayerSpecSwift.hpp
index 8e285a55..697dea35 100644
--- a/packages/react-native-video/nitrogen/generated/ios/c++/HybridVideoPlayerSpecSwift.hpp
+++ b/packages/react-native-video/nitrogen/generated/ios/c++/HybridVideoPlayerSpecSwift.hpp
@@ -177,6 +177,14 @@ namespace margelo::nitro::video {
std::rethrow_exception(__result.error());
}
}
+ inline std::shared_ptr> initialize() override {
+ auto __result = _swiftPart.initialize();
+ if (__result.hasError()) [[unlikely]] {
+ std::rethrow_exception(__result.error());
+ }
+ auto __value = std::move(__result.value());
+ return __value;
+ }
inline std::shared_ptr> preload() override {
auto __result = _swiftPart.preload();
if (__result.hasError()) [[unlikely]] {
diff --git a/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoPlayerSpec.swift b/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoPlayerSpec.swift
index 1b5ef107..6cc90962 100644
--- a/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoPlayerSpec.swift
+++ b/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoPlayerSpec.swift
@@ -31,6 +31,7 @@ public protocol HybridVideoPlayerSpec_protocol: HybridObject {
func replaceSourceAsync(source: (any HybridVideoPlayerSourceSpec)?) throws -> Promise
func getAvailableTextTracks() throws -> [TextTrack]
func selectTextTrack(textTrack: TextTrack?) throws -> Void
+ func initialize() throws -> Promise
func preload() throws -> Promise
func play() throws -> Void
func pause() throws -> Void
diff --git a/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoPlayerSpec_cxx.swift b/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoPlayerSpec_cxx.swift
index ede32db0..5d1f9741 100644
--- a/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoPlayerSpec_cxx.swift
+++ b/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoPlayerSpec_cxx.swift
@@ -324,6 +324,25 @@ open class HybridVideoPlayerSpec_cxx {
}
}
+ @inline(__always)
+ public final func initialize() -> bridge.Result_std__shared_ptr_Promise_void___ {
+ do {
+ let __result = try self.__implementation.initialize()
+ let __resultCpp = { () -> bridge.std__shared_ptr_Promise_void__ in
+ let __promise = bridge.create_std__shared_ptr_Promise_void__()
+ let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_void__(__promise)
+ __result
+ .then({ __result in __promiseHolder.resolve() })
+ .catch({ __error in __promiseHolder.reject(__error.toCpp()) })
+ return __promise
+ }()
+ return bridge.create_Result_std__shared_ptr_Promise_void___(__resultCpp)
+ } catch (let __error) {
+ let __exceptionPtr = __error.toCpp()
+ return bridge.create_Result_std__shared_ptr_Promise_void___(__exceptionPtr)
+ }
+ }
+
@inline(__always)
public final func preload() -> bridge.Result_std__shared_ptr_Promise_void___ {
do {
diff --git a/packages/react-native-video/nitrogen/generated/ios/swift/NativeVideoConfig.swift b/packages/react-native-video/nitrogen/generated/ios/swift/NativeVideoConfig.swift
index c68511bf..8544ea4d 100644
--- a/packages/react-native-video/nitrogen/generated/ios/swift/NativeVideoConfig.swift
+++ b/packages/react-native-video/nitrogen/generated/ios/swift/NativeVideoConfig.swift
@@ -18,7 +18,7 @@ public extension NativeVideoConfig {
/**
* Create a new instance of `NativeVideoConfig`.
*/
- init(uri: String, externalSubtitles: [NativeExternalSubtitle]?, drm: NativeDrmParams?, headers: Dictionary?) {
+ init(uri: String, externalSubtitles: [NativeExternalSubtitle]?, drm: NativeDrmParams?, headers: Dictionary?, initializeOnCreation: Bool?) {
self.init(std.string(uri), { () -> bridge.std__optional_std__vector_NativeExternalSubtitle__ in
if let __unwrappedValue = externalSubtitles {
return bridge.create_std__optional_std__vector_NativeExternalSubtitle__({ () -> bridge.std__vector_NativeExternalSubtitle_ in
@@ -49,6 +49,12 @@ public extension NativeVideoConfig {
} else {
return .init()
}
+ }(), { () -> bridge.std__optional_bool_ in
+ if let __unwrappedValue = initializeOnCreation {
+ return bridge.create_std__optional_bool_(__unwrappedValue)
+ } else {
+ return .init()
+ }
}())
}
@@ -151,4 +157,21 @@ public extension NativeVideoConfig {
}()
}
}
+
+ var initializeOnCreation: Bool? {
+ @inline(__always)
+ get {
+ return self.__initializeOnCreation.value
+ }
+ @inline(__always)
+ set {
+ self.__initializeOnCreation = { () -> bridge.std__optional_bool_ in
+ if let __unwrappedValue = newValue {
+ return bridge.create_std__optional_bool_(__unwrappedValue)
+ } else {
+ return .init()
+ }
+ }()
+ }
+ }
}
diff --git a/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoPlayerSpec.cpp b/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoPlayerSpec.cpp
index c6cc919b..95cd2edd 100644
--- a/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoPlayerSpec.cpp
+++ b/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoPlayerSpec.cpp
@@ -41,6 +41,7 @@ namespace margelo::nitro::video {
prototype.registerHybridMethod("replaceSourceAsync", &HybridVideoPlayerSpec::replaceSourceAsync);
prototype.registerHybridMethod("getAvailableTextTracks", &HybridVideoPlayerSpec::getAvailableTextTracks);
prototype.registerHybridMethod("selectTextTrack", &HybridVideoPlayerSpec::selectTextTrack);
+ prototype.registerHybridMethod("initialize", &HybridVideoPlayerSpec::initialize);
prototype.registerHybridMethod("preload", &HybridVideoPlayerSpec::preload);
prototype.registerHybridMethod("play", &HybridVideoPlayerSpec::play);
prototype.registerHybridMethod("pause", &HybridVideoPlayerSpec::pause);
diff --git a/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoPlayerSpec.hpp b/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoPlayerSpec.hpp
index 43f04724..41396d5e 100644
--- a/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoPlayerSpec.hpp
+++ b/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoPlayerSpec.hpp
@@ -94,6 +94,7 @@ namespace margelo::nitro::video {
virtual std::shared_ptr> replaceSourceAsync(const std::optional>& source) = 0;
virtual std::vector getAvailableTextTracks() = 0;
virtual void selectTextTrack(const std::optional& textTrack) = 0;
+ virtual std::shared_ptr> initialize() = 0;
virtual std::shared_ptr> preload() = 0;
virtual void play() = 0;
virtual void pause() = 0;
diff --git a/packages/react-native-video/nitrogen/generated/shared/c++/NativeVideoConfig.hpp b/packages/react-native-video/nitrogen/generated/shared/c++/NativeVideoConfig.hpp
index 70964113..c891ee21 100644
--- a/packages/react-native-video/nitrogen/generated/shared/c++/NativeVideoConfig.hpp
+++ b/packages/react-native-video/nitrogen/generated/shared/c++/NativeVideoConfig.hpp
@@ -41,10 +41,11 @@ namespace margelo::nitro::video {
std::optional> externalSubtitles SWIFT_PRIVATE;
std::optional drm SWIFT_PRIVATE;
std::optional> headers SWIFT_PRIVATE;
+ std::optional initializeOnCreation SWIFT_PRIVATE;
public:
NativeVideoConfig() = default;
- explicit NativeVideoConfig(std::string uri, std::optional> externalSubtitles, std::optional drm, std::optional> headers): uri(uri), externalSubtitles(externalSubtitles), drm(drm), headers(headers) {}
+ explicit NativeVideoConfig(std::string uri, std::optional> externalSubtitles, std::optional drm, std::optional> headers, std::optional initializeOnCreation): uri(uri), externalSubtitles(externalSubtitles), drm(drm), headers(headers), initializeOnCreation(initializeOnCreation) {}
};
} // namespace margelo::nitro::video
@@ -60,7 +61,8 @@ namespace margelo::nitro {
JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "uri")),
JSIConverter>>::fromJSI(runtime, obj.getProperty(runtime, "externalSubtitles")),
JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "drm")),
- JSIConverter>>::fromJSI(runtime, obj.getProperty(runtime, "headers"))
+ JSIConverter>>::fromJSI(runtime, obj.getProperty(runtime, "headers")),
+ JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "initializeOnCreation"))
);
}
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::video::NativeVideoConfig& arg) {
@@ -69,6 +71,7 @@ namespace margelo::nitro {
obj.setProperty(runtime, "externalSubtitles", JSIConverter>>::toJSI(runtime, arg.externalSubtitles));
obj.setProperty(runtime, "drm", JSIConverter>::toJSI(runtime, arg.drm));
obj.setProperty(runtime, "headers", JSIConverter>>::toJSI(runtime, arg.headers));
+ obj.setProperty(runtime, "initializeOnCreation", JSIConverter>::toJSI(runtime, arg.initializeOnCreation));
return obj;
}
static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
@@ -80,6 +83,7 @@ namespace margelo::nitro {
if (!JSIConverter>>::canConvert(runtime, obj.getProperty(runtime, "externalSubtitles"))) return false;
if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "drm"))) return false;
if (!JSIConverter>>::canConvert(runtime, obj.getProperty(runtime, "headers"))) return false;
+ if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "initializeOnCreation"))) return false;
return true;
}
};
diff --git a/packages/react-native-video/src/core/VideoPlayer.ts b/packages/react-native-video/src/core/VideoPlayer.ts
index 034bad86..850affb5 100644
--- a/packages/react-native-video/src/core/VideoPlayer.ts
+++ b/packages/react-native-video/src/core/VideoPlayer.ts
@@ -185,6 +185,12 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
return this.player.isPlaying;
}
+ async initialize(): Promise {
+ await this.wrapPromise(this.player.initialize());
+
+ NitroModules.updateMemorySize(this.player);
+ }
+
async preload(): Promise {
await this.wrapPromise(this.player.preload());
diff --git a/packages/react-native-video/src/core/types/VideoConfig.ts b/packages/react-native-video/src/core/types/VideoConfig.ts
index 14647e34..061a4dda 100644
--- a/packages/react-native-video/src/core/types/VideoConfig.ts
+++ b/packages/react-native-video/src/core/types/VideoConfig.ts
@@ -44,6 +44,13 @@ export type VideoConfig = {
* ```
*/
externalSubtitles?: ExternalSubtitle[];
+ /**
+ * when the player is created, this flag will determine if native player should be initialized immediately.
+ * If set to true, the player will be initialized as soon as player is created
+ * If set to false, the player will need be initialized manually later
+ * @default true
+ */
+ initializeOnCreation?: boolean;
};
// @internal
diff --git a/packages/react-native-video/src/core/types/VideoPlayerBase.ts b/packages/react-native-video/src/core/types/VideoPlayerBase.ts
index ae766ab7..8167877d 100644
--- a/packages/react-native-video/src/core/types/VideoPlayerBase.ts
+++ b/packages/react-native-video/src/core/types/VideoPlayerBase.ts
@@ -4,6 +4,9 @@ import type { TextTrack } from './TextTrack';
import type { VideoPlayerSourceBase } from './VideoPlayerSourceBase';
import type { VideoPlayerStatus } from './VideoPlayerStatus';
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+import type { VideoConfig } from './VideoConfig';
+
export interface VideoPlayerBase {
/**
* The source of the video.
@@ -105,6 +108,11 @@ export interface VideoPlayerBase {
*/
readonly isPlaying: boolean;
+ /**
+ * Manually initialize the player. You don't need to call this method manually, unless you set `initializeOnCreation` to false in {@link VideoConfig}
+ */
+ initialize(): Promise;
+
/**
* Preload the video.
* This is useful to avoid delay when the user plays the video.
diff --git a/packages/react-native-video/src/core/utils/playerFactory.ts b/packages/react-native-video/src/core/utils/playerFactory.ts
index 4eeedcd0..7efe0f46 100644
--- a/packages/react-native-video/src/core/utils/playerFactory.ts
+++ b/packages/react-native-video/src/core/utils/playerFactory.ts
@@ -6,6 +6,7 @@ import type {
import type { VideoPlayerSource } from '../../spec/nitro/VideoPlayerSource.nitro';
import type { VideoConfig, VideoSource } from '../types/VideoConfig';
import { createSource, isVideoPlayerSource } from './sourceFactory';
+import { tryParseNativeVideoError } from '../types/VideoError';
const VideoPlayerFactory =
NitroModules.createHybridObject('VideoPlayerFactory');
@@ -20,9 +21,13 @@ const VideoPlayerFactory =
export const createPlayer = (
source: VideoSource | VideoConfig | VideoPlayerSource
): VideoPlayer => {
- if (isVideoPlayerSource(source)) {
- return VideoPlayerFactory.createPlayer(source);
- }
+ try {
+ if (isVideoPlayerSource(source)) {
+ return VideoPlayerFactory.createPlayer(source);
+ }
- return VideoPlayerFactory.createPlayer(createSource(source));
+ return VideoPlayerFactory.createPlayer(createSource(source));
+ } catch (error) {
+ throw tryParseNativeVideoError(error);
+ }
};
diff --git a/packages/react-native-video/src/core/utils/sourceFactory.ts b/packages/react-native-video/src/core/utils/sourceFactory.ts
index 0d83c6e7..36f49fa2 100644
--- a/packages/react-native-video/src/core/utils/sourceFactory.ts
+++ b/packages/react-native-video/src/core/utils/sourceFactory.ts
@@ -72,6 +72,11 @@ export const createSourceFromVideoConfig = (
}
}
+ // Set default value for initializeOnCreation (true)
+ if (config.initializeOnCreation === undefined) {
+ config.initializeOnCreation = true;
+ }
+
try {
return VideoPlayerSourceFactory.fromVideoConfig(
config as NativeVideoConfig