feat: refactor player initialization

This commit is contained in:
Krzysztof Moch
2025-01-18 23:56:02 +01:00
parent cefee73076
commit 8de0a93b8b
10 changed files with 203 additions and 88 deletions
+4 -4
View File
@@ -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()
}
@@ -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<Unit> {
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<Unit> {
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()
}
@@ -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)
+102 -19
View File
@@ -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<Void> {
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<Void> {
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)
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
+1 -3
View File
@@ -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)
}
}
+18 -4
View File
@@ -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<void>;
/**
* 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<void>;
}
export interface VideoPlayerFactory