From e56571701607abfce86976af7a02977c3124e1fc Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 26 Jul 2026 19:40:32 +0200 Subject: [PATCH] fix(exoplayer): fix hotswap between players --- .../main/java/dev/zoriya/omni/OmniPlayer.kt | 5 +- example/App.tsx | 80 ++++++++++++++++++- 2 files changed, 83 insertions(+), 2 deletions(-) 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",