From ff882c23d08f08b8f34723966a1a4b853e57e3a4 Mon Sep 17 00:00:00 2001 From: Krzysztof Moch Date: Fri, 19 Dec 2025 13:19:46 +0100 Subject: [PATCH] refactor: don't use nitro dispose (#4802) --- bun.lock | 6 +-- example/ios/Podfile.lock | 8 ++-- .../android/c++/JHybridPluginManagerSpec.cpp | 6 +++ .../android/c++/JHybridPluginManagerSpec.hpp | 1 + .../nitro/videodrm/HybridPluginManagerSpec.kt | 7 ++- .../ReactNativeVideoDrm-Swift-Cxx-Bridge.cpp | 1 + .../ios/c++/HybridPluginManagerSpecSwift.hpp | 3 ++ .../ios/swift/HybridPluginManagerSpec.swift | 7 +++ .../swift/HybridPluginManagerSpec_cxx.swift | 10 ++++- .../java/com/twg/video/core/VideoManager.kt | 10 ++++- .../hybrids/videoplayer/HybridVideoPlayer.kt | 13 ++++-- .../HybridVideoPlayerEventEmitter.kt | 30 +++++++++---- .../HybridVideoPlayer+Events.swift | 2 +- .../VideoPlayer/HybridVideoPlayer.swift | 3 ++ .../android/c++/JHybridVideoPlayerSpec.cpp | 4 ++ .../android/c++/JHybridVideoPlayerSpec.hpp | 1 + .../nitro/video/HybridVideoPlayerSpec.kt | 4 ++ .../ios/c++/HybridVideoPlayerSpecSwift.hpp | 6 +++ .../ios/swift/HybridVideoPlayerSpec.swift | 1 + .../ios/swift/HybridVideoPlayerSpec_cxx.swift | 11 +++++ .../shared/c++/HybridVideoPlayerSpec.cpp | 1 + .../shared/c++/HybridVideoPlayerSpec.hpp | 1 + .../src/core/VideoPlayer.ts | 44 ++++++++++++++++--- .../src/spec/nitro/VideoPlayer.nitro.ts | 11 +++++ 24 files changed, 164 insertions(+), 27 deletions(-) diff --git a/bun.lock b/bun.lock index 3fa372ae..152b10e4 100644 --- a/bun.lock +++ b/bun.lock @@ -52,7 +52,7 @@ }, "example": { "name": "react-native-video-example", - "version": "7.0.0-beta.0", + "version": "7.0.0-beta.1", "dependencies": { "@react-native-community/slider": "^4.5.6", "@react-native-video/drm": "*", @@ -80,7 +80,7 @@ }, "packages/drm-plugin": { "name": "@react-native-video/drm", - "version": "7.0.0-beta.0", + "version": "7.0.0-beta.1", "devDependencies": { "@react-native/babel-preset": "0.79.2", "@release-it/conventional-changelog": "^9.0.2", @@ -108,7 +108,7 @@ }, "packages/react-native-video": { "name": "react-native-video", - "version": "7.0.0-beta.0", + "version": "7.0.0-beta.1", "devDependencies": { "@expo/config-plugins": "^10.0.2", "@react-native/eslint-config": "^0.77.0", diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index b11621a2..c1807acc 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1565,7 +1565,7 @@ PODS: - React-logger (= 0.77.3) - React-perflogger (= 0.77.3) - React-utils (= 0.77.3) - - ReactNativeVideo (7.0.0-beta.0): + - ReactNativeVideo (7.0.0-beta.1): - DoubleConversion - glog - hermes-engine @@ -1587,7 +1587,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - ReactNativeVideoDrm (7.0.0-beta.0): + - ReactNativeVideoDrm (7.0.0-beta.1): - DoubleConversion - glog - hermes-engine @@ -1904,8 +1904,8 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: 31015410a4a53b9fd0a908ad4d6e3e2b9a25086a ReactCodegen: 53316394e985ded1babc7f143c90c77d2bb1b43c ReactCommon: bf4612cba0fa356b529385029f470d5529dddde4 - ReactNativeVideo: 5a5e609057e980e9ea2736914377804358c53ae9 - ReactNativeVideoDrm: 4f266c3b018170319ed16bc511218c0d411358d5 + ReactNativeVideo: 10dd0a47f8228b41565a3efb13df5b323633b590 + ReactNativeVideoDrm: 07b826ab66fd0a00ab3dd3bef2dd2c026e919a9a SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: 92f3bb322c40a86b7233b815854730442e01b8c4 diff --git a/packages/drm-plugin/nitrogen/generated/android/c++/JHybridPluginManagerSpec.cpp b/packages/drm-plugin/nitrogen/generated/android/c++/JHybridPluginManagerSpec.cpp index 3db3d032..8926322d 100644 --- a/packages/drm-plugin/nitrogen/generated/android/c++/JHybridPluginManagerSpec.cpp +++ b/packages/drm-plugin/nitrogen/generated/android/c++/JHybridPluginManagerSpec.cpp @@ -33,6 +33,12 @@ namespace margelo::nitro::videodrm { method(_javaPart); } + std::string JHybridPluginManagerSpec::toString() { + static const auto method = javaClassStatic()->getMethod("toString"); + auto javaString = method(_javaPart); + return javaString->toStdString(); + } + // Properties bool JHybridPluginManagerSpec::getIsEnabled() { static const auto method = javaClassStatic()->getMethod("isEnabled"); diff --git a/packages/drm-plugin/nitrogen/generated/android/c++/JHybridPluginManagerSpec.hpp b/packages/drm-plugin/nitrogen/generated/android/c++/JHybridPluginManagerSpec.hpp index 8e23a069..31372eb8 100644 --- a/packages/drm-plugin/nitrogen/generated/android/c++/JHybridPluginManagerSpec.hpp +++ b/packages/drm-plugin/nitrogen/generated/android/c++/JHybridPluginManagerSpec.hpp @@ -41,6 +41,7 @@ namespace margelo::nitro::videodrm { public: size_t getExternalMemorySize() noexcept override; void dispose() noexcept override; + std::string toString() override; public: inline const jni::global_ref& getJavaPart() const noexcept { diff --git a/packages/drm-plugin/nitrogen/generated/android/kotlin/com/margelo/nitro/videodrm/HybridPluginManagerSpec.kt b/packages/drm-plugin/nitrogen/generated/android/kotlin/com/margelo/nitro/videodrm/HybridPluginManagerSpec.kt index 8c792ef3..3657adec 100644 --- a/packages/drm-plugin/nitrogen/generated/android/kotlin/com/margelo/nitro/videodrm/HybridPluginManagerSpec.kt +++ b/packages/drm-plugin/nitrogen/generated/android/kotlin/com/margelo/nitro/videodrm/HybridPluginManagerSpec.kt @@ -10,7 +10,7 @@ package com.margelo.nitro.videodrm import androidx.annotation.Keep import com.facebook.jni.HybridData import com.facebook.proguard.annotations.DoNotStrip -import com.margelo.nitro.core.* +import com.margelo.nitro.core.HybridObject /** * A Kotlin class representing the PluginManager HybridObject. @@ -36,6 +36,11 @@ abstract class HybridPluginManagerSpec: HybridObject() { super.updateNative(hybridData) } + // Default implementation of `HybridObject.toString()` + override fun toString(): String { + return "[HybridObject PluginManager]" + } + // Properties @get:DoNotStrip @get:Keep diff --git a/packages/drm-plugin/nitrogen/generated/ios/ReactNativeVideoDrm-Swift-Cxx-Bridge.cpp b/packages/drm-plugin/nitrogen/generated/ios/ReactNativeVideoDrm-Swift-Cxx-Bridge.cpp index ab74af40..22d47811 100644 --- a/packages/drm-plugin/nitrogen/generated/ios/ReactNativeVideoDrm-Swift-Cxx-Bridge.cpp +++ b/packages/drm-plugin/nitrogen/generated/ios/ReactNativeVideoDrm-Swift-Cxx-Bridge.cpp @@ -10,6 +10,7 @@ // Include C++ implementation defined types #include "HybridPluginManagerSpecSwift.hpp" #include "ReactNativeVideoDrm-Swift-Cxx-Umbrella.hpp" +#include namespace margelo::nitro::videodrm::bridge::swift { diff --git a/packages/drm-plugin/nitrogen/generated/ios/c++/HybridPluginManagerSpecSwift.hpp b/packages/drm-plugin/nitrogen/generated/ios/c++/HybridPluginManagerSpecSwift.hpp index 595efbc7..0683ef30 100644 --- a/packages/drm-plugin/nitrogen/generated/ios/c++/HybridPluginManagerSpecSwift.hpp +++ b/packages/drm-plugin/nitrogen/generated/ios/c++/HybridPluginManagerSpecSwift.hpp @@ -50,6 +50,9 @@ namespace margelo::nitro::videodrm { void dispose() noexcept override { _swiftPart.dispose(); } + std::string toString() override { + return _swiftPart.toString(); + } public: // Properties diff --git a/packages/drm-plugin/nitrogen/generated/ios/swift/HybridPluginManagerSpec.swift b/packages/drm-plugin/nitrogen/generated/ios/swift/HybridPluginManagerSpec.swift index 46f5983b..0960e0eb 100644 --- a/packages/drm-plugin/nitrogen/generated/ios/swift/HybridPluginManagerSpec.swift +++ b/packages/drm-plugin/nitrogen/generated/ios/swift/HybridPluginManagerSpec.swift @@ -18,6 +18,13 @@ public protocol HybridPluginManagerSpec_protocol: HybridObject { func disable() throws -> Void } +public extension HybridPluginManagerSpec_protocol { + /// Default implementation of ``HybridObject.toString`` + func toString() -> String { + return "[HybridObject PluginManager]" + } +} + /// See ``HybridPluginManagerSpec`` open class HybridPluginManagerSpec_base { private weak var cxxWrapper: HybridPluginManagerSpec_cxx? = nil diff --git a/packages/drm-plugin/nitrogen/generated/ios/swift/HybridPluginManagerSpec_cxx.swift b/packages/drm-plugin/nitrogen/generated/ios/swift/HybridPluginManagerSpec_cxx.swift index 5357d15b..7c050884 100644 --- a/packages/drm-plugin/nitrogen/generated/ios/swift/HybridPluginManagerSpec_cxx.swift +++ b/packages/drm-plugin/nitrogen/generated/ios/swift/HybridPluginManagerSpec_cxx.swift @@ -76,7 +76,7 @@ open class HybridPluginManagerSpec_cxx { */ public func getCxxPart() -> bridge.std__shared_ptr_HybridPluginManagerSpec_ { let cachedCxxPart = self.__cxxPart.lock() - if cachedCxxPart.__convertToBool() { + if Bool(fromCxx: cachedCxxPart) { return cachedCxxPart } else { let newCxxPart = bridge.create_std__shared_ptr_HybridPluginManagerSpec_(self.toUnsafe()) @@ -105,6 +105,14 @@ open class HybridPluginManagerSpec_cxx { self.__implementation.dispose() } + /** + * Call toString() on the Swift class. + */ + @inline(__always) + public func toString() -> String { + return self.__implementation.toString() + } + // Properties public final var isEnabled: Bool { @inline(__always) diff --git a/packages/react-native-video/android/src/main/java/com/twg/video/core/VideoManager.kt b/packages/react-native-video/android/src/main/java/com/twg/video/core/VideoManager.kt index cbb9a54d..c8bbca65 100644 --- a/packages/react-native-video/android/src/main/java/com/twg/video/core/VideoManager.kt +++ b/packages/react-native-video/android/src/main/java/com/twg/video/core/VideoManager.kt @@ -170,9 +170,17 @@ object VideoManager : LifecycleEventListener { } fun unregisterPlayer(player: HybridVideoPlayer) { - players.remove(player) audioFocusManager.unregisterPlayer(player) PluginsRegistry.shared.notifyPlayerDestroyed(WeakReference(player)) + + // Remove player from any views that were using it + players[player]?.forEach { nitroId -> + views[nitroId]?.get()?.let { view -> + view.hybridPlayer = null + } + } + + players.remove(player) } fun getPlayerByNitroId(nitroId: Int): HybridVideoPlayer? { 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 f3b3c8be..25a99711 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 @@ -43,7 +43,7 @@ import kotlin.math.max @UnstableApi @DoNotStrip -class HybridVideoPlayer() : HybridVideoPlayerSpec() { +class HybridVideoPlayer() : HybridVideoPlayerSpec(), AutoCloseable { override lateinit var source: HybridVideoPlayerSourceSpec override var eventEmitter = HybridVideoPlayerEventEmitter() set(value) { @@ -348,7 +348,7 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() { } } - private fun release() { + override fun release() { if (playInBackground || showNotificationControls) { VideoPlaybackService.stopService(this, videoPlaybackServiceConnection) } @@ -358,6 +358,8 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() { stopProgressUpdates() loadedWithSource = false + eventEmitter.clearAllListeners() + player.removeListener(playerListener) player.removeAnalyticsListener(analyticsListener) player.release() // Release player @@ -384,8 +386,13 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() { release() } + override fun close() { + release() + } + override val memorySize: Long - get() = allocator?.totalBytesAllocated?.toLong() ?: 0L + // 1 MiB by default + get() = allocator?.totalBytesAllocated?.toLong() ?: (1024L * 1024L) private fun startProgressUpdates() { stopProgressUpdates() // Ensure no multiple runnables diff --git a/packages/react-native-video/android/src/main/java/com/twg/video/hybrids/videoplayereventemitter/HybridVideoPlayerEventEmitter.kt b/packages/react-native-video/android/src/main/java/com/twg/video/hybrids/videoplayereventemitter/HybridVideoPlayerEventEmitter.kt index 45c6c41d..ecdcbf6a 100644 --- a/packages/react-native-video/android/src/main/java/com/twg/video/hybrids/videoplayereventemitter/HybridVideoPlayerEventEmitter.kt +++ b/packages/react-native-video/android/src/main/java/com/twg/video/hybrids/videoplayereventemitter/HybridVideoPlayerEventEmitter.kt @@ -7,31 +7,43 @@ import java.util.UUID data class ListenerPair(val id: UUID, val eventName: String, val callback: Any) class HybridVideoPlayerEventEmitter : HybridVideoPlayerEventEmitterSpec() { + private val lock = Any() + var listeners: MutableList = mutableListOf() // MARK: - Private helpers private fun addListener(eventName: String, listener: T): ListenerSubscription { val id = UUID.randomUUID() - listeners.add(ListenerPair(id, eventName, listener)) - return ListenerSubscription { listeners.removeAll { it.id == id } } + synchronized(lock) { + listeners.add(ListenerPair(id, eventName, listener)) + } + return ListenerSubscription { + synchronized(lock) { + listeners.removeAll { it.id == id } + } + } } private inline fun emitEvent(eventName: String, invokeCallback: (T) -> Unit) { - listeners.filter { it.eventName == eventName }.forEach { pair -> + val snapshot: List = synchronized(lock) { + listeners.filter { it.eventName == eventName }.toList() + } + + snapshot.forEach { pair -> try { @Suppress("UNCHECKED_CAST") - val callback = pair.callback as? T - if (callback == null) { + val callback = pair.callback as? T ?: run { Log.d(TAG, "Invalid callback type for $eventName") return@forEach } invokeCallback(callback) - } catch (error: Error) { - Log.d(TAG, "Error calling $eventName listener $error") + } catch (t: Throwable) { + Log.d(TAG, "Error calling $eventName listener", t) } } } + // MARK: - Listener registration methods override fun addOnAudioBecomingNoisyListener(listener: () -> Unit) = @@ -92,7 +104,9 @@ class HybridVideoPlayerEventEmitter : HybridVideoPlayerEventEmitterSpec() { addListener("onVolumeChange", listener) override fun clearAllListeners() { - listeners.clear() + synchronized(lock) { + listeners.clear() + } } // MARK: - Event emission methods 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 99cef8e9..624e351a 100644 --- a/packages/react-native-video/ios/hybrids/VideoPlayer/HybridVideoPlayer+Events.swift +++ b/packages/react-native-video/ios/hybrids/VideoPlayer/HybridVideoPlayer+Events.swift @@ -49,7 +49,7 @@ extension HybridVideoPlayer: VideoPlayerObserverDelegate { func onPlaybackLikelyToKeepUp() { isCurrentlyBuffering = false - if player.timeControlStatus == .playing { + if player.timeControlStatus != .waitingToPlayAtSpecifiedRate { status = .readytoplay } updateAndEmitPlaybackState() diff --git a/packages/react-native-video/ios/hybrids/VideoPlayer/HybridVideoPlayer.swift b/packages/react-native-video/ios/hybrids/VideoPlayer/HybridVideoPlayer.swift index ca68784a..70ce2aa3 100644 --- a/packages/react-native-video/ios/hybrids/VideoPlayer/HybridVideoPlayer.swift +++ b/packages/react-native-video/ios/hybrids/VideoPlayer/HybridVideoPlayer.swift @@ -206,6 +206,9 @@ class HybridVideoPlayer: HybridVideoPlayerSpec, NativeVideoPlayerSpec { func release() { sourceLoader.cancelSync() NowPlayingInfoCenterManager.shared.removePlayer(player: player) + + try? _eventEmitter?.clearAllListeners() + self.player.replaceCurrentItem(with: nil) self.playerItem = nil 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 efa58327..90b1e2f4 100644 --- a/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerSpec.cpp +++ b/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerSpec.cpp @@ -228,6 +228,10 @@ namespace margelo::nitro::video { static const auto method = javaClassStatic()->getMethod /* textTrack */)>("selectTextTrack"); method(_javaPart, textTrack.has_value() ? JVariant_NullType_TextTrack::fromCpp(textTrack.value()) : nullptr); } + void JHybridVideoPlayerSpec::release() { + static const auto method = javaClassStatic()->getMethod("release"); + method(_javaPart); + } std::shared_ptr> JHybridVideoPlayerSpec::initialize() { static const auto method = javaClassStatic()->getMethod()>("initialize"); 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 da546e87..174d7138 100644 --- a/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerSpec.hpp +++ b/packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoPlayerSpec.hpp @@ -82,6 +82,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; + void release() override; std::shared_ptr> initialize() override; std::shared_ptr> preload() override; void play() override; 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 161ea527..75caebbd 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 @@ -141,6 +141,10 @@ abstract class HybridVideoPlayerSpec: HybridObject() { @Keep abstract fun selectTextTrack(textTrack: Variant_NullType_TextTrack?): Unit + @DoNotStrip + @Keep + abstract fun release(): Unit + @DoNotStrip @Keep abstract fun initialize(): Promise 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 aa367019..096ae8c6 100644 --- a/packages/react-native-video/nitrogen/generated/ios/c++/HybridVideoPlayerSpecSwift.hpp +++ b/packages/react-native-video/nitrogen/generated/ios/c++/HybridVideoPlayerSpecSwift.hpp @@ -188,6 +188,12 @@ namespace margelo::nitro::video { std::rethrow_exception(__result.error()); } } + inline void release() override { + auto __result = _swiftPart.release(); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + } inline std::shared_ptr> initialize() override { auto __result = _swiftPart.initialize(); 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 a3f913c5..fd50ac52 100644 --- a/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoPlayerSpec.swift +++ b/packages/react-native-video/nitrogen/generated/ios/swift/HybridVideoPlayerSpec.swift @@ -32,6 +32,7 @@ public protocol HybridVideoPlayerSpec_protocol: HybridObject { func replaceSourceAsync(source: Variant_NullType__any_HybridVideoPlayerSourceSpec_?) throws -> Promise func getAvailableTextTracks() throws -> [TextTrack] func selectTextTrack(textTrack: Variant_NullType_TextTrack?) throws -> Void + func release() throws -> Void func initialize() throws -> Promise func preload() throws -> Promise func play() 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 b72d0bb6..f972bd41 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 @@ -369,6 +369,17 @@ open class HybridVideoPlayerSpec_cxx { } } + @inline(__always) + public final func release() -> bridge.Result_void_ { + do { + try self.__implementation.release() + return bridge.create_Result_void_() + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_void_(__exceptionPtr) + } + } + @inline(__always) public final func initialize() -> bridge.Result_std__shared_ptr_Promise_void___ { do { 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 0c6e677d..ee8795a1 100644 --- a/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoPlayerSpec.cpp +++ b/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoPlayerSpec.cpp @@ -43,6 +43,7 @@ namespace margelo::nitro::video { prototype.registerHybridMethod("replaceSourceAsync", &HybridVideoPlayerSpec::replaceSourceAsync); prototype.registerHybridMethod("getAvailableTextTracks", &HybridVideoPlayerSpec::getAvailableTextTracks); prototype.registerHybridMethod("selectTextTrack", &HybridVideoPlayerSpec::selectTextTrack); + prototype.registerHybridMethod("release", &HybridVideoPlayerSpec::release); prototype.registerHybridMethod("initialize", &HybridVideoPlayerSpec::initialize); prototype.registerHybridMethod("preload", &HybridVideoPlayerSpec::preload); prototype.registerHybridMethod("play", &HybridVideoPlayerSpec::play); 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 bd9f9eca..760583d9 100644 --- a/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoPlayerSpec.hpp +++ b/packages/react-native-video/nitrogen/generated/shared/c++/HybridVideoPlayerSpec.hpp @@ -98,6 +98,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 void release() = 0; virtual std::shared_ptr> initialize() = 0; virtual std::shared_ptr> preload() = 0; virtual void play() = 0; diff --git a/packages/react-native-video/src/core/VideoPlayer.ts b/packages/react-native-video/src/core/VideoPlayer.ts index 304e8c31..8dbef384 100644 --- a/packages/react-native-video/src/core/VideoPlayer.ts +++ b/packages/react-native-video/src/core/VideoPlayer.ts @@ -18,7 +18,19 @@ import { createSource } from './utils/sourceFactory'; import { VideoPlayerEvents } from './VideoPlayerEvents'; class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase { - protected player: VideoPlayerImpl; + private _player: VideoPlayerImpl | undefined; + private _releaseTimeout: ReturnType | undefined; + + protected get player(): VideoPlayerImpl { + if (this._player === undefined) { + throw new VideoRuntimeError( + 'player/released', + "You can't access player after it's released" + ); + } + + return this._player; + } constructor(source: VideoSource | VideoConfig | VideoPlayerSource) { const hybridSource = createSource(source); @@ -26,17 +38,39 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase { // Initialize events super(player.eventEmitter); - this.player = player; + this._player = player; } /** - * Cleans up player's native resources and releases native state. - * After calling this method, the player is no longer usable. + * Releases the player's native resources and releases native state. * @internal */ __destroy() { + if (this._player === undefined) return; + this.clearAllEvents(); - this.player.dispose(); + + try { + this.player.release(); + } catch (error) { + // Best effort cleanup: teardown must never crash app unmount. + console.error('Failed to cleanup native player resources', error); + } + + // We leave hybrid object to be cleaned up by garbage collector + // So we update memory size to ensure that memory is released + // when needed + this.updateMemorySize(); + + // We wait for 5s to let late events that were triggered before release to be processed + if (this._releaseTimeout !== undefined) { + clearTimeout(this._releaseTimeout); + } + + this._releaseTimeout = setTimeout(() => { + this._player = undefined; + this._releaseTimeout = undefined; + }, 5000); } /** diff --git a/packages/react-native-video/src/spec/nitro/VideoPlayer.nitro.ts b/packages/react-native-video/src/spec/nitro/VideoPlayer.nitro.ts index 62ae54a4..83bc3068 100644 --- a/packages/react-native-video/src/spec/nitro/VideoPlayer.nitro.ts +++ b/packages/react-native-video/src/spec/nitro/VideoPlayer.nitro.ts @@ -23,6 +23,12 @@ export interface VideoPlayer */ showNotificationControls: boolean; + /** + * Replace the current source of the player. + * @param source - The new source of the video. + * @note If you want to clear the source, you can pass null. It has the same effect as {@link release}. + * see {@link VideoPlayerSourceBase} + */ replaceSourceAsync(source: VideoPlayerSource | null): Promise; /** @@ -36,6 +42,11 @@ export interface VideoPlayer * @param textTrack - Text track to select, or null to unselect current track */ selectTextTrack(textTrack: TextTrack | null): void; + + /** + * Releases the player's native resources and releases native state. + */ + release(): void; } export interface VideoPlayerFactory