Compare commits

..
2 Commits
Author SHA1 Message Date
zoriya e5615cbc0c fix(source): allow no source on init 2026-07-29 23:44:56 +02:00
zoriya 5f2d74f321 fix(vlc): fix a crash in vlc on source change 2026-07-29 12:45:46 +02:00
3 changed files with 77 additions and 33 deletions
@@ -106,21 +106,34 @@ class OmniPlayer(
override var showNotification: Boolean? = false
set(value) {
if (value == true) {
field = value
syncNotificationService()
}
private var serviceRunning = false
private fun syncNotificationService() {
val shouldShow = showNotification == true && source != null
when {
shouldShow && !serviceRunning -> {
val otherIsPlaying = notificationPlayer?.let { other ->
runOnMainThreadSync { other.isPlaying }
other !== localPlayer && runOnMainThreadSync { other.isPlaying }
} == true
if (otherIsPlaying) {
throw Error("Two players can't display notifications at the same time.")
}
notificationPlayer = localPlayer
ctx.startForegroundService(Intent(ctx, OmniPlayerService::class.java))
} else if (field == true && notificationPlayer == localPlayer) {
serviceRunning = true
}
!shouldShow && serviceRunning -> {
ctx.stopService(Intent(ctx, OmniPlayerService::class.java))
notificationPlayer = null
if (notificationPlayer == localPlayer) notificationPlayer = null
serviceRunning = false
}
field = value
}
}
override val castStatus: CastStatus
get() = runOnMainThreadSync { computeCastStatus() }
@@ -295,9 +308,9 @@ class OmniPlayer(
field = value
if (value == null) {
runOnMainThreadSync {
player.setMediaItem(MediaItem.EMPTY)
player.prepare()
player.clearMediaItems()
}
syncNotificationService()
return
}
val handleAudioFocus =
@@ -330,6 +343,7 @@ class OmniPlayer(
player.setMediaItems(mediaItems, startIndex, startPositionMs.toLong())
player.prepare()
}
syncNotificationService()
}
override fun play() {
@@ -505,7 +519,13 @@ class OmniPlayerService : MediaSessionService() {
override fun onCreate() {
super.onCreate()
player = OmniPlayer.notificationPlayer ?: throw Error("No player available")
val available = OmniPlayer.notificationPlayer
if (available == null) {
startForeground(1, createImmediateNotification())
stopSelf()
return
}
player = available
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)?.apply {
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
@@ -541,7 +561,8 @@ class OmniPlayerService : MediaSessionService() {
player = current
mediaSession.player = current
}
return super.onStartCommand(intent, flags, startId)
super.onStartCommand(intent, flags, startId)
return START_NOT_STICKY
}
private fun createImmediateNotification(): Notification {
@@ -582,7 +603,7 @@ class OmniPlayerService : MediaSessionService() {
}
override fun onDestroy() {
mediaSession.release()
if (::mediaSession.isInitialized) mediaSession.release()
super.onDestroy()
}
}
@@ -689,25 +689,27 @@ class VlcPlayer(ctx: Context) :
}
override fun getCurrentTimeline(): Timeline {
if (mediaItems.isEmpty()) return Timeline.EMPTY
// snapshot the current playlist for the returned timeline.
// timelines are immutable
val items = mediaItems
val currentIndex = currentMediaItemIndex
val currentDurationUs = duration.takeIf { it != TIME_UNSET }?.let { it * 1000L } ?: TIME_UNSET
if (items.isEmpty()) return Timeline.EMPTY
return object : Timeline() {
private fun durationUsForIndex(index: Int): Long {
if (index != currentMediaItemIndex) return TIME_UNSET
val dur = duration
return if (dur == TIME_UNSET) TIME_UNSET else dur * 1000L
}
private fun durationUsForIndex(index: Int): Long =
if (index == currentIndex) currentDurationUs else TIME_UNSET
override fun getWindowCount(): Int = mediaItems.size
override fun getWindowCount(): Int = items.size
override fun getWindow(
windowIndex: Int,
window: Window,
defaultPositionProjectionUs: Long
): Window {
check(windowIndex in mediaItems.indices)
val index = windowIndex.coerceIn(items.indices)
return window.set(
windowIndex,
mediaItems[windowIndex],
index,
items[index],
null,
TIME_UNSET,
TIME_UNSET,
@@ -716,34 +718,33 @@ class VlcPlayer(ctx: Context) :
false,
null,
0L,
durationUsForIndex(windowIndex),
0,
mediaItems.size - 1,
durationUsForIndex(index),
index,
index,
0L
)
}
override fun getPeriodCount(): Int = mediaItems.size
override fun getPeriodCount(): Int = items.size
override fun getPeriod(periodIndex: Int, period: Period, setIds: Boolean): Period {
check(periodIndex in mediaItems.indices)
return period.set(periodIndex, periodIndex, 0, durationUsForIndex(periodIndex), 0L)
val index = periodIndex.coerceIn(items.indices)
return period.set(index, index, index, durationUsForIndex(index), 0L)
}
override fun getIndexOfPeriod(uid: Any): Int {
return if (uid is Int && uid in mediaItems.indices) uid else INDEX_UNSET
return if (uid is Int && uid in items.indices) uid else INDEX_UNSET
}
override fun getUidOfPeriod(periodIndex: Int): Any {
check(periodIndex in mediaItems.indices)
return periodIndex
return periodIndex.coerceIn(items.indices)
}
}
}
override fun getCurrentPeriodIndex() = currentMediaItemIndex
override fun getCurrentPeriodIndex() = currentMediaItemIndex.coerceAtLeast(0)
override fun getCurrentMediaItemIndex() = currentMediaItemIndex
override fun getCurrentMediaItemIndex() = currentMediaItemIndex.coerceAtLeast(0)
override fun getDuration(): Long = player.length.takeIf { it > 0 } ?: TIME_UNSET
+24 -2
View File
@@ -57,12 +57,16 @@ function PlayerExample({
trackLabel,
backend,
onSwitchBackend,
hasSource,
onLoad,
}: {
onPrev: () => void;
onNext: () => void;
trackLabel: string;
backend: AndroidBackend;
onSwitchBackend: (backend: AndroidBackend) => void;
hasSource: boolean;
onLoad: () => void;
}): React.JSX.Element {
const player = usePlayer();
const status = usePlayerState("status");
@@ -233,7 +237,20 @@ function PlayerExample({
return (
<ScrollView style={styles.container}>
<Text style={styles.heading}>react-native-omni</Text>
<Text style={styles.subheading}>{trackLabel}</Text>
<Text style={styles.subheading}>
{hasSource ? trackLabel : "No media loaded"}
</Text>
<View style={styles.row}>
<Pressable
style={[styles.button, hasSource && styles.selectedTrackButton]}
onPress={onLoad}
>
<Text style={styles.buttonText}>
{hasSource ? "Unload media" : "Load media"}
</Text>
</Pressable>
</View>
<View style={styles.row}>
<Pressable
@@ -497,6 +514,9 @@ function PlayerExample({
function App(): React.JSX.Element {
const [currentIndex, setCurrentIndex] = useState(0);
// Start with no media loaded so we can verify the app boots (and the media
// 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,
@@ -574,7 +594,7 @@ function App(): React.JSX.Element {
return (
<OmniProvider
key={backend}
source={source}
source={hasSource ? source : undefined}
backend={{ android: backend }}
cast={{receiverApplicationId: "D8FB0FC1"}}
showNotification
@@ -585,6 +605,8 @@ function App(): React.JSX.Element {
trackLabel={PLAYLIST[currentIndex].title}
backend={backend}
onSwitchBackend={handleSwitchBackend}
hasSource={hasSource}
onLoad={() => setHasSource((v) => !v)}
/>
</OmniProvider>
);