From f929d56a87519b25bb3d782ba26ab4a3d3e5a7ec Mon Sep 17 00:00:00 2001 From: Krzysztof Moch Date: Mon, 1 Sep 2025 13:50:51 +0200 Subject: [PATCH] refactor: load player on initialization (#4673) --- bun.lock | 4 +- docs/docs/events/events.md | 4 + docs/docs/player/player-lifecycle.md | 48 ++- docs/docs/player/player.md | 10 +- example/ios/Podfile.lock | 4 +- example/src/App.tsx | 16 +- .../services/playback/VideoPlaybackService.kt | 4 +- .../hybrids/videoplayer/HybridVideoPlayer.kt | 155 +++++----- .../HybridVideoPlayerSourceFactory.kt | 2 +- .../ios/core/HLSSubtitleInjector.swift | 14 + .../ios/core/Spec/NativeVideoPlayerSpec.swift | 5 +- .../ios/core/VideoManager.swift | 12 +- .../HybridVideoPlayer+Events.swift | 6 +- .../VideoPlayer/HybridVideoPlayer.swift | 275 ++++++++---------- .../HybridVideoPlayerSourceFactory.swift | 2 +- .../ios/view/VideoComponentView.swift | 2 +- .../android/c++/JHybridVideoPlayerSpec.cpp | 15 + .../android/c++/JHybridVideoPlayerSpec.hpp | 1 + .../android/c++/JNativeVideoConfig.hpp | 8 +- .../nitro/video/HybridVideoPlayerSpec.kt | 4 + .../margelo/nitro/video/NativeVideoConfig.kt | 5 +- .../ios/c++/HybridVideoPlayerSpecSwift.hpp | 8 + .../ios/swift/HybridVideoPlayerSpec.swift | 1 + .../ios/swift/HybridVideoPlayerSpec_cxx.swift | 19 ++ .../ios/swift/NativeVideoConfig.swift | 25 +- .../shared/c++/HybridVideoPlayerSpec.cpp | 1 + .../shared/c++/HybridVideoPlayerSpec.hpp | 1 + .../shared/c++/NativeVideoConfig.hpp | 8 +- .../src/core/VideoPlayer.ts | 6 + .../src/core/types/VideoConfig.ts | 7 + .../src/core/types/VideoPlayerBase.ts | 8 + .../src/core/utils/playerFactory.ts | 13 +- .../src/core/utils/sourceFactory.ts | 5 + 33 files changed, 411 insertions(+), 287 deletions(-) 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