Migrate to mpv

This commit is contained in:
2026-04-19 15:39:49 +02:00
parent f6721de3a6
commit 835510115e
15 changed files with 203 additions and 107 deletions
+1 -1
View File
@@ -138,7 +138,7 @@ dependencies {
// Add a dependency on NitroModules
implementation project(":react-native-nitro-modules")
implementation("org.videolan.android:libvlc-all:4.0.0-eap24")
implementation("dev.jdtech.mpv:libmpv:1.0.0")
}
if (isNewArchitectureEnabled()) {
+1 -1
View File
@@ -1,5 +1,5 @@
Omni_kotlinVersion=2.1.20
Omni_minSdkVersion=23
Omni_minSdkVersion=26
Omni_targetSdkVersion=35
Omni_compileSdkVersion=34
Omni_ndkVersion=27.1.12297006
@@ -1,100 +1,127 @@
package dev.zoriya.omni
import android.net.Uri
import com.margelo.nitro.NitroModules
import com.margelo.nitro.omni.HybridOmniEventMapSpec
import com.margelo.nitro.omni.Source
import com.margelo.nitro.omni.HybridOmniPlayerSpec
import com.margelo.nitro.omni.PlayerStatus
import com.margelo.nitro.omni.Rendition
import com.margelo.nitro.omni.Track
import dev.jdtech.mpv.MPVLib
import dev.zoriya.omni.utils.deferredObservable
import org.videolan.libvlc.LibVLC
import org.videolan.libvlc.Media
import org.videolan.libvlc.MediaPlayer
import org.videolan.libvlc.interfaces.IMedia
object VlcConst {
val vlc = LibVLC(NitroModules.applicationContext ?: throw Error("No Context available!"))
}
class OmniPlayer : HybridOmniPlayerSpec() {
val ctx = NitroModules.applicationContext ?: throw Error("No Context available!")
val player = MPVLib.create(ctx) ?: throw Error("Failed to initialize MPVLib")
override val eventMap = EventMap(player)
class OmniPlayer() : HybridOmniPlayerSpec() {
val player = MediaPlayer(VlcConst.vlc)
override val eventMap = EventMap(player);
init {
player.setOptionString("vo", "gpu")
player.setOptionString("gpu-context", "android")
player.setOptionString("opengl-es", "yes")
player.setOptionString("hwdec", "mediacodec-copy")
player.setOptionString("profile", "fast")
player.setOptionString("cache", "yes")
player.setOptionString("cache-pause-initial", "yes")
player.setOptionString("demuxer-max-bytes", "150MiB")
player.setOptionString("demuxer-max-back-bytes", "75MiB")
player.setOptionString("demuxer-readahead-secs", "20")
// seek to keyframes
player.setOptionString("hr-seek", "no")
player.init()
}
override fun dispose() {
super.dispose()
player.detachSurface()
eventMap.dispose()
player.destroy()
}
override var source: Source by deferredObservable { _, _, new ->
val src = new.src.firstOrNull()?.uri ?: return@deferredObservable
player.media = Media(VlcConst.vlc, Uri.parse(src))
player.command(arrayOf("stop"))
val src = new.src.firstOrNull() ?: return@deferredObservable
player.setPropertyString(
"http-header-fields",
src.headers.entries.joinToString("\r\n") { "${it.key}: ${it.value}" }
)
new.startTime?.let { start ->
player.time = (start * 1000.0).toLong().coerceAtLeast(0L)
player.setPropertyDouble("start", start.coerceAtLeast(0.0))
}
// TODO: set video/audio/sub specified
player.command(arrayOf("loadfile", src.uri, "replace"))
for (subtitle in new.subtitles) {
player.command(arrayOf("sub-add", subtitle.link, "cached"))
}
}
fun setSurface(surface: android.view.Surface?) {
if (surface == null) {
player.detachSurface()
} else {
player.attachSurface(surface)
}
}
override val hasPrev get() = source.metadata?.hasPrev ?: false
override val hasNext get() = source.metadata?.hasNext ?: false
override val status
get() = when (player.playerState) {
IMedia.State.NothingSpecial -> PlayerStatus.IDLE
IMedia.State.Opening -> PlayerStatus.LOADING
IMedia.State.Playing -> PlayerStatus.READYTOPLAY
IMedia.State.Paused -> PlayerStatus.READYTOPLAY
IMedia.State.Stopped -> PlayerStatus.IDLE
IMedia.State.Ended -> PlayerStatus.IDLE
IMedia.State.Error -> PlayerStatus.ERROR
else -> PlayerStatus.ERROR
}
override val isPlaying: Boolean get() = player.isPlaying
override var currentTime: Double
get() = player.time.coerceAtLeast(0L) / 1000.0
set(value) {
player.time = (value * 1000.0).toLong().coerceAtLeast(0L)
}
override val buffered: Double
get() {
val total = duration
if (total <= 0.0) return 0.0
return (total * player.position.toDouble()).coerceIn(0.0, total)
}
override val duration: Double get() = player.length.coerceAtLeast(0L) / 1000.0
override var playbackRate: Double
get() = player.rate.toDouble()
set(value) {
if (value > 0.0) player.rate = value.toFloat()
}
override var volume: Double
get() = (player.volume.coerceIn(0, 100) / 100.0)
set(value) {
player.volume = (value.coerceIn(0.0, 1.0) * 100.0).toInt()
}
override val videos get() = getTracks(IMedia.Track.Type.Video)
override val audios get() = getTracks(IMedia.Track.Type.Audio)
override val subtitles get() = getTracks(IMedia.Track.Type.Text)
override val status: PlayerStatus
get() {
val idle = player.getPropertyBoolean("core-idle") ?: false
val loading = player.getPropertyBoolean("paused-for-cache") ?: false
return when {
idle -> PlayerStatus.IDLE
loading -> PlayerStatus.LOADING
else -> PlayerStatus.READYTOPLAY
}
}
override val isPlaying get() = !(player.getPropertyBoolean("pause") ?: true)
override var currentTime
get() = (player.getPropertyDouble("time-pos") ?: 0.0).coerceAtLeast(0.0)
set(value) {
player.command(arrayOf("seek", value.coerceAtLeast(0.0).toString(), "absolute"))
}
override val buffered get() = (player.getPropertyDouble("demuxer-cache-time") ?: 0.0).coerceAtLeast(0.0)
override val duration get() = (player.getPropertyDouble("duration") ?: 0.0).coerceAtLeast(0.0)
fun getTracks(type: Int): Array<Track> {
val tracks = player.getTracks(type)
val selected = player.getSelectedTrack(type)
return tracks.map {
Track(
id = it.id,
label = it.name,
language = it.language,
selected = it.id == selected.id
)
}.toTypedArray()
}
override var playbackRate
get() = player.getPropertyDouble("speed") ?: 1.0
set(value) {
player.setPropertyDouble("speed", value.coerceAtLeast(0.0))
}
override var muted
get() = player.getPropertyBoolean("muted") ?: false
set(value) {
player.setPropertyBoolean("muted", value)
}
override var volume
get() = (player.getPropertyDouble("volume") ?: 100.0).coerceIn(0.0, 100.0) / 100.0
set(value) {
player.setPropertyDouble("volume", value.coerceIn(0.0, 1.0) * 100.0)
}
override val videos get() = getTracks("video")
override val audios get() = getTracks("audio")
override val subtitles get() = getTracks("sub")
// TODO: use `edition-list`
override val rendition: Array<Rendition> get() = emptyArray()
override fun play() {
player.play()
player.setPropertyBoolean("pause", false)
}
override fun pause() {
player.pause()
player.setPropertyBoolean("pause", true)
}
override fun seekBy(offset: Double) {
currentTime += offset
player.command(arrayOf("seek", offset.toString(), "relative"))
}
override fun playPrev() {
@@ -105,22 +132,56 @@ class OmniPlayer() : HybridOmniPlayerSpec() {
// App-level action; no default implementation in VLC player.
}
override fun selectVideo(video: Track) {
player.selectTrack(video.id)
}
override fun selectVideo(video: Track) {
val id = video.id.toIntOrNull() ?: return
player.setPropertyInt("vid", id)
}
override fun selectAudio(audio: Track) {
player.selectTrack(audio.id)
}
override fun selectAudio(audio: Track) {
val id = audio.id.toIntOrNull() ?: return
player.setPropertyInt("aid", id)
}
override fun selectSubtitle(subtitle: Track?) {
when (subtitle) {
null -> player.unselectTrackType(IMedia.Track.Type.Text)
else -> player.selectTrack(subtitle.id)
}
}
override fun selectSubtitle(subtitle: Track?) {
when (subtitle) {
null -> player.setPropertyString("sid", "no")
else -> {
val id = subtitle.id.toIntOrNull() ?: return
player.setPropertyInt("sid", id)
}
}
}
override fun selectRendition(rendition: Rendition?) {
// Not supported by libVLC MediaPlayer track APIs.
}
private fun getTracks(type: String): Array<Track> {
val count = player.getPropertyInt("track-list/count") ?: 0
if (count <= 0) return emptyArray()
val tracks = ArrayList<Track>(count)
for (i in 0 until count) {
val base = "track-list/$i"
val trackType = player.getPropertyString("$base/type") ?: continue
if (trackType != type) continue
val id = player.getPropertyInt("$base/id") ?: continue
val selected = player.getPropertyBoolean("$base/selected") ?: false
val label = player.getPropertyString("$base/title")
?: player.getPropertyString("$base/codec")
val language = player.getPropertyString("$base/lang")
tracks.add(
Track(
id = id.toString(),
label = label,
language = language,
selected = selected
)
)
}
return tracks.toTypedArray()
}
}
@@ -2,11 +2,11 @@ package dev.zoriya.omni
import com.facebook.react.uimanager.ThemedReactContext
import com.margelo.nitro.omni.HybridOmniPlayerFactorySpec
import com.margelo.nitro.omni.HybridOmniPlayerPropsSpec
import com.margelo.nitro.omni.HybridOmniPlayerSpec
import com.margelo.nitro.omni.Source
class OmniPlayerFactory(val context: ThemedReactContext): HybridOmniPlayerFactorySpec() {
override fun createPlayer(props: HybridOmniPlayerPropsSpec): HybridOmniPlayerSpec {
override fun createPlayer(props: Source): HybridOmniPlayerSpec {
return OmniPlayer().apply {
source = props
}
@@ -1,23 +1,43 @@
package dev.zoriya.omni
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.View
import com.facebook.react.uimanager.ThemedReactContext
import com.margelo.nitro.omni.HybridOmniPlayerSpec
import com.margelo.nitro.omni.HybridOmniViewSpec
import org.videolan.libvlc.util.VLCVideoLayout
class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec() {
private val videoLayout = VLCVideoLayout(context)
private var viewsAttached = false
private val surfaceView = SurfaceView(context)
private var surfaceReady = false
private var boundPlayer: OmniPlayer? = null
override val view: View = videoLayout
override val view: View = surfaceView
override lateinit var player: HybridOmniPlayerSpec
override var autoplay: Boolean? = true
override var showNotification: 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() {
if (!::player.isInitialized) {
throw IllegalStateException("Player is null in OmniView")
@@ -25,17 +45,14 @@ class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec() {
val omniPlayer = player as? OmniPlayer
?: throw IllegalStateException("Player is not an OmniPlayer in OmniView")
val vout = omniPlayer.player.vlcVout
if (boundPlayer !== omniPlayer) {
boundPlayer?.player?.detachViews()
viewsAttached = false
boundPlayer?.setSurface(null)
boundPlayer = omniPlayer
}
if (!viewsAttached || !vout.areViewsAttached()) {
omniPlayer.player.attachViews(videoLayout, null, false, false)
viewsAttached = true
if (surfaceReady) {
omniPlayer.setSurface(surfaceView.holder.surface)
}
if (autoplay == true && !omniPlayer.isPlaying) {
@@ -47,11 +64,7 @@ class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec() {
if (!::player.isInitialized) return
val omniPlayer = player as? OmniPlayer ?: return
val vout = omniPlayer.player.vlcVout
if (vout.areViewsAttached()) {
omniPlayer.player.detachViews()
}
viewsAttached = false
omniPlayer.setSurface(null)
boundPlayer = null
}
}
@@ -16,6 +16,6 @@ inline fun <T : Any> deferredObservable(
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
val old = this.value
this.value = value
onChange(old, value)
onChange(thisRef, old, value)
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
buildscript {
ext {
buildToolsVersion = "36.0.0"
minSdkVersion = 24
minSdkVersion = 26
compileSdkVersion = 36
targetSdkVersion = 36
ndkVersion = "27.1.12297006"
@@ -207,6 +207,15 @@ namespace margelo::nitro::omni {
static const auto method = _javaPart->javaClassStatic()->getMethod<void(double /* volume */)>("setVolume");
method(_javaPart, volume);
}
bool JHybridOmniPlayerSpec::getMuted() {
static const auto method = _javaPart->javaClassStatic()->getMethod<jboolean()>("getMuted");
auto __result = method(_javaPart);
return static_cast<bool>(__result);
}
void JHybridOmniPlayerSpec::setMuted(bool muted) {
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jboolean /* muted */)>("setMuted");
method(_javaPart, muted);
}
// Methods
void JHybridOmniPlayerSpec::play() {
@@ -69,6 +69,8 @@ namespace margelo::nitro::omni {
void setPlaybackRate(double playbackRate) override;
double getVolume() override;
void setVolume(double volume) override;
bool getMuted() override;
void setMuted(bool muted) override;
public:
// Methods
@@ -92,6 +92,12 @@ abstract class HybridOmniPlayerSpec: HybridObject() {
@set:DoNotStrip
@set:Keep
abstract var volume: Double
@get:DoNotStrip
@get:Keep
@set:DoNotStrip
@set:Keep
abstract var muted: Boolean
// Methods
@DoNotStrip
+2
View File
@@ -33,6 +33,8 @@ namespace margelo::nitro::omni {
prototype.registerHybridSetter("playbackRate", &HybridOmniPlayerSpec::setPlaybackRate);
prototype.registerHybridGetter("volume", &HybridOmniPlayerSpec::getVolume);
prototype.registerHybridSetter("volume", &HybridOmniPlayerSpec::setVolume);
prototype.registerHybridGetter("muted", &HybridOmniPlayerSpec::getMuted);
prototype.registerHybridSetter("muted", &HybridOmniPlayerSpec::setMuted);
prototype.registerHybridMethod("play", &HybridOmniPlayerSpec::play);
prototype.registerHybridMethod("pause", &HybridOmniPlayerSpec::pause);
prototype.registerHybridMethod("seekBy", &HybridOmniPlayerSpec::seekBy);
+2
View File
@@ -79,6 +79,8 @@ namespace margelo::nitro::omni {
virtual void setPlaybackRate(double playbackRate) = 0;
virtual double getVolume() = 0;
virtual void setVolume(double volume) = 0;
virtual bool getMuted() = 0;
virtual void setMuted(bool muted) = 0;
public:
// Methods
+1 -1
View File
@@ -23,7 +23,7 @@ in
nodejs
biome
jdk
((android-studio.withSdk android.androidsdk).overrideAttrs {forceWayland = true;})
((android-studio.override {forceWayland = true;}).withSdk android.androidsdk)
];
shellHook = ''
+6 -6
View File
@@ -1,17 +1,17 @@
import type { HybridObject } from "react-native-nitro-modules";
import type { OmniEvents } from "../types/events";
import type {
OmniPlayerState,
// OmniPlayerState,
OmniPlayer as OmniPlayerT,
} from "../types/player";
import type { Source } from "../types/source";
export interface OmniEventMap extends HybridObject<{ android: "kotlin" }> {
addStateListener(
key: keyof OmniPlayerState,
cb: (value: number) => void,
): void;
removeStateListener(cb: OmniEvents["end"]): void;
// addStateListener(
// key: keyof OmniPlayerState,
// cb: (value: number) => void,
// ): void;
// removeStateListener(cb: OmniEvents["end"]): void;
addOnEndListener(cb: OmniEvents["end"]): void;
removeOnEndListener(cb: OmniEvents["end"]): void;
+1
View File
@@ -32,6 +32,7 @@ export interface OmniPlayerState {
playbackRate: number;
// between 0 and 1
volume: number;
muted: boolean;
}
export type PlayerStatus = "idle" | "loading" | "readyToPlay" | "error";