diff --git a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt index fb8bd89..4053fc3 100644 --- a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt +++ b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt @@ -168,18 +168,25 @@ class OmniPlayer( } } - override fun dispose() { - showNotification = false - super.dispose() + @Volatile + private var released = false - eventMap.dispose() + override fun release() { + if (released) return + released = true + showNotification = false runOnMainThread { + eventMap.dispose() castContext?.removeCastStateListener(castStateListener) - // release both cast and local players. player.release() } } + override fun dispose() { + release() + super.dispose() + } + private fun buildMediaItem( src: com.margelo.nitro.omni.VideoSrc, metadata: com.margelo.nitro.omni.Metadata?, diff --git a/android/src/main/java/dev/zoriya/omni/VlcPlayer.kt b/android/src/main/java/dev/zoriya/omni/VlcPlayer.kt index 88d86d2..e9cc2b7 100644 --- a/android/src/main/java/dev/zoriya/omni/VlcPlayer.kt +++ b/android/src/main/java/dev/zoriya/omni/VlcPlayer.kt @@ -99,6 +99,8 @@ class VlcPlayer(ctx: Context) : private var currentTrackSelectionParameters = TrackSelectionParameters.Builder().build() @Volatile private var playerError: PlaybackException? = null + @Volatile + private var released = false private var playlistMetadata: MediaMetadata = MediaMetadata.EMPTY private var userInitiatedTransition: Boolean = false @Volatile @@ -353,6 +355,7 @@ class VlcPlayer(ctx: Context) : media.addOption(":start-time=${targetMs / 1000.0}") player.setMedia(media) + media.release() } } @@ -430,7 +433,7 @@ class VlcPlayer(ctx: Context) : when { playerError != null -> STATE_IDLE currentMediaItemIndex == INDEX_UNSET -> STATE_IDLE - player.media == null -> STATE_IDLE + player.media?.also { it.release() } == null -> STATE_IDLE player.playerState == IMedia.State.Opening -> STATE_BUFFERING player.isPlaying -> STATE_READY player.isSeekable && player.time >= player.length && player.length > 0 -> STATE_ENDED @@ -519,13 +522,17 @@ class VlcPlayer(ctx: Context) : } override fun release() { + if (released) return + released = true player.setEventListener(null) listeners.release() abandonAudioFocus() player.stop() clearVideoSurface() - player.release() - libVLC.release() + applicationHandler.post { + player.release() + libVLC.release() + } } override fun getCurrentTracks(): Tracks { diff --git a/example/App.tsx b/example/App.tsx index f97ca6e..1dca28b 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -1,5 +1,5 @@ import type React from "react"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useMemo, useRef, useState } from "react"; import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native"; import { type AndroidBackend, @@ -134,10 +134,6 @@ function PlayerExample({ ); const switchBackend = (target: AndroidBackend) => { if (target === backend) return; - // Pause before tearing down: the native media-session guard refuses a - // second player while the current one is still *playing*, so this keeps - // the transition clean (we don't preserve state across a switch anyway). - player.pause(); onSwitchBackend(target); }; @@ -461,27 +457,13 @@ function App(): React.JSX.Element { // notification stays hidden) before any source is set. const [hasSource, setHasSource] = useState(false); const [backend, setBackend] = useState("vlc"); - const [pendingBackend, setPendingBackend] = useState( - null, - ); - // Switching backend recreates the native player, so we fully unmount the - // provider first (rendering nothing) and remount it on the next tick with - // the new backend. This lets the old OmniView/player tear down before the - // new one is created, avoiding the "only one view"/"two players" guards. + // Switching the backend just updates the prop: OmniProvider recreates (and + // disposes) the native player internally, so the change applies live. const handleSwitchBackend = useCallback((next: AndroidBackend) => { - setPendingBackend(next); + setBackend(next); }, []); - useEffect(() => { - if (pendingBackend === null) return; - const id = setTimeout(() => { - setBackend(pendingBackend); - setPendingBackend(null); - }, 300); - return () => clearTimeout(id); - }, [pendingBackend]); - const handlePrev = useCallback(() => { setCurrentIndex((index) => (index === 0 ? PLAYLIST.length - 1 : index - 1)); }, []); @@ -525,18 +507,8 @@ function App(): React.JSX.Element { [currentIndex], ); - // While switching, render nothing so the current player is torn down first. - if (pendingBackend !== null) { - return ( - - Switching to {pendingBackend}… - - ); - } - return ( javaClassStatic()->getMethod("release"); + method(_javaPart); + } void JHybridOmniPlayerSpec::play() { static const auto method = _javaPart->javaClassStatic()->getMethod("play"); method(_javaPart); diff --git a/nitrogen/generated/android/c++/JHybridOmniPlayerSpec.hpp b/nitrogen/generated/android/c++/JHybridOmniPlayerSpec.hpp index 654793c..1198e01 100644 --- a/nitrogen/generated/android/c++/JHybridOmniPlayerSpec.hpp +++ b/nitrogen/generated/android/c++/JHybridOmniPlayerSpec.hpp @@ -78,6 +78,7 @@ namespace margelo::nitro::omni { public: // Methods + void release() override; void play() override; void pause() override; void seekBy(double offset) override; diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/omni/HybridOmniPlayerSpec.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/omni/HybridOmniPlayerSpec.kt index b2a11de..2896a89 100644 --- a/nitrogen/generated/android/kotlin/com/margelo/nitro/omni/HybridOmniPlayerSpec.kt +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/omni/HybridOmniPlayerSpec.kt @@ -114,6 +114,10 @@ abstract class HybridOmniPlayerSpec: HybridObject() { abstract val renditions: Array // Methods + @DoNotStrip + @Keep + abstract fun release(): Unit + @DoNotStrip @Keep abstract fun play(): Unit diff --git a/nitrogen/generated/shared/c++/HybridOmniPlayerSpec.cpp b/nitrogen/generated/shared/c++/HybridOmniPlayerSpec.cpp index 6c84a79..40ff406 100644 --- a/nitrogen/generated/shared/c++/HybridOmniPlayerSpec.cpp +++ b/nitrogen/generated/shared/c++/HybridOmniPlayerSpec.cpp @@ -39,6 +39,7 @@ namespace margelo::nitro::omni { prototype.registerHybridGetter("audios", &HybridOmniPlayerSpec::getAudios); prototype.registerHybridGetter("subtitles", &HybridOmniPlayerSpec::getSubtitles); prototype.registerHybridGetter("renditions", &HybridOmniPlayerSpec::getRenditions); + prototype.registerHybridMethod("release", &HybridOmniPlayerSpec::release); prototype.registerHybridMethod("play", &HybridOmniPlayerSpec::play); prototype.registerHybridMethod("pause", &HybridOmniPlayerSpec::pause); prototype.registerHybridMethod("seekBy", &HybridOmniPlayerSpec::seekBy); diff --git a/nitrogen/generated/shared/c++/HybridOmniPlayerSpec.hpp b/nitrogen/generated/shared/c++/HybridOmniPlayerSpec.hpp index ff35abe..9b727e0 100644 --- a/nitrogen/generated/shared/c++/HybridOmniPlayerSpec.hpp +++ b/nitrogen/generated/shared/c++/HybridOmniPlayerSpec.hpp @@ -91,6 +91,7 @@ namespace margelo::nitro::omni { public: // Methods + virtual void release() = 0; virtual void play() = 0; virtual void pause() = 0; virtual void seekBy(double offset) = 0; diff --git a/src/provider.tsx b/src/provider.tsx index 2f9a1a1..d47bf74 100755 --- a/src/provider.tsx +++ b/src/provider.tsx @@ -1,9 +1,18 @@ -import { createContext, type ReactNode, useContext, useEffect } from "react"; +import { + createContext, + type ReactNode, + useContext, + useEffect, + useEffectEvent, + useState, +} from "react"; import { NitroModules } from "react-native-nitro-modules"; -import type { OmniPlayerFactory } from "./specs/omni-player.nitro"; -import type { OmniPlayer, PlayerBackend } from "./types/player"; +import type { + OmniPlayer as NativeOmniPlayer, + OmniPlayerFactory, +} from "./specs/omni-player.nitro"; +import type { AndroidBackend, OmniPlayer, PlayerBackend } from "./types/player"; import type { CastOptions, Source } from "./types/source"; -import { useLazyRef } from "./utils/lazy-ref"; const ProviderFactory = NitroModules.createHybridObject("OmniPlayerFactory"); @@ -23,18 +32,30 @@ export const OmniProvider = ({ children: ReactNode; showNotification?: boolean; }) => { - const player = useLazyRef(() => - ProviderFactory.createPlayer(source, backend, cast), + const [player, setPlayer] = useState(null); + + const createPlayer = useEffectEvent((aBackend: AndroidBackend) => + ProviderFactory.createPlayer(source, { android: aBackend }, cast), ); useEffect(() => { - player.source = source; - }, [source]); + setPlayer(createPlayer(backend.android ?? "vlc")); + }, [backend.android]); useEffect(() => { - player.showNotification = showNotification; - }, [showNotification]); + if (!player) return; + return () => player.release(); + }, [player]); + useEffect(() => { + if (player) player.source = source; + }, [player, source]); + + useEffect(() => { + if (player) player.showNotification = showNotification; + }, [player, showNotification]); + + if (!player) return null; return {children}; }; diff --git a/src/specs/omni-player.nitro.ts b/src/specs/omni-player.nitro.ts index d422403..693ec9e 100644 --- a/src/specs/omni-player.nitro.ts +++ b/src/specs/omni-player.nitro.ts @@ -80,6 +80,8 @@ export interface OmniPlayer extends HybridObject<{ android: "kotlin" }>, OmniPlayerT { readonly eventMap: OmniEventMap; + + release(): void; } export interface OmniPlayerFactory extends HybridObject<{ android: "kotlin" }> {