mirror of
https://github.com/zoriya/react-native-omni.git
synced 2026-08-04 21:26:48 +00:00
Migrate to mpv
This commit is contained in:
@@ -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,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,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
|
||||
|
||||
+6
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = ''
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface OmniPlayerState {
|
||||
playbackRate: number;
|
||||
// between 0 and 1
|
||||
volume: number;
|
||||
muted: boolean;
|
||||
}
|
||||
|
||||
export type PlayerStatus = "idle" | "loading" | "readyToPlay" | "error";
|
||||
|
||||
Reference in New Issue
Block a user