Compare commits

...
3 Commits
Author SHA1 Message Date
zoriya 77a8b2337d fix(vlc): prevent crashes with pip & cast 2026-07-30 13:44:27 +02:00
zoriya cebe97f168 chore(example): fix auto-rotate setting flip bug 2026-07-30 01:17:44 +02:00
zoriya e5615cbc0c fix(source): allow no source on init 2026-07-29 23:44:56 +02:00
5 changed files with 83 additions and 17 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() }
@@ -266,7 +279,7 @@ class OmniPlayer(
override var playbackRate by mainThreadProperty(
get = { player.playbackParameters.speed.toDouble() },
set = { value -> player.setPlaybackSpeed(value.toFloat().coerceAtLeast(0f)) }
set = { value -> player.setPlaybackSpeed(value.toFloat().coerceAtLeast(0.01f)) }
)
override var muted by mainThreadProperty(
@@ -297,6 +310,7 @@ class OmniPlayer(
runOnMainThreadSync {
player.clearMediaItems()
}
syncNotificationService()
return
}
val handleAudioFocus =
@@ -329,6 +343,7 @@ class OmniPlayer(
player.setMediaItems(mediaItems, startIndex, startPositionMs.toLong())
player.prepare()
}
syncNotificationService()
}
override fun play() {
@@ -504,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)
}
@@ -540,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 {
@@ -581,7 +603,7 @@ class OmniPlayerService : MediaSessionService() {
}
override fun onDestroy() {
mediaSession.release()
if (::mediaSession.isInitialized) mediaSession.release()
super.onDestroy()
}
}
@@ -219,6 +219,11 @@ class OmniView(val context: ThemedReactContext) :
return
}
if (boundPlayer?.isCasting == true) {
clearPictureInPictureParams()
return
}
val autoEnterEnabled =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
autoPip == true &&
@@ -242,9 +247,18 @@ class OmniView(val context: ThemedReactContext) :
)
}
private fun clampedAspectRatio(width: Int, height: Int): Rational {
val minRatio = 0.42
val maxRatio = 2.38
val ratio = width.toDouble() / height.toDouble()
if (ratio in minRatio..maxRatio) return Rational(width, height)
val clamped = ratio.coerceIn(minRatio, maxRatio)
return Rational((clamped * 1000).toInt(), 1000)
}
private fun buildPipParams(autoEnterEnabled: Boolean): PictureInPictureParams {
val aspectRatio = if (surfaceView.width > 0 && surfaceView.height > 0) {
Rational(surfaceView.width, surfaceView.height)
clampedAspectRatio(surfaceView.width, surfaceView.height)
} else {
Rational(16, 9)
}
@@ -97,9 +97,11 @@ class VlcPlayer(ctx: Context) :
private var mediaItems: List<MediaItem> = emptyList()
private var currentMediaItemIndex: Int = INDEX_UNSET
private var currentTrackSelectionParameters = TrackSelectionParameters.Builder().build()
@Volatile
private var playerError: PlaybackException? = null
private var playlistMetadata: MediaMetadata = MediaMetadata.EMPTY
private var userInitiatedTransition: Boolean = false
@Volatile
private var cachedBufferedPosition: Long = 0L
private var boundSurfaceView: SurfaceView? = null
@@ -467,7 +469,11 @@ class VlcPlayer(ctx: Context) :
override fun getShuffleModeEnabled(): Boolean = false
override fun isLoading() = player.playerState == IMedia.State.Opening
override fun isLoading(): Boolean {
val state = playbackState
if (state == STATE_IDLE || state == STATE_ENDED) return false
return player.playerState == IMedia.State.Opening
}
override fun seekTo(
mediaItemIndex: Int,
@@ -493,11 +499,11 @@ class VlcPlayer(ctx: Context) :
override fun getSeekForwardIncrement(): Long = 15_000L
override fun setPlaybackParameters(playbackParameters: PlaybackParameters) {
player.rate = playbackParameters.speed.coerceAtLeast(0f)
player.rate = playbackParameters.speed.coerceAtLeast(0.01f)
}
override fun getPlaybackParameters(): PlaybackParameters =
PlaybackParameters(player.rate.coerceAtLeast(0f))
PlaybackParameters(player.rate.takeIf { it > 0f } ?: 1f)
override fun stop() {
player.stop()
@@ -505,6 +511,7 @@ class VlcPlayer(ctx: Context) :
override fun release() {
player.setEventListener(null)
listeners.release()
abandonAudioFocus()
player.stop()
clearVideoSurface()
@@ -742,9 +749,9 @@ class VlcPlayer(ctx: Context) :
}
}
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>
);
@@ -14,6 +14,7 @@
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:screenOrientation="nosensor"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:launchMode="singleTask"
android:resizeableActivity="true"