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 // Add a dependency on NitroModules
implementation project(":react-native-nitro-modules") 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()) { if (isNewArchitectureEnabled()) {
+1 -1
View File
@@ -1,5 +1,5 @@
Omni_kotlinVersion=2.1.20 Omni_kotlinVersion=2.1.20
Omni_minSdkVersion=23 Omni_minSdkVersion=26
Omni_targetSdkVersion=35 Omni_targetSdkVersion=35
Omni_compileSdkVersion=34 Omni_compileSdkVersion=34
Omni_ndkVersion=27.1.12297006 Omni_ndkVersion=27.1.12297006
@@ -1,100 +1,127 @@
package dev.zoriya.omni package dev.zoriya.omni
import android.net.Uri
import com.margelo.nitro.NitroModules import com.margelo.nitro.NitroModules
import com.margelo.nitro.omni.HybridOmniEventMapSpec
import com.margelo.nitro.omni.Source import com.margelo.nitro.omni.Source
import com.margelo.nitro.omni.HybridOmniPlayerSpec import com.margelo.nitro.omni.HybridOmniPlayerSpec
import com.margelo.nitro.omni.PlayerStatus import com.margelo.nitro.omni.PlayerStatus
import com.margelo.nitro.omni.Rendition import com.margelo.nitro.omni.Rendition
import com.margelo.nitro.omni.Track import com.margelo.nitro.omni.Track
import dev.jdtech.mpv.MPVLib
import dev.zoriya.omni.utils.deferredObservable 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 { class OmniPlayer : HybridOmniPlayerSpec() {
val vlc = LibVLC(NitroModules.applicationContext ?: throw Error("No Context available!")) 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() { init {
val player = MediaPlayer(VlcConst.vlc) player.setOptionString("vo", "gpu")
override val eventMap = EventMap(player); 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 -> override var source: Source by deferredObservable { _, _, new ->
val src = new.src.firstOrNull()?.uri ?: return@deferredObservable player.command(arrayOf("stop"))
player.media = Media(VlcConst.vlc, Uri.parse(src))
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 -> 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 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 override val status: PlayerStatus
get() = when (player.playerState) { get() {
IMedia.State.NothingSpecial -> PlayerStatus.IDLE val idle = player.getPropertyBoolean("core-idle") ?: false
IMedia.State.Opening -> PlayerStatus.LOADING val loading = player.getPropertyBoolean("paused-for-cache") ?: false
IMedia.State.Playing -> PlayerStatus.READYTOPLAY return when {
IMedia.State.Paused -> PlayerStatus.READYTOPLAY idle -> PlayerStatus.IDLE
IMedia.State.Stopped -> PlayerStatus.IDLE loading -> PlayerStatus.LOADING
IMedia.State.Ended -> PlayerStatus.IDLE else -> PlayerStatus.READYTOPLAY
IMedia.State.Error -> PlayerStatus.ERROR }
else -> PlayerStatus.ERROR }
} override val isPlaying get() = !(player.getPropertyBoolean("pause") ?: true)
override val isPlaying: Boolean get() = player.isPlaying override var currentTime
override var currentTime: Double get() = (player.getPropertyDouble("time-pos") ?: 0.0).coerceAtLeast(0.0)
get() = player.time.coerceAtLeast(0L) / 1000.0 set(value) {
set(value) { player.command(arrayOf("seek", value.coerceAtLeast(0.0).toString(), "absolute"))
player.time = (value * 1000.0).toLong().coerceAtLeast(0L) }
} override val buffered get() = (player.getPropertyDouble("demuxer-cache-time") ?: 0.0).coerceAtLeast(0.0)
override val buffered: Double override val duration get() = (player.getPropertyDouble("duration") ?: 0.0).coerceAtLeast(0.0)
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)
fun getTracks(type: Int): Array<Track> { override var playbackRate
val tracks = player.getTracks(type) get() = player.getPropertyDouble("speed") ?: 1.0
val selected = player.getSelectedTrack(type) set(value) {
return tracks.map { player.setPropertyDouble("speed", value.coerceAtLeast(0.0))
Track( }
id = it.id,
label = it.name,
language = it.language,
selected = it.id == selected.id
)
}.toTypedArray()
}
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 val rendition: Array<Rendition> get() = emptyArray()
override fun play() { override fun play() {
player.play() player.setPropertyBoolean("pause", false)
} }
override fun pause() { override fun pause() {
player.pause() player.setPropertyBoolean("pause", true)
} }
override fun seekBy(offset: Double) { override fun seekBy(offset: Double) {
currentTime += offset player.command(arrayOf("seek", offset.toString(), "relative"))
} }
override fun playPrev() { override fun playPrev() {
@@ -105,22 +132,56 @@ class OmniPlayer() : HybridOmniPlayerSpec() {
// App-level action; no default implementation in VLC player. // App-level action; no default implementation in VLC player.
} }
override fun selectVideo(video: Track) { override fun selectVideo(video: Track) {
player.selectTrack(video.id) val id = video.id.toIntOrNull() ?: return
} player.setPropertyInt("vid", id)
}
override fun selectAudio(audio: Track) { override fun selectAudio(audio: Track) {
player.selectTrack(audio.id) val id = audio.id.toIntOrNull() ?: return
} player.setPropertyInt("aid", id)
}
override fun selectSubtitle(subtitle: Track?) { override fun selectSubtitle(subtitle: Track?) {
when (subtitle) { when (subtitle) {
null -> player.unselectTrackType(IMedia.Track.Type.Text) null -> player.setPropertyString("sid", "no")
else -> player.selectTrack(subtitle.id) else -> {
} val id = subtitle.id.toIntOrNull() ?: return
} player.setPropertyInt("sid", id)
}
}
}
override fun selectRendition(rendition: Rendition?) { override fun selectRendition(rendition: Rendition?) {
// Not supported by libVLC MediaPlayer track APIs. // 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.facebook.react.uimanager.ThemedReactContext
import com.margelo.nitro.omni.HybridOmniPlayerFactorySpec import com.margelo.nitro.omni.HybridOmniPlayerFactorySpec
import com.margelo.nitro.omni.HybridOmniPlayerPropsSpec
import com.margelo.nitro.omni.HybridOmniPlayerSpec import com.margelo.nitro.omni.HybridOmniPlayerSpec
import com.margelo.nitro.omni.Source
class OmniPlayerFactory(val context: ThemedReactContext): HybridOmniPlayerFactorySpec() { class OmniPlayerFactory(val context: ThemedReactContext): HybridOmniPlayerFactorySpec() {
override fun createPlayer(props: HybridOmniPlayerPropsSpec): HybridOmniPlayerSpec { override fun createPlayer(props: Source): HybridOmniPlayerSpec {
return OmniPlayer().apply { return OmniPlayer().apply {
source = props source = props
} }
@@ -1,23 +1,43 @@
package dev.zoriya.omni package dev.zoriya.omni
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.View import android.view.View
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
import org.videolan.libvlc.util.VLCVideoLayout
class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec() { class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec() {
private val videoLayout = VLCVideoLayout(context) private val surfaceView = SurfaceView(context)
private var viewsAttached = false private var surfaceReady = false
private var boundPlayer: OmniPlayer? = null private var boundPlayer: OmniPlayer? = null
override val view: View = videoLayout 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() {
if (!::player.isInitialized) { if (!::player.isInitialized) {
throw IllegalStateException("Player is null in OmniView") throw IllegalStateException("Player is null in OmniView")
@@ -25,17 +45,14 @@ class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec() {
val omniPlayer = player as? OmniPlayer val omniPlayer = player as? OmniPlayer
?: throw IllegalStateException("Player is not an OmniPlayer in OmniView") ?: throw IllegalStateException("Player is not an OmniPlayer in OmniView")
val vout = omniPlayer.player.vlcVout
if (boundPlayer !== omniPlayer) { if (boundPlayer !== omniPlayer) {
boundPlayer?.player?.detachViews() boundPlayer?.setSurface(null)
viewsAttached = false
boundPlayer = omniPlayer boundPlayer = omniPlayer
} }
if (!viewsAttached || !vout.areViewsAttached()) { if (surfaceReady) {
omniPlayer.player.attachViews(videoLayout, null, false, false) omniPlayer.setSurface(surfaceView.holder.surface)
viewsAttached = true
} }
if (autoplay == true && !omniPlayer.isPlaying) { if (autoplay == true && !omniPlayer.isPlaying) {
@@ -47,11 +64,7 @@ class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec() {
if (!::player.isInitialized) return if (!::player.isInitialized) return
val omniPlayer = player as? OmniPlayer ?: return val omniPlayer = player as? OmniPlayer ?: return
val vout = omniPlayer.player.vlcVout omniPlayer.setSurface(null)
if (vout.areViewsAttached()) {
omniPlayer.player.detachViews()
}
viewsAttached = false
boundPlayer = null boundPlayer = null
} }
} }
@@ -16,6 +16,6 @@ inline fun <T : Any> deferredObservable(
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
val old = this.value val old = this.value
this.value = value this.value = value
onChange(old, value) onChange(thisRef, old, value)
} }
} }
+1 -1
View File
@@ -1,7 +1,7 @@
buildscript { buildscript {
ext { ext {
buildToolsVersion = "36.0.0" buildToolsVersion = "36.0.0"
minSdkVersion = 24 minSdkVersion = 26
compileSdkVersion = 36 compileSdkVersion = 36
targetSdkVersion = 36 targetSdkVersion = 36
ndkVersion = "27.1.12297006" ndkVersion = "27.1.12297006"
@@ -207,6 +207,15 @@ namespace margelo::nitro::omni {
static const auto method = _javaPart->javaClassStatic()->getMethod<void(double /* volume */)>("setVolume"); static const auto method = _javaPart->javaClassStatic()->getMethod<void(double /* volume */)>("setVolume");
method(_javaPart, volume); 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 // Methods
void JHybridOmniPlayerSpec::play() { void JHybridOmniPlayerSpec::play() {
@@ -69,6 +69,8 @@ namespace margelo::nitro::omni {
void setPlaybackRate(double playbackRate) override; void setPlaybackRate(double playbackRate) override;
double getVolume() override; double getVolume() override;
void setVolume(double volume) override; void setVolume(double volume) override;
bool getMuted() override;
void setMuted(bool muted) override;
public: public:
// Methods // Methods
@@ -92,6 +92,12 @@ abstract class HybridOmniPlayerSpec: HybridObject() {
@set:DoNotStrip @set:DoNotStrip
@set:Keep @set:Keep
abstract var volume: Double abstract var volume: Double
@get:DoNotStrip
@get:Keep
@set:DoNotStrip
@set:Keep
abstract var muted: Boolean
// Methods // Methods
@DoNotStrip @DoNotStrip
+2
View File
@@ -33,6 +33,8 @@ namespace margelo::nitro::omni {
prototype.registerHybridSetter("playbackRate", &HybridOmniPlayerSpec::setPlaybackRate); prototype.registerHybridSetter("playbackRate", &HybridOmniPlayerSpec::setPlaybackRate);
prototype.registerHybridGetter("volume", &HybridOmniPlayerSpec::getVolume); prototype.registerHybridGetter("volume", &HybridOmniPlayerSpec::getVolume);
prototype.registerHybridSetter("volume", &HybridOmniPlayerSpec::setVolume); prototype.registerHybridSetter("volume", &HybridOmniPlayerSpec::setVolume);
prototype.registerHybridGetter("muted", &HybridOmniPlayerSpec::getMuted);
prototype.registerHybridSetter("muted", &HybridOmniPlayerSpec::setMuted);
prototype.registerHybridMethod("play", &HybridOmniPlayerSpec::play); prototype.registerHybridMethod("play", &HybridOmniPlayerSpec::play);
prototype.registerHybridMethod("pause", &HybridOmniPlayerSpec::pause); prototype.registerHybridMethod("pause", &HybridOmniPlayerSpec::pause);
prototype.registerHybridMethod("seekBy", &HybridOmniPlayerSpec::seekBy); prototype.registerHybridMethod("seekBy", &HybridOmniPlayerSpec::seekBy);
+2
View File
@@ -79,6 +79,8 @@ namespace margelo::nitro::omni {
virtual void setPlaybackRate(double playbackRate) = 0; virtual void setPlaybackRate(double playbackRate) = 0;
virtual double getVolume() = 0; virtual double getVolume() = 0;
virtual void setVolume(double volume) = 0; virtual void setVolume(double volume) = 0;
virtual bool getMuted() = 0;
virtual void setMuted(bool muted) = 0;
public: public:
// Methods // Methods
+1 -1
View File
@@ -23,7 +23,7 @@ in
nodejs nodejs
biome biome
jdk jdk
((android-studio.withSdk android.androidsdk).overrideAttrs {forceWayland = true;}) ((android-studio.override {forceWayland = true;}).withSdk android.androidsdk)
]; ];
shellHook = '' shellHook = ''
+6 -6
View File
@@ -1,17 +1,17 @@
import type { HybridObject } from "react-native-nitro-modules"; import type { HybridObject } from "react-native-nitro-modules";
import type { OmniEvents } from "../types/events"; import type { OmniEvents } from "../types/events";
import type { import type {
OmniPlayerState, // OmniPlayerState,
OmniPlayer as OmniPlayerT, OmniPlayer as OmniPlayerT,
} from "../types/player"; } from "../types/player";
import type { Source } from "../types/source"; import type { Source } from "../types/source";
export interface OmniEventMap extends HybridObject<{ android: "kotlin" }> { export interface OmniEventMap extends HybridObject<{ android: "kotlin" }> {
addStateListener( // addStateListener(
key: keyof OmniPlayerState, // key: keyof OmniPlayerState,
cb: (value: number) => void, // cb: (value: number) => void,
): void; // ): void;
removeStateListener(cb: OmniEvents["end"]): void; // removeStateListener(cb: OmniEvents["end"]): void;
addOnEndListener(cb: OmniEvents["end"]): void; addOnEndListener(cb: OmniEvents["end"]): void;
removeOnEndListener(cb: OmniEvents["end"]): void; removeOnEndListener(cb: OmniEvents["end"]): void;
+1
View File
@@ -32,6 +32,7 @@ export interface OmniPlayerState {
playbackRate: number; playbackRate: number;
// between 0 and 1 // between 0 and 1
volume: number; volume: number;
muted: boolean;
} }
export type PlayerStatus = "idle" | "loading" | "readyToPlay" | "error"; export type PlayerStatus = "idle" | "loading" | "readyToPlay" | "error";