diff --git a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt
index cf870bf..6ff754e 100644
--- a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt
+++ b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt
@@ -107,7 +107,10 @@ class OmniPlayer(
override var showNotification: Boolean? = false
set(value) {
if (value == true) {
- if (notificationPlayer != null && notificationPlayer?.isPlaying == true) {
+ val otherIsPlaying = notificationPlayer?.let { other ->
+ runOnMainThreadSync { other.isPlaying }
+ } == true
+ if (otherIsPlaying) {
throw Error("Two players can't display notifications at the same time.")
}
notificationPlayer = localPlayer
diff --git a/example/App.tsx b/example/App.tsx
index 14f4487..6fff3e0 100644
--- a/example/App.tsx
+++ b/example/App.tsx
@@ -2,6 +2,7 @@ import type React from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
import {
+ type AndroidBackend,
OmniProvider,
OmniView,
useEvent,
@@ -54,10 +55,14 @@ function PlayerExample({
onPrev,
onNext,
trackLabel,
+ backend,
+ onSwitchBackend,
}: {
onPrev: () => void;
onNext: () => void;
trackLabel: string;
+ backend: AndroidBackend;
+ onSwitchBackend: (backend: AndroidBackend) => void;
}): React.JSX.Element {
const player = usePlayer();
const status = usePlayerState("status");
@@ -176,6 +181,15 @@ 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);
+ };
+
const togglePlayback = () => {
if (isPlaying) {
player.pause();
@@ -220,6 +234,27 @@ function PlayerExample({
react-native-omni
{trackLabel}
+
+ switchBackend("vlc")}
+ >
+ VLC
+
+ switchBackend("exoplayer")}
+ >
+ ExoPlayer
+
+
+
("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.
+ const handleSwitchBackend = useCallback((next: AndroidBackend) => {
+ setPendingBackend(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));
@@ -505,12 +561,28 @@ 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 (
-
+
);
@@ -525,6 +597,12 @@ const styles = StyleSheet.create({
paddingVertical: 12,
gap: 12,
},
+ switching: {
+ flex: 1,
+ backgroundColor: "#0b1020",
+ alignItems: "center",
+ justifyContent: "center",
+ },
heading: {
fontSize: 24,
fontWeight: "700",