From 8de0a93b8bcd31e20a758288b559fac73b8f1cc0 Mon Sep 17 00:00:00 2001 From: Krzysztof Moch Date: Sat, 18 Jan 2025 23:56:02 +0100 Subject: [PATCH] feat: refactor player initialization --- android/src/main/java/com/video/VideoView.kt | 8 +- .../com/video/hybrids/HybridVideoPlayer.kt | 98 ++++++++++---- .../video/hybrids/HybridVideoPlayerFactory.kt | 1 - ios/hybrids/HybridVideoPlayer.swift | 121 +++++++++++++++--- ios/hybrids/HybridVideoPlayerFactory.swift | 8 -- .../HybridVideoPlayerSourceFactory.swift | 13 +- ios/hybrids/HybridVideoViewViewManager.swift | 8 -- .../HybridVideoViewViewManagerFactory.swift | 8 -- ios/view/VideoComponentView.swift | 4 +- src/spec/nitro/VideoPlayer.nitro.ts | 22 +++- 10 files changed, 203 insertions(+), 88 deletions(-) diff --git a/android/src/main/java/com/video/VideoView.kt b/android/src/main/java/com/video/VideoView.kt index c2db7ced..cf24b04c 100644 --- a/android/src/main/java/com/video/VideoView.kt +++ b/android/src/main/java/com/video/VideoView.kt @@ -18,7 +18,7 @@ class VideoView @JvmOverloads constructor( set(value) { // Clear the SurfaceView when player is about to be set to null if (value == null && field != null) { - val player = field?.player + val player = field?.playerPointer player?.clearVideoSurfaceView(surfaceView) player?.setVideoSurfaceView(null) } @@ -27,7 +27,7 @@ class VideoView @JvmOverloads constructor( // Set the SurfaceView to the player when it's available surfaceView?.let { - field?.player?.setVideoSurfaceView(it) + field?.playerPointer?.setVideoSurfaceView(it) } } @@ -56,12 +56,12 @@ class VideoView @JvmOverloads constructor( // -------- View Lifecycle Methods -------- override fun onDetachedFromWindow() { - hybridPlayer?.player?.clearVideoSurfaceView(surfaceView) + hybridPlayer?.playerPointer?.clearVideoSurfaceView(surfaceView) super.onDetachedFromWindow() } override fun onAttachedToWindow() { - hybridPlayer?.player?.setVideoSurfaceView(surfaceView) + hybridPlayer?.playerPointer?.setVideoSurfaceView(surfaceView) super.onAttachedToWindow() } diff --git a/android/src/main/java/com/video/hybrids/HybridVideoPlayer.kt b/android/src/main/java/com/video/hybrids/HybridVideoPlayer.kt index 4451e96f..362615bf 100644 --- a/android/src/main/java/com/video/hybrids/HybridVideoPlayer.kt +++ b/android/src/main/java/com/video/hybrids/HybridVideoPlayer.kt @@ -9,74 +9,128 @@ import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.upstream.DefaultAllocator import com.facebook.proguard.annotations.DoNotStrip import com.margelo.nitro.NitroModules +import com.margelo.nitro.core.Promise import com.video.utils.Threading.Companion.runOnMainThreadSync import com.video.utils.Threading.Companion.runOnMainThread @UnstableApi @DoNotStrip class HybridVideoPlayer() : HybridVideoPlayerSpec() { - lateinit var player: Player - private lateinit var allocator: DefaultAllocator override lateinit var source: HybridVideoPlayerSourceSpec + private var allocator: DefaultAllocator? = null + + private var player: Player? = null + + var playerPointer: Player + get() { + if (player == null) { + runOnMainThreadSync { + initializePlayer() + + if (player == null) { + throw Exception("Could not initialize player!") + } + + if (player!!.playbackState == Player.STATE_IDLE) { + player!!.prepare() + } + } + } + + return player!! + } + private set(value) { + player = value + } // Player Properties override var currentTime: Double - get() = runOnMainThreadSync { return@runOnMainThreadSync player.currentPosition.toDouble() } - set(value) = runOnMainThread { player.seekTo(value.toLong()) } + get() = runOnMainThreadSync { return@runOnMainThreadSync playerPointer.currentPosition.toDouble() / 1000.0 } + set(value) = runOnMainThread { playerPointer.seekTo((value * 1000).toLong()) } override var volume: Double - get() = runOnMainThreadSync { return@runOnMainThreadSync player.volume.toDouble() } - set(value) = runOnMainThread { player.volume = value.toFloat() } + get() = runOnMainThreadSync { return@runOnMainThreadSync playerPointer.volume.toDouble() } + set(value) = runOnMainThread { playerPointer.volume = value.toFloat() } override val duration: Double - get() = runOnMainThreadSync { return@runOnMainThreadSync player.duration.toDouble() } - - private fun initializePlayerFromSource(source: HybridVideoPlayerSource) { - this.source = source + get() { + val duration = runOnMainThreadSync { return@runOnMainThreadSync playerPointer.duration } + return if (duration == C.TIME_UNSET) Double.NaN else duration.toDouble() / 1000.0 + } + private fun initializePlayer() { if (NitroModules.applicationContext == null) { throw Exception("HybridVideoPlayer: Application Context is null!") } + val hybridSource = source as? HybridVideoPlayerSource ?: throw Exception("Invalid source type") + // Initialize the allocator allocator = DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE) // Create a LoadControl with the allocator val loadControl = DefaultLoadControl.Builder() - .setAllocator(allocator) + .setAllocator(allocator!!) .build() // Build the player with the LoadControl - player = ExoPlayer.Builder(NitroModules.applicationContext!!) + playerPointer = ExoPlayer.Builder(NitroModules.applicationContext!!) .setLoadControl(loadControl) .setLooper(Looper.getMainLooper()) .build() - player.setMediaItem(source.mediaItem) - - // TODO: Move this to single method to allow more control - player.prepare() + playerPointer.setMediaItem(hybridSource.mediaItem) } constructor(source: HybridVideoPlayerSource) : this() { - runOnMainThreadSync { - initializePlayerFromSource(source) - } + this.source = source } override fun play() { runOnMainThread { - player.play() + playerPointer.play() } } override fun pause() { runOnMainThread { - player.pause() + playerPointer.pause() + } + } + + override fun replaceSourceAsync(source: HybridVideoPlayerSourceSpec): Promise { + return Promise.async { + val hybridSource = source as? HybridVideoPlayerSource ?: throw Exception("Invalid source type") + + runOnMainThreadSync { + // Update source + this.source = source + playerPointer.setMediaItem(hybridSource.mediaItem) + + // Prepare player + playerPointer.prepare() + } + } + } + + override fun preload(): Promise { + return Promise.async { + runOnMainThreadSync { + if (playerPointer.playbackState != Player.STATE_IDLE) { + return@runOnMainThreadSync + } + + if (player == null) { + initializePlayer() + } + + player?.prepare() + } } } // Updated memorySize property override val memorySize: Long - get() = allocator.totalBytesAllocated.toLong() + // If player is null, allocator is not ready yet + get() = if (allocator == null) 0 else allocator!!.totalBytesAllocated.toLong() } diff --git a/android/src/main/java/com/video/hybrids/HybridVideoPlayerFactory.kt b/android/src/main/java/com/video/hybrids/HybridVideoPlayerFactory.kt index 46ea57ff..edf00a06 100644 --- a/android/src/main/java/com/video/hybrids/HybridVideoPlayerFactory.kt +++ b/android/src/main/java/com/video/hybrids/HybridVideoPlayerFactory.kt @@ -6,7 +6,6 @@ import com.facebook.proguard.annotations.DoNotStrip @DoNotStrip class HybridVideoPlayerFactory(): HybridVideoPlayerFactorySpec() { - @OptIn(UnstableApi::class) override fun createPlayer(source: HybridVideoPlayerSourceSpec): HybridVideoPlayerSpec { return HybridVideoPlayer(source as HybridVideoPlayerSource) diff --git a/ios/hybrids/HybridVideoPlayer.swift b/ios/hybrids/HybridVideoPlayer.swift index b9118030..35760df4 100644 --- a/ios/hybrids/HybridVideoPlayer.swift +++ b/ios/hybrids/HybridVideoPlayer.swift @@ -10,59 +10,142 @@ import NitroModules import AVFoundation class HybridVideoPlayer: HybridVideoPlayerSpec { - var player: AVPlayer + /** + * This in general should not be used directly, use `playerPointer` instead. This should be set only from within the playerQueue. + */ + private var _player: AVPlayer? + + /** + * The player queue is used to synchronize player initialization. + */ + private let playerQueue = DispatchQueue(label: "com.nitro.hybridplayer") + + /** + * 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 initialization + playerQueue.sync { + // If player is already initialized and playerItem is set, return it + // If playerItem is not set, it means that player was not loaded with any source + if _player != nil && playerItem != nil { + return _player! + } + + do { + let item = try initializePlayerItem() + _player?.replaceCurrentItem(with: item) + playerItem = item + _player = AVPlayer(playerItem: playerItem) + } catch { + playerItem = nil + _player = AVPlayer() + } + + return _player! + } + } + set { + playerQueue.sync { + _player = newValue + } + } + } + + var playerItem: AVPlayerItem? var source: any HybridVideoPlayerSourceSpec var volume: Double { set { - player.volume = Float(newValue) + playerPointer.volume = Float(newValue) } get { - return Double(player.volume) + return Double(playerPointer.volume) } } var currentTime: Double { set { - player.seek( + playerPointer.seek( to: CMTime(seconds: newValue, preferredTimescale: 1000), toleranceBefore: .zero, toleranceAfter: .zero ) } get { - player.currentTime().seconds + playerPointer.currentTime().seconds } } var duration: Double { - Double(player.currentItem?.duration.seconds ?? Double.nan) + Double(playerPointer.currentItem?.duration.seconds ?? Double.nan) } - init(source: HybridVideoPlayerSourceSpec) throws { + init(source: (any HybridVideoPlayerSourceSpec)) throws { self.source = source - - guard let url = URL(string: source.uri) else { - throw RuntimeError.error(withMessage: "Invalid URL: \(source.uri)") + super.init() + } + + func preload() throws -> NitroModules.Promise { + return Promise.parallel { [weak self] in + guard let self else { + throw RuntimeError.error(withMessage: "HybridVideoPlayer has been deallocated") + } + + try self.playerQueue.sync { + self.playerItem = try self.initializePlayerItem() + self._player = AVPlayer(playerItem: self.playerItem) + } } - - player = AVPlayer(url: url) } func play() throws { - player.play() + playerPointer.play() } func pause() throws { - player.pause() + playerPointer.pause() } - // Initialize HybridContext - var hybridContext = margelo.nitro.HybridContext() + func replaceSourceAsync(source: (any HybridVideoPlayerSourceSpec)) throws -> Promise { + return Promise.parallel { [weak self] in + guard let self else { + throw RuntimeError.error(withMessage: "HybridVideoPlayer has been deallocated") + } + + try playerQueue.sync { + self.source = source + + do { + self.playerItem = try self.initializePlayerItem() + + guard let player = self._player else { + throw RuntimeError.error(withMessage: "Player not initialized") + } + + player.replaceCurrentItem(with: self.playerItem) + } catch { + self.playerItem = nil + self._player = AVPlayer() + throw error + } + } + } + } - // Return size of the instance to inform JS GC about memory pressure - var memorySize: Int { - return getSizeOf(self) + private func initializePlayerItem() throws -> AVPlayerItem { + guard let _source = source as? HybridVideoPlayerSource else { + throw RuntimeError.error(withMessage: "Invalid source") + } + + try _source.initializeAsset() + + guard let asset = _source.asset else { + throw RuntimeError.error(withMessage: "Failed to initialize asset") + } + + return AVPlayerItem(asset: asset) } } diff --git a/ios/hybrids/HybridVideoPlayerFactory.swift b/ios/hybrids/HybridVideoPlayerFactory.swift index af479081..d625cab0 100644 --- a/ios/hybrids/HybridVideoPlayerFactory.swift +++ b/ios/hybrids/HybridVideoPlayerFactory.swift @@ -12,12 +12,4 @@ class HybridVideoPlayerFactory: HybridVideoPlayerFactorySpec { func createPlayer(source: HybridVideoPlayerSourceSpec) throws -> HybridVideoPlayerSpec { return try HybridVideoPlayer(source: source) } - - // Initialize HybridContext - var hybridContext = margelo.nitro.HybridContext() - - // Return size of the instance to inform JS GC about memory pressure - var memorySize: Int { - return getSizeOf(self) - } } diff --git a/ios/hybrids/HybridVideoPlayerSourceFactory.swift b/ios/hybrids/HybridVideoPlayerSourceFactory.swift index a6b41f3d..806bef25 100644 --- a/ios/hybrids/HybridVideoPlayerSourceFactory.swift +++ b/ios/hybrids/HybridVideoPlayerSourceFactory.swift @@ -8,16 +8,7 @@ import Foundation class HybridVideoPlayerSourceFactory: HybridVideoPlayerSourceFactorySpec { - func fromUri(uri: String) -> HybridVideoPlayerSourceSpec { - return HybridVideoPlayerSource(uri: uri) - } - - - // Initialize HybridContext - var hybridContext = margelo.nitro.HybridContext() - - // Return size of the instance to inform JS GC about memory pressure - var memorySize: Int { - return getSizeOf(self) + func fromUri(uri: String) throws -> HybridVideoPlayerSourceSpec { + return try HybridVideoPlayerSource(uri: uri) } } diff --git a/ios/hybrids/HybridVideoViewViewManager.swift b/ios/hybrids/HybridVideoViewViewManager.swift index 9662106b..95ee94b7 100644 --- a/ios/hybrids/HybridVideoViewViewManager.swift +++ b/ios/hybrids/HybridVideoViewViewManager.swift @@ -35,12 +35,4 @@ class HybridVideoViewViewManager: HybridVideoViewViewManagerSpec { self.view = view } - - // Initialize HybridContext - var hybridContext = margelo.nitro.HybridContext() - - // Return size of the instance to inform JS GC about memory pressure - var memorySize: Int { - return getSizeOf(self) - } } diff --git a/ios/hybrids/HybridVideoViewViewManagerFactory.swift b/ios/hybrids/HybridVideoViewViewManagerFactory.swift index 8fac918c..7d59e983 100644 --- a/ios/hybrids/HybridVideoViewViewManagerFactory.swift +++ b/ios/hybrids/HybridVideoViewViewManagerFactory.swift @@ -11,12 +11,4 @@ class HybridVideoViewViewManagerFactory: HybridVideoViewViewManagerFactorySpec { func createViewManager(nitroId: Double) throws -> any HybridVideoViewViewManagerSpec { return try HybridVideoViewViewManager(nitroId: nitroId) } - - // Initialize HybridContext - var hybridContext = margelo.nitro.HybridContext() - - // Return size of the instance to inform JS GC about memory pressure - var memorySize: Int { - return getSizeOf(self) - } } diff --git a/ios/view/VideoComponentView.swift b/ios/view/VideoComponentView.swift index 549a28dc..42215b59 100644 --- a/ios/view/VideoComponentView.swift +++ b/ios/view/VideoComponentView.swift @@ -13,16 +13,14 @@ import AVFoundation public var player: HybridVideoPlayerSpec? = nil { didSet { guard let player = player as? HybridVideoPlayer else { return } - configureAVPlayerLayer(with: player.player) + configureAVPlayerLayer(with: player.playerPointer) } } private var playerView: UIView? = nil - private var avPlayer: AVPlayer? private var avPlayerLayer: AVPlayerLayer? @objc public var nitroId: NSNumber = -1 { didSet { - let test = nitroId VideoComponentView.globalViewsMap.setObject(self, forKey: nitroId) } } diff --git a/src/spec/nitro/VideoPlayer.nitro.ts b/src/spec/nitro/VideoPlayer.nitro.ts index a596cd59..a9d39d15 100644 --- a/src/spec/nitro/VideoPlayer.nitro.ts +++ b/src/spec/nitro/VideoPlayer.nitro.ts @@ -8,7 +8,13 @@ export interface VideoPlayer * Changing the source will reload player. * see {@link VideoPlayerSource} */ - source: VideoPlayerSource; + readonly source: VideoPlayerSource; + + /** + * The current time of the video in seconds (1.0 = 1 sec). + * Returns NaN if the current time is not available. + */ + readonly duration: number; /** * The volume of the video (0.0 = 0%, 1.0 = 100%). @@ -22,10 +28,11 @@ export interface VideoPlayer currentTime: number; /** - * The current time of the video in seconds (1.0 = 1 sec). - * Returns NaN if the current time is not available. + * Preload the video. + * This is useful to avoid delay when the user plays the video. + * Preloading too many videos can lead to memory issues or performance issues. */ - readonly duration: number; + preload(): Promise; /** * Start playback of player. @@ -36,6 +43,13 @@ export interface VideoPlayer * Pause playback of player. */ pause(): void; + + /** + * Replace the current source of the player. + * @param source - The new source of the video. + * see {@link VideoPlayerSource} + */ + replaceSourceAsync(source: VideoPlayerSource): Promise; } export interface VideoPlayerFactory