mirror of
https://github.com/zoriya/react-native-omni.git
synced 2026-08-04 21:26:48 +00:00
fix(backend): allow backend switch
This commit is contained in:
@@ -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?,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+8
-39
@@ -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<AndroidBackend>("vlc");
|
||||
const [pendingBackend, setPendingBackend] = useState<AndroidBackend | null>(
|
||||
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 (
|
||||
<View style={styles.switching}>
|
||||
<Text style={styles.subheading}>Switching to {pendingBackend}…</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<OmniProvider
|
||||
key={backend}
|
||||
source={hasSource ? source : undefined}
|
||||
backend={{ android: backend }}
|
||||
cast={{ receiverApplicationId: "D8FB0FC1" }}
|
||||
@@ -564,12 +536,6 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: 12,
|
||||
gap: 12,
|
||||
},
|
||||
switching: {
|
||||
flex: 1,
|
||||
backgroundColor: "#0b1020",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
heading: {
|
||||
fontSize: 24,
|
||||
fontWeight: "700",
|
||||
@@ -581,7 +547,10 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
video: {
|
||||
width: "100%",
|
||||
aspectRatio: 16 / 9,
|
||||
// Intentionally wider than the 16:9 sample videos so the view's aspect
|
||||
// ratio differs from the video's. This surfaces the PIP aspect-ratio bug
|
||||
// where PIP would otherwise inherit the view's shape, not the video's.
|
||||
aspectRatio: 21 / 9,
|
||||
borderRadius: 14,
|
||||
overflow: "hidden",
|
||||
},
|
||||
|
||||
@@ -241,6 +241,10 @@ namespace margelo::nitro::omni {
|
||||
}
|
||||
|
||||
// Methods
|
||||
void JHybridOmniPlayerSpec::release() {
|
||||
static const auto method = _javaPart->javaClassStatic()->getMethod<void()>("release");
|
||||
method(_javaPart);
|
||||
}
|
||||
void JHybridOmniPlayerSpec::play() {
|
||||
static const auto method = _javaPart->javaClassStatic()->getMethod<void()>("play");
|
||||
method(_javaPart);
|
||||
|
||||
@@ -78,6 +78,7 @@ namespace margelo::nitro::omni {
|
||||
|
||||
public:
|
||||
// Methods
|
||||
void release() override;
|
||||
void play() override;
|
||||
void pause() override;
|
||||
void seekBy(double offset) override;
|
||||
|
||||
+4
@@ -114,6 +114,10 @@ abstract class HybridOmniPlayerSpec: HybridObject() {
|
||||
abstract val renditions: Array<Rendition>
|
||||
|
||||
// Methods
|
||||
@DoNotStrip
|
||||
@Keep
|
||||
abstract fun release(): Unit
|
||||
|
||||
@DoNotStrip
|
||||
@Keep
|
||||
abstract fun play(): Unit
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
+31
-10
@@ -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>("OmniPlayerFactory");
|
||||
@@ -23,18 +32,30 @@ export const OmniProvider = ({
|
||||
children: ReactNode;
|
||||
showNotification?: boolean;
|
||||
}) => {
|
||||
const player = useLazyRef(() =>
|
||||
ProviderFactory.createPlayer(source, backend, cast),
|
||||
const [player, setPlayer] = useState<NativeOmniPlayer | null>(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 <PlayerCtx.Provider value={player}>{children}</PlayerCtx.Provider>;
|
||||
};
|
||||
|
||||
|
||||
@@ -80,6 +80,8 @@ export interface OmniPlayer
|
||||
extends HybridObject<{ android: "kotlin" }>,
|
||||
OmniPlayerT {
|
||||
readonly eventMap: OmniEventMap;
|
||||
|
||||
release(): void;
|
||||
}
|
||||
|
||||
export interface OmniPlayerFactory extends HybridObject<{ android: "kotlin" }> {
|
||||
|
||||
Reference in New Issue
Block a user