mirror of
https://github.com/zoriya/react-native-omni.git
synced 2026-08-05 05:36:17 +00:00
Start using media3 player as a wrapper
This commit is contained in:
@@ -144,6 +144,9 @@ dependencies {
|
||||
implementation project(":react-native-nitro-modules")
|
||||
|
||||
implementation("dev.jdtech.mpv:libmpv:1.0.0")
|
||||
implementation("androidx.media3:media3-common:1.4.1")
|
||||
implementation("androidx.media3:media3-session:1.4.1")
|
||||
implementation("androidx.media3:media3-ui:1.4.1")
|
||||
}
|
||||
|
||||
if (isNewArchitectureEnabled()) {
|
||||
|
||||
@@ -0,0 +1,388 @@
|
||||
package dev.zoriya.omni
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Looper
|
||||
import android.view.Surface
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.PlaybackParameters
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.SimpleBasePlayer
|
||||
import androidx.media3.common.util.Size
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.margelo.nitro.omni.Source
|
||||
import com.margelo.nitro.omni.Track
|
||||
import dev.jdtech.mpv.MPVLib
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
class MpvPlayer(ctx: Context) : SimpleBasePlayer(Looper.getMainLooper()), MPVLib.EventObserver {
|
||||
val mpv: MPVLib = MPVLib.create(ctx) ?: throw Error("Failed to initialize MPVLib")
|
||||
|
||||
private var source: Source? = null
|
||||
private var mediaItem: MediaItem? = null
|
||||
private var released = false
|
||||
private var surfaceSize: Size = Size.UNKNOWN
|
||||
|
||||
private val availableCommands: Player.Commands = Player.Commands.Builder()
|
||||
.add(COMMAND_PLAY_PAUSE)
|
||||
.add(COMMAND_PREPARE)
|
||||
.add(COMMAND_STOP)
|
||||
.add(COMMAND_RELEASE)
|
||||
.add(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM)
|
||||
.add(COMMAND_SEEK_BACK)
|
||||
.add(COMMAND_SEEK_FORWARD)
|
||||
.add(COMMAND_SET_SPEED_AND_PITCH)
|
||||
.add(COMMAND_GET_VOLUME)
|
||||
.add(COMMAND_SET_VOLUME)
|
||||
.add(COMMAND_SET_VIDEO_SURFACE)
|
||||
.add(COMMAND_GET_CURRENT_MEDIA_ITEM)
|
||||
.add(COMMAND_GET_METADATA)
|
||||
.add(COMMAND_GET_TIMELINE)
|
||||
.add(Player.COMMAND_GET_MEDIA_ITEMS_METADATA)
|
||||
.build()
|
||||
|
||||
init {
|
||||
mpv.setOptionString("vo", "gpu-next")
|
||||
mpv.setOptionString("force-window", "yes")
|
||||
mpv.setOptionString("gpu-context", "android")
|
||||
mpv.setOptionString("opengl-es", "yes")
|
||||
mpv.setOptionString("hwdec", "mediacodec-copy")
|
||||
mpv.setOptionString("profile", "fast")
|
||||
|
||||
mpv.setOptionString("cache", "yes")
|
||||
mpv.setOptionString("cache-pause-initial", "yes")
|
||||
mpv.setOptionString("demuxer-max-bytes", "150MiB")
|
||||
mpv.setOptionString("demuxer-max-back-bytes", "75MiB")
|
||||
mpv.setOptionString("demuxer-readahead-secs", "20")
|
||||
|
||||
mpv.setOptionString("save-position-on-quit", "no")
|
||||
mpv.setOptionString("ytdl", "no")
|
||||
mpv.setOptionString("hr-seek", "no")
|
||||
|
||||
mpv.init()
|
||||
mpv.addObserver(this)
|
||||
mpv.observeProperty("core-idle", MPVLib.MpvFormat.MPV_FORMAT_FLAG)
|
||||
mpv.observeProperty("paused-for-cache", MPVLib.MpvFormat.MPV_FORMAT_FLAG)
|
||||
mpv.observeProperty("pause", MPVLib.MpvFormat.MPV_FORMAT_FLAG)
|
||||
mpv.observeProperty("eof-reached", MPVLib.MpvFormat.MPV_FORMAT_FLAG)
|
||||
mpv.observeProperty("time-pos", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE)
|
||||
mpv.observeProperty("demuxer-cache-time", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE)
|
||||
mpv.observeProperty("duration", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE)
|
||||
mpv.observeProperty("speed", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE)
|
||||
mpv.observeProperty("volume", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE)
|
||||
}
|
||||
|
||||
fun setSource(value: Source) {
|
||||
source = value
|
||||
mediaItem = buildMediaItem(value)
|
||||
|
||||
mpv.command(arrayOf("stop"))
|
||||
|
||||
val src = value.src.firstOrNull()
|
||||
if (src == null) {
|
||||
invalidateState()
|
||||
return
|
||||
}
|
||||
|
||||
mpv.setPropertyString(
|
||||
"http-header-fields",
|
||||
src.headers.entries.joinToString("\r\n") { "${it.key}: ${it.value}" }
|
||||
)
|
||||
mpv.setPropertyDouble("start", (value.startTime ?: 0.0).coerceAtLeast(0.0))
|
||||
mpv.command(arrayOf("loadfile", src.uri, "replace"))
|
||||
for (subtitle in value.subtitles) {
|
||||
mpv.command(arrayOf("sub-add", subtitle.link, "cached"))
|
||||
}
|
||||
|
||||
invalidateState()
|
||||
}
|
||||
|
||||
fun setSurface(surface: Surface?) {
|
||||
if (surface == null) {
|
||||
mpv.setOptionString("vo", "null")
|
||||
mpv.setOptionString("force-window", "no")
|
||||
mpv.detachSurface()
|
||||
surfaceSize = Size.UNKNOWN
|
||||
} else {
|
||||
mpv.attachSurface(surface)
|
||||
mpv.setOptionString("force-window", "yes")
|
||||
mpv.setOptionString("vo", "gpu-next")
|
||||
}
|
||||
invalidateState()
|
||||
}
|
||||
|
||||
fun setSurfaceSize(width: Int, height: Int) {
|
||||
if (width <= 0 || height <= 0) return
|
||||
surfaceSize = Size(width, height)
|
||||
mpv.setPropertyString("android-surface-size", "${width}x${height}")
|
||||
invalidateState()
|
||||
}
|
||||
|
||||
fun getTracks(type: String): Array<Track> {
|
||||
val count = mpv.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 = mpv.getPropertyString("$base/type") ?: continue
|
||||
if (trackType != type) continue
|
||||
|
||||
val id = mpv.getPropertyInt("$base/id") ?: continue
|
||||
val selected = mpv.getPropertyBoolean("$base/selected") ?: false
|
||||
val label = mpv.getPropertyString("$base/title") ?: mpv.getPropertyString("$base/codec")
|
||||
val language = mpv.getPropertyString("$base/lang")
|
||||
|
||||
tracks.add(
|
||||
Track(
|
||||
id = id.toString(),
|
||||
label = label,
|
||||
language = language,
|
||||
selected = selected
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return tracks.toTypedArray()
|
||||
}
|
||||
|
||||
fun selectVideo(video: Track) {
|
||||
val id = video.id.toIntOrNull() ?: return
|
||||
mpv.setPropertyInt("vid", id)
|
||||
}
|
||||
|
||||
fun selectAudio(audio: Track) {
|
||||
val id = audio.id.toIntOrNull() ?: return
|
||||
mpv.setPropertyInt("aid", id)
|
||||
}
|
||||
|
||||
fun selectSubtitle(subtitle: Track?) {
|
||||
when (subtitle) {
|
||||
null -> mpv.setPropertyString("sid", "no")
|
||||
else -> {
|
||||
val id = subtitle.id.toIntOrNull() ?: return
|
||||
mpv.setPropertyInt("sid", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun seekBy(offsetSeconds: Double) {
|
||||
mpv.command(arrayOf("seek", offsetSeconds.toString(), "relative"))
|
||||
invalidateState()
|
||||
}
|
||||
|
||||
fun getCurrentTimeSeconds(): Double {
|
||||
return (mpv.getPropertyDouble("time-pos") ?: 0.0).coerceAtLeast(0.0)
|
||||
}
|
||||
|
||||
fun setCurrentTimeSeconds(value: Double) {
|
||||
mpv.command(arrayOf("seek", value.coerceAtLeast(0.0).toString(), "absolute"))
|
||||
invalidateState()
|
||||
}
|
||||
|
||||
fun getBufferedSeconds(): Double {
|
||||
return (mpv.getPropertyDouble("demuxer-cache-time") ?: 0.0).coerceAtLeast(0.0)
|
||||
}
|
||||
|
||||
fun getDurationSeconds(): Double {
|
||||
return (mpv.getPropertyDouble("duration") ?: 0.0).coerceAtLeast(0.0)
|
||||
}
|
||||
|
||||
fun getPlaybackRate(): Double {
|
||||
return mpv.getPropertyDouble("speed") ?: 1.0
|
||||
}
|
||||
|
||||
fun setPlaybackRate(value: Double) {
|
||||
mpv.setPropertyDouble("speed", value.coerceAtLeast(0.0))
|
||||
invalidateState()
|
||||
}
|
||||
|
||||
fun isMuted(): Boolean {
|
||||
return mpv.getPropertyBoolean("mute") ?: false
|
||||
}
|
||||
|
||||
fun setMuted(value: Boolean) {
|
||||
mpv.setPropertyBoolean("mute", value)
|
||||
invalidateState()
|
||||
}
|
||||
|
||||
fun getVolume01(): Double {
|
||||
return (mpv.getPropertyDouble("volume") ?: 100.0).coerceIn(0.0, 100.0) / 100.0
|
||||
}
|
||||
|
||||
fun setVolume01(value: Double) {
|
||||
mpv.setPropertyDouble("volume", value.coerceIn(0.0, 1.0) * 100.0)
|
||||
invalidateState()
|
||||
}
|
||||
|
||||
fun isPlayingMpv(): Boolean {
|
||||
return !(mpv.getPropertyBoolean("pause") ?: true)
|
||||
}
|
||||
|
||||
fun hasPrevMetadata(): Boolean = source?.metadata?.hasPrev ?: false
|
||||
|
||||
fun hasNextMetadata(): Boolean = source?.metadata?.hasNext ?: false
|
||||
|
||||
private fun buildMediaItem(value: Source): MediaItem? {
|
||||
val src = value.src.firstOrNull() ?: return null
|
||||
val metadata = value.metadata
|
||||
val mediaMetadataBuilder = MediaMetadata.Builder()
|
||||
.setTitle(metadata?.title)
|
||||
.setAlbumTitle(metadata?.album)
|
||||
.setArtist(metadata?.artist)
|
||||
|
||||
metadata?.imageLink?.let {
|
||||
mediaMetadataBuilder.setArtworkUri(Uri.parse(it))
|
||||
}
|
||||
|
||||
return MediaItem.Builder()
|
||||
.setUri(src.uri)
|
||||
.setMediaId(src.uri)
|
||||
.setMediaMetadata(mediaMetadataBuilder.build())
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun playbackState(): Int {
|
||||
val ended = mpv.getPropertyBoolean("eof-reached") ?: false
|
||||
if (ended) return STATE_ENDED
|
||||
|
||||
val idle = mpv.getPropertyBoolean("core-idle") ?: false
|
||||
if (idle) return STATE_IDLE
|
||||
|
||||
val loading = mpv.getPropertyBoolean("paused-for-cache") ?: false
|
||||
return if (loading) STATE_BUFFERING else STATE_READY
|
||||
}
|
||||
|
||||
override fun getState(): State {
|
||||
val isPaused = mpv.getPropertyBoolean("pause") ?: true
|
||||
val speed = (mpv.getPropertyDouble("speed") ?: 1.0).toFloat().coerceAtLeast(0f)
|
||||
val volume = (mpv.getPropertyDouble("volume") ?: 100.0).coerceIn(0.0, 100.0).toFloat() / 100f
|
||||
val posMs = (getCurrentTimeSeconds() * 1000.0).toLong()
|
||||
val bufferedMs = (getBufferedSeconds() * 1000.0).toLong()
|
||||
val durationMs = (getDurationSeconds() * 1000.0).toLong()
|
||||
|
||||
val builder = State.Builder()
|
||||
.setAvailableCommands(availableCommands)
|
||||
.setPlayWhenReady(!isPaused, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST)
|
||||
.setPlaybackState(playbackState())
|
||||
.setPlaybackSuppressionReason(PLAYBACK_SUPPRESSION_REASON_NONE)
|
||||
.setIsLoading(mpv.getPropertyBoolean("paused-for-cache") ?: false)
|
||||
.setPlaybackParameters(PlaybackParameters(speed))
|
||||
.setVolume(volume)
|
||||
.setSurfaceSize(surfaceSize)
|
||||
.setContentPositionMs(posMs)
|
||||
.setContentBufferedPositionMs { posMs + bufferedMs }
|
||||
.setTotalBufferedDurationMs { bufferedMs }
|
||||
|
||||
val currentMediaItem = mediaItem
|
||||
if (currentMediaItem != null) {
|
||||
val mediaItemData = MediaItemData.Builder(currentMediaItem.mediaId)
|
||||
.setMediaItem(currentMediaItem)
|
||||
.setMediaMetadata(currentMediaItem.mediaMetadata)
|
||||
.setIsSeekable(true)
|
||||
.setDurationUs(if (durationMs > 0L) durationMs * 1000L else C.TIME_UNSET)
|
||||
.build()
|
||||
|
||||
builder
|
||||
.setPlaylist(listOf(mediaItemData))
|
||||
.setCurrentMediaItemIndex(0)
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
override fun handlePrepare(): ListenableFuture<*> {
|
||||
return Futures.immediateFuture(null)
|
||||
}
|
||||
|
||||
override fun handleSetPlayWhenReady(playWhenReady: Boolean): ListenableFuture<*> {
|
||||
mpv.setPropertyBoolean("pause", !playWhenReady)
|
||||
invalidateState()
|
||||
return Futures.immediateFuture(null)
|
||||
}
|
||||
|
||||
override fun handleSetPlaybackParameters(playbackParameters: PlaybackParameters): ListenableFuture<*> {
|
||||
mpv.setPropertyDouble("speed", playbackParameters.speed.coerceAtLeast(0f).toDouble())
|
||||
invalidateState()
|
||||
return Futures.immediateFuture(null)
|
||||
}
|
||||
|
||||
override fun handleSeek(
|
||||
mediaItemIndex: Int,
|
||||
positionMs: Long,
|
||||
seekCommand: Int
|
||||
): ListenableFuture<*> {
|
||||
if (mediaItemIndex != C.INDEX_UNSET && mediaItemIndex != 0) {
|
||||
return Futures.immediateFuture(null)
|
||||
}
|
||||
|
||||
val targetMs = if (positionMs == C.TIME_UNSET) 0L else positionMs.coerceAtLeast(0L)
|
||||
mpv.command(arrayOf("seek", (targetMs / 1000.0).toString(), "absolute"))
|
||||
invalidateState()
|
||||
return Futures.immediateFuture(null)
|
||||
}
|
||||
|
||||
override fun handleSetVolume(volume: Float): ListenableFuture<*> {
|
||||
mpv.setPropertyDouble("volume", volume.coerceIn(0f, 1f) * 100.0)
|
||||
invalidateState()
|
||||
return Futures.immediateFuture(null)
|
||||
}
|
||||
|
||||
override fun handleSetVideoOutput(videoOutput: Any): ListenableFuture<*> {
|
||||
if (videoOutput is Surface) {
|
||||
setSurface(videoOutput)
|
||||
}
|
||||
return Futures.immediateFuture(null)
|
||||
}
|
||||
|
||||
override fun handleClearVideoOutput(videoOutput: Any): ListenableFuture<*> {
|
||||
if (videoOutput is Surface) {
|
||||
setSurface(null)
|
||||
}
|
||||
return Futures.immediateFuture(null)
|
||||
}
|
||||
|
||||
override fun handleStop(): ListenableFuture<*> {
|
||||
mpv.command(arrayOf("stop"))
|
||||
invalidateState()
|
||||
return Futures.immediateFuture(null)
|
||||
}
|
||||
|
||||
override fun handleRelease(): ListenableFuture<*> {
|
||||
if (!released) {
|
||||
released = true
|
||||
mpv.removeObserver(this)
|
||||
mpv.detachSurface()
|
||||
mpv.destroy()
|
||||
}
|
||||
return Futures.immediateFuture(null)
|
||||
}
|
||||
|
||||
override fun event(event: Int) {
|
||||
invalidateState()
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String) {
|
||||
invalidateState()
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String, value: Long) {
|
||||
invalidateState()
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String, value: Double) {
|
||||
invalidateState()
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String, value: Boolean) {
|
||||
invalidateState()
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String, value: String) {
|
||||
invalidateState()
|
||||
}
|
||||
}
|
||||
@@ -1,145 +1,96 @@
|
||||
package dev.zoriya.omni
|
||||
|
||||
import android.util.Log
|
||||
import androidx.media3.common.Player
|
||||
import com.margelo.nitro.NitroModules
|
||||
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.Source
|
||||
import com.margelo.nitro.omni.Track
|
||||
import dev.jdtech.mpv.MPVLib
|
||||
import dev.zoriya.omni.utils.deferredObservable
|
||||
|
||||
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)
|
||||
|
||||
init {
|
||||
// `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("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")
|
||||
|
||||
player.setOptionString("save-position-on-quit", "no")
|
||||
player.setOptionString("ytdl", "no")
|
||||
|
||||
// seek to keyframes
|
||||
player.setOptionString("hr-seek", "no")
|
||||
|
||||
player.init()
|
||||
}
|
||||
val player = MpvPlayer(ctx)
|
||||
override val eventMap = EventMap(player.mpv)
|
||||
private val mediaSessionBridge = OmniMediaSessionBridge(ctx, this)
|
||||
|
||||
override fun dispose() {
|
||||
super.dispose()
|
||||
|
||||
player.detachSurface()
|
||||
mediaSessionBridge.dispose()
|
||||
eventMap.dispose()
|
||||
player.destroy()
|
||||
player.release()
|
||||
}
|
||||
|
||||
override var source: Source by deferredObservable { _, _, new ->
|
||||
Log.e("omni", "Chaning source")
|
||||
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.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"))
|
||||
}
|
||||
player.setSource(new)
|
||||
mediaSessionBridge.onSourceChanged()
|
||||
}
|
||||
|
||||
fun setSurface(surface: android.view.Surface?) {
|
||||
if (surface == null) {
|
||||
player.setOptionString("vo", "null")
|
||||
player.setOptionString("force-window", "no")
|
||||
player.detachSurface()
|
||||
} else {
|
||||
player.attachSurface(surface)
|
||||
player.setOptionString("force-window", "yes")
|
||||
player.setOptionString("vo", "gpu-next")
|
||||
}
|
||||
player.setSurface(surface)
|
||||
}
|
||||
|
||||
fun setSurfaceSize(width: Int, height: Int) {
|
||||
if (width <= 0 || height <= 0) return
|
||||
player.setPropertyString("android-surface-size", "${width}x${height}")
|
||||
player.setSurfaceSize(width, height)
|
||||
}
|
||||
|
||||
override val hasPrev get() = source.metadata?.hasPrev ?: false
|
||||
override val hasNext get() = source.metadata?.hasNext ?: false
|
||||
override val hasPrev get() = player.hasPrevMetadata()
|
||||
override val hasNext get() = player.hasNextMetadata()
|
||||
override val status: PlayerStatus
|
||||
get() {
|
||||
val loading = player.getPropertyBoolean("paused-for-cache") ?: false
|
||||
val idle = player.getPropertyBoolean("core-idle") ?: false
|
||||
return when {
|
||||
loading -> PlayerStatus.LOADING
|
||||
idle -> PlayerStatus.IDLE
|
||||
get() = when (player.playbackState) {
|
||||
Player.STATE_IDLE -> PlayerStatus.IDLE
|
||||
Player.STATE_ENDED -> PlayerStatus.IDLE
|
||||
Player.STATE_BUFFERING -> PlayerStatus.LOADING
|
||||
else -> PlayerStatus.READYTOPLAY
|
||||
}
|
||||
}
|
||||
override val isPlaying get() = !(player.getPropertyBoolean("pause") ?: true)
|
||||
|
||||
override val isPlaying get() = player.isPlaying
|
||||
override var currentTime
|
||||
get() = (player.getPropertyDouble("time-pos") ?: 0.0).coerceAtLeast(0.0)
|
||||
get() = player.getCurrentTimeSeconds()
|
||||
set(value) {
|
||||
player.command(arrayOf("seek", value.coerceAtLeast(0.0).toString(), "absolute"))
|
||||
player.setCurrentTimeSeconds(value)
|
||||
}
|
||||
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)
|
||||
override val buffered get() = player.getBufferedSeconds()
|
||||
override val duration get() = player.getDurationSeconds()
|
||||
|
||||
override var playbackRate
|
||||
get() = player.getPropertyDouble("speed") ?: 1.0
|
||||
get() = player.getPlaybackRate()
|
||||
set(value) {
|
||||
player.setPropertyDouble("speed", value.coerceAtLeast(0.0))
|
||||
player.setPlaybackRate(value)
|
||||
}
|
||||
|
||||
override var muted
|
||||
get() = player.getPropertyBoolean("mute") ?: false
|
||||
get() = player.isMuted()
|
||||
set(value) {
|
||||
player.setPropertyBoolean("mute", value)
|
||||
player.setMuted(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 var volume
|
||||
get() = player.getVolume01()
|
||||
set(value) {
|
||||
player.setVolume01(value)
|
||||
}
|
||||
|
||||
override val videos get() = player.getTracks("video")
|
||||
override val audios get() = player.getTracks("audio")
|
||||
override val subtitles get() = player.getTracks("sub")
|
||||
|
||||
override val rendition: Array<Rendition> get() = emptyArray()
|
||||
|
||||
override fun play() {
|
||||
player.setPropertyBoolean("pause", false)
|
||||
player.play()
|
||||
}
|
||||
|
||||
override fun pause() {
|
||||
player.setPropertyBoolean("pause", true)
|
||||
player.pause()
|
||||
}
|
||||
|
||||
override fun seekBy(offset: Double) {
|
||||
player.command(arrayOf("seek", offset.toString(), "relative"))
|
||||
player.seekBy(offset)
|
||||
}
|
||||
|
||||
override fun playPrev() {
|
||||
@@ -151,55 +102,17 @@ class OmniPlayer : HybridOmniPlayerSpec() {
|
||||
}
|
||||
|
||||
override fun selectVideo(video: Track) {
|
||||
val id = video.id.toIntOrNull() ?: return
|
||||
player.setPropertyInt("vid", id)
|
||||
player.selectVideo(video)
|
||||
}
|
||||
|
||||
override fun selectAudio(audio: Track) {
|
||||
val id = audio.id.toIntOrNull() ?: return
|
||||
player.setPropertyInt("aid", id)
|
||||
player.selectAudio(audio)
|
||||
}
|
||||
|
||||
override fun selectSubtitle(subtitle: Track?) {
|
||||
when (subtitle) {
|
||||
null -> player.setPropertyString("sid", "no")
|
||||
else -> {
|
||||
val id = subtitle.id.toIntOrNull() ?: return
|
||||
player.setPropertyInt("sid", id)
|
||||
}
|
||||
}
|
||||
player.selectSubtitle(subtitle)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user