Fix omni-view

This commit is contained in:
2026-04-20 19:53:39 +02:00
parent 4bb13739d5
commit f1112bbc8f
4 changed files with 134 additions and 115 deletions
@@ -16,7 +16,10 @@ class OmniPlayer : HybridOmniPlayerSpec() {
override val eventMap = EventMap(player) override val eventMap = EventMap(player)
init { init {
player.setOptionString("vo", "null") // `vo` is effectively fixed after init in libmpv.
// Initializing with `null` keeps audio working but prevents video output forever.
player.setOptionString("vo", "gpu-next")
player.setOptionString("force-window", "yes")
player.setOptionString("gpu-context", "android") player.setOptionString("gpu-context", "android")
player.setOptionString("opengl-es", "yes") player.setOptionString("opengl-es", "yes")
player.setOptionString("hwdec", "mediacodec-copy") player.setOptionString("hwdec", "mediacodec-copy")
@@ -46,6 +49,7 @@ class OmniPlayer : HybridOmniPlayerSpec() {
} }
override var source: Source by deferredObservable { _, _, new -> override var source: Source by deferredObservable { _, _, new ->
Log.e("omni", "Chaning source")
player.command(arrayOf("stop")) player.command(arrayOf("stop"))
val src = new.src.firstOrNull() ?: return@deferredObservable val src = new.src.firstOrNull() ?: return@deferredObservable
@@ -76,6 +80,11 @@ class OmniPlayer : HybridOmniPlayerSpec() {
} }
} }
fun setSurfaceSize(width: Int, height: Int) {
if (width <= 0 || height <= 0) return
player.setPropertyString("android-surface-size", "${width}x${height}")
}
override val hasPrev get() = source.metadata?.hasPrev ?: false override val hasPrev get() = source.metadata?.hasPrev ?: false
override val hasNext get() = source.metadata?.hasNext ?: false override val hasNext get() = source.metadata?.hasNext ?: false
override val status: PlayerStatus override val status: PlayerStatus
@@ -1,61 +1,57 @@
package dev.zoriya.omni package dev.zoriya.omni
import android.util.Log
import android.view.SurfaceHolder import android.view.SurfaceHolder
import android.view.SurfaceView import android.view.SurfaceView
import android.view.View import android.view.View
import android.widget.FrameLayout
import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.ThemedReactContext
import com.margelo.nitro.omni.HybridOmniPlayerSpec import com.margelo.nitro.omni.HybridOmniPlayerSpec
import com.margelo.nitro.omni.HybridOmniViewSpec import com.margelo.nitro.omni.HybridOmniViewSpec
class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec() { class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec(), SurfaceHolder.Callback {
private val surfaceView = SurfaceView(context) override val view = FrameLayout(context)
private val surfaceView = SurfaceView(context).apply {
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
holder.addCallback(this@OmniView)
view.addView(this)
}
private var surfaceReady = false private var surfaceReady = false
private var boundPlayer: OmniPlayer? = null private var boundPlayer: OmniPlayer? = null
override val view: View = surfaceView
override lateinit var player: HybridOmniPlayerSpec override lateinit var player: HybridOmniPlayerSpec
override var autoplay: Boolean? = true override var autoplay: Boolean? = true
override var showNotification: Boolean? = true override var showNotification: Boolean? = true
override var autoPip: Boolean? = true override var autoPip: Boolean? = true
init {
surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {
surfaceReady = true
boundPlayer?.setSurface(holder.surface)
}
override fun surfaceChanged(
holder: SurfaceHolder,
format: Int,
width: Int,
height: Int
) {
surfaceReady = true
boundPlayer?.setSurface(holder.surface)
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
surfaceReady = false
boundPlayer?.setSurface(null)
}
})
}
override fun afterUpdate() { override fun afterUpdate() {
Log.e("omniView", "After update called")
if (!::player.isInitialized) { if (!::player.isInitialized) {
throw IllegalStateException("Player is null in OmniView") Log.w("omniView", "Skipping update because player is not set yet")
return
} }
val omniPlayer = player as? OmniPlayer val omniPlayer = player as? OmniPlayer
?: throw IllegalStateException("Player is not an OmniPlayer in OmniView") ?: run {
Log.w(
"omniView",
"Skipping update because player has unexpected type: ${player::class.java.name}"
)
return
}
if (boundPlayer !== omniPlayer) { if (boundPlayer === omniPlayer) {
boundPlayer?.setSurface(null) return;
boundPlayer = omniPlayer
} }
Log.e("omniView", "Resetting old player")
boundPlayer?.setSurface(null)
boundPlayer = omniPlayer
if (surfaceReady) { if (surfaceReady) {
omniPlayer.setSurface(surfaceView.holder.surface) omniPlayer.setSurface(surfaceView.holder.surface)
} }
@@ -66,10 +62,32 @@ class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec() {
} }
override fun onDropView() { override fun onDropView() {
Log.e("omniView", "omni-view dropped")
if (!::player.isInitialized) return if (!::player.isInitialized) return
val omniPlayer = player as? OmniPlayer ?: return val omniPlayer = player as? OmniPlayer ?: return
omniPlayer.setSurface(null) omniPlayer.setSurface(null)
boundPlayer = null boundPlayer = null
} }
override fun surfaceCreated(holder: SurfaceHolder) {
surfaceReady = true
Log.e("omniView", "surface created")
boundPlayer?.setSurface(holder.surface)
}
override fun surfaceChanged(
holder: SurfaceHolder,
format: Int,
width: Int,
height: Int
) {
boundPlayer?.setSurfaceSize(width, height)
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
Log.e("omniView", "surface destroyed")
surfaceReady = false
boundPlayer?.setSurface(null)
}
} }
+67 -76
View File
@@ -1,13 +1,6 @@
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import type React from "react"; import type React from "react";
import { import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
Pressable,
SafeAreaView,
ScrollView,
StyleSheet,
Text,
View,
} from "react-native";
import { import {
OmniProvider, OmniProvider,
OmniView, OmniView,
@@ -129,72 +122,73 @@ function PlayerExample({
}; };
return ( return (
<SafeAreaView style={styles.safeArea}> <View style={styles.container}>
<View style={styles.container}> <Text style={styles.heading}>react-native-omni</Text>
<Text style={styles.heading}>react-native-omni</Text> <Text style={styles.subheading}>{trackLabel}</Text>
<Text style={styles.subheading}>{trackLabel}</Text>
<View style={styles.video}> <OmniView style={styles.video} autoplay={true} showNotification={true} />
<OmniView autoplay={true} showNotification={true} />
</View>
<View style={styles.row}> <View style={styles.row}>
<Pressable style={styles.button} onPress={togglePlayback}> <Pressable style={styles.button} onPress={togglePlayback}>
<Text style={styles.buttonText}>{isPlaying ? "Pause" : "Play"}</Text> <Text style={styles.buttonText}>{isPlaying ? "Pause" : "Play"}</Text>
</Pressable> </Pressable>
<Pressable style={styles.button} onPress={() => player.seekBy(-10)}> <Pressable style={styles.button} onPress={() => player.seekBy(-10)}>
<Text style={styles.buttonText}>-10s</Text> <Text style={styles.buttonText}>-10s</Text>
</Pressable> </Pressable>
<Pressable style={styles.button} onPress={() => player.seekBy(10)}> <Pressable style={styles.button} onPress={() => player.seekBy(10)}>
<Text style={styles.buttonText}>+10s</Text> <Text style={styles.buttonText}>+10s</Text>
</Pressable> </Pressable>
</View>
<View style={styles.row}>
<Pressable style={styles.button} onPress={() => player.playPrev()}>
<Text style={styles.buttonText}>Prev</Text>
</Pressable>
<Pressable style={styles.button} onPress={() => player.playNext()}>
<Text style={styles.buttonText}>Next</Text>
</Pressable>
<Pressable style={styles.button} onPress={toggleMute}>
<Text style={styles.buttonText}>{muted ? "Unmute" : "Mute"}</Text>
</Pressable>
</View>
<View style={styles.row}>
<Pressable style={styles.button} onPress={() => changeVolume(-0.1)}>
<Text style={styles.buttonText}>Vol -</Text>
</Pressable>
<Pressable style={styles.button} onPress={() => changeVolume(0.1)}>
<Text style={styles.buttonText}>Vol +</Text>
</Pressable>
<Pressable style={styles.button} onPress={cyclePlaybackRate}>
<Text style={styles.buttonText}>{playbackRate.toFixed(2)}x</Text>
</Pressable>
</View>
<View style={styles.statsCard}>
<Text style={styles.statText}>Status: {status}</Text>
<Text style={styles.statText}>
Time: {formatTime(currentTime)} / {formatTime(duration)}
</Text>
<Text style={styles.statText}>Volume: {(volume * 100).toFixed(0)}%</Text>
</View>
<ScrollView style={styles.logCard} contentContainerStyle={styles.logContent}>
{logs.length === 0 ? (
<Text style={styles.logText}>Event log will appear here.</Text>
) : (
logs.map((entry, index) => (
<Text key={`${entry}-${index}`} style={styles.logText}>
{entry}
</Text>
))
)}
</ScrollView>
</View> </View>
</SafeAreaView>
<View style={styles.row}>
<Pressable style={styles.button} onPress={() => player.playPrev()}>
<Text style={styles.buttonText}>Prev</Text>
</Pressable>
<Pressable style={styles.button} onPress={() => player.playNext()}>
<Text style={styles.buttonText}>Next</Text>
</Pressable>
<Pressable style={styles.button} onPress={toggleMute}>
<Text style={styles.buttonText}>{muted ? "Unmute" : "Mute"}</Text>
</Pressable>
</View>
<View style={styles.row}>
<Pressable style={styles.button} onPress={() => changeVolume(-0.1)}>
<Text style={styles.buttonText}>Vol -</Text>
</Pressable>
<Pressable style={styles.button} onPress={() => changeVolume(0.1)}>
<Text style={styles.buttonText}>Vol +</Text>
</Pressable>
<Pressable style={styles.button} onPress={cyclePlaybackRate}>
<Text style={styles.buttonText}>{playbackRate.toFixed(2)}x</Text>
</Pressable>
</View>
<View style={styles.statsCard}>
<Text style={styles.statText}>Status: {status}</Text>
<Text style={styles.statText}>
Time: {formatTime(currentTime)} / {formatTime(duration)}
</Text>
<Text style={styles.statText}>
Volume: {(volume * 100).toFixed(0)}%
</Text>
</View>
<ScrollView
style={styles.logCard}
contentContainerStyle={styles.logContent}
>
{logs.length === 0 ? (
<Text style={styles.logText}>Event log will appear here.</Text>
) : (
logs.map((entry, index) => (
<Text key={`${entry}-${index}`} style={styles.logText}>
{entry}
</Text>
))
)}
</ScrollView>
</View>
); );
} }
@@ -239,13 +233,11 @@ function App(): React.JSX.Element {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: "#0b1020",
},
container: { container: {
flex: 1, flex: 1,
backgroundColor: "#0b1020",
paddingHorizontal: 16, paddingHorizontal: 16,
marginTop: 64,
paddingVertical: 12, paddingVertical: 12,
gap: 12, gap: 12,
}, },
@@ -263,7 +255,6 @@ const styles = StyleSheet.create({
aspectRatio: 16 / 9, aspectRatio: 16 / 9,
borderRadius: 14, borderRadius: 14,
overflow: "hidden", overflow: "hidden",
backgroundColor: "#000000",
}, },
row: { row: {
flexDirection: "row", flexDirection: "row",
+6 -5
View File
@@ -7,13 +7,14 @@ import { usePlayer } from "./provider";
import type { OmniPlayer } from "./specs/omni-player.nitro"; import type { OmniPlayer } from "./specs/omni-player.nitro";
import type { Props } from "./specs/omni-view.nitro"; import type { Props } from "./specs/omni-view.nitro";
import type { OmniViewProps } from "./types/view"; import type { OmniViewProps } from "./types/view";
import type { ViewStyle } from "react-native";
import { memo } from "react";
const NativeView = getHostComponent<Props, HybridViewMethods>( const NativeView = memo(
"OmniView", getHostComponent<Props, HybridViewMethods>("OmniView", () => OmniConfig),
() => OmniConfig,
); );
export const OmniView = (props: OmniViewProps) => { export const OmniView = memo((props: OmniViewProps & { style: ViewStyle }) => {
const player = usePlayer() as OmniPlayer; const player = usePlayer() as OmniPlayer;
return <NativeView player={player} {...props} />; return <NativeView player={player} {...props} />;
}; });