Rework mpv player to use media3 interface

This commit is contained in:
2026-04-21 13:22:31 +02:00
parent a68eb16fb3
commit 62f6575f9f
3 changed files with 494 additions and 332 deletions
+317 -287
View File
@@ -2,34 +2,70 @@ package dev.zoriya.omni
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.net.Uri import android.os.Bundle
import android.os.Looper import android.os.Looper
import android.view.Surface import android.view.Surface
import android.view.SurfaceHolder
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.Format
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata import androidx.media3.common.MediaMetadata
import androidx.media3.common.MimeTypes
import androidx.media3.common.PlaybackParameters import androidx.media3.common.PlaybackParameters
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.SimpleBasePlayer import androidx.media3.common.SimpleBasePlayer
import androidx.media3.common.util.Size import androidx.media3.common.TrackGroup
import androidx.media3.common.TrackSelectionParameters
import androidx.media3.common.Tracks
import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import com.margelo.nitro.omni.Source
import com.margelo.nitro.omni.Track
import dev.jdtech.mpv.MPVLib import dev.jdtech.mpv.MPVLib
@SuppressLint("UnsafeOptInUsageError") @SuppressLint("UnsafeOptInUsageError")
class MpvPlayer(ctx: Context) : SimpleBasePlayer(Looper.getMainLooper()), MPVLib.EventObserver { class MpvPlayer(ctx: Context) : SimpleBasePlayer(Looper.getMainLooper()), MPVLib.EventObserver {
val mpv: MPVLib = MPVLib.create(ctx) ?: throw Error("Failed to initialize MPVLib") internal val mpv = (MPVLib.create(ctx) ?: throw Error("Failed to initialize MPVLib")).apply {
setOptionString("vo", "gpu-next")
setOptionString("force-window", "yes")
setOptionString("gpu-context", "android")
setOptionString("opengl-es", "yes")
setOptionString("hwdec", "mediacodec-copy")
setOptionString("profile", "fast")
private var source: Source? = null setOptionString("cache", "yes")
private var mediaItem: MediaItem? = null setOptionString("cache-pause-initial", "yes")
private var released = false setOptionString("demuxer-max-bytes", "150MiB")
private var surfaceSize: Size = Size.UNKNOWN setOptionString("demuxer-max-back-bytes", "75MiB")
setOptionString("demuxer-readahead-secs", "20")
setOptionString("save-position-on-quit", "no")
setOptionString("ytdl", "no")
setOptionString("hr-seek", "no")
init()
addObserver(this@MpvPlayer)
observeProperty("core-idle", MPVLib.MpvFormat.MPV_FORMAT_FLAG)
observeProperty("paused-for-cache", MPVLib.MpvFormat.MPV_FORMAT_FLAG)
observeProperty("pause", MPVLib.MpvFormat.MPV_FORMAT_FLAG)
observeProperty("eof-reached", MPVLib.MpvFormat.MPV_FORMAT_FLAG)
observeProperty("time-pos", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE)
observeProperty("demuxer-cache-time", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE)
observeProperty("duration", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE)
observeProperty("speed", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE)
observeProperty("volume", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE)
observeProperty("vid", MPVLib.MpvFormat.MPV_FORMAT_INT64)
observeProperty("aid", MPVLib.MpvFormat.MPV_FORMAT_INT64)
observeProperty("sid", MPVLib.MpvFormat.MPV_FORMAT_INT64)
}
// private data class TrackGroupBinding(val group: TrackGroup, val mpvIds: IntArray)
// private var currentMediaItem: MediaItem? = null
// private var currentTrackSelectionParameters: TrackSelectionParameters =
// TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT
// private var trackGroupsByType: Map<Int, List<TrackGroupBinding>> = emptyMap()
private val availableCommands: Player.Commands = Player.Commands.Builder() private val availableCommands: Player.Commands = Player.Commands.Builder()
.add(COMMAND_PLAY_PAUSE) .add(COMMAND_PLAY_PAUSE)
.add(COMMAND_PREPARE)
.add(COMMAND_STOP) .add(COMMAND_STOP)
.add(COMMAND_RELEASE) .add(COMMAND_RELEASE)
.add(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM) .add(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM)
@@ -39,338 +75,185 @@ class MpvPlayer(ctx: Context) : SimpleBasePlayer(Looper.getMainLooper()), MPVLib
.add(COMMAND_GET_VOLUME) .add(COMMAND_GET_VOLUME)
.add(COMMAND_SET_VOLUME) .add(COMMAND_SET_VOLUME)
.add(COMMAND_SET_VIDEO_SURFACE) .add(COMMAND_SET_VIDEO_SURFACE)
.add(COMMAND_SET_MEDIA_ITEM)
.add(COMMAND_GET_CURRENT_MEDIA_ITEM) .add(COMMAND_GET_CURRENT_MEDIA_ITEM)
.add(COMMAND_GET_METADATA) .add(COMMAND_GET_METADATA)
.add(COMMAND_GET_TIMELINE) .add(COMMAND_GET_TIMELINE)
.add(Player.COMMAND_GET_MEDIA_ITEMS_METADATA) .add(COMMAND_GET_TRACKS)
.add(COMMAND_SET_TRACK_SELECTION_PARAMETERS)
.build() .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 { override fun getState(): State {
val isPaused = mpv.getPropertyBoolean("pause") ?: true val positionMs = ((mpv.getPropertyDouble("time-pos") ?: 0.0).coerceAtLeast(0.0) * 1000.0).toLong()
val speed = (mpv.getPropertyDouble("speed") ?: 1.0).toFloat().coerceAtLeast(0f) val cacheMs = ((mpv.getPropertyDouble("demuxer-cache-time") ?: 0.0).coerceAtLeast(0.0) * 1000.0).toLong()
val volume = (mpv.getPropertyDouble("volume") ?: 100.0).coerceIn(0.0, 100.0).toFloat() / 100f val durationMs = ((mpv.getPropertyDouble("duration") ?: 0.0).coerceAtLeast(0.0) * 1000.0).toLong()
val posMs = (getCurrentTimeSeconds() * 1000.0).toLong()
val bufferedMs = (getBufferedSeconds() * 1000.0).toLong()
val durationMs = (getDurationSeconds() * 1000.0).toLong()
val builder = State.Builder() val builder = State.Builder()
.setAvailableCommands(availableCommands) .setAvailableCommands(availableCommands)
.setPlayWhenReady(!isPaused, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST) .setPlayWhenReady(
.setPlaybackState(playbackState()) !(mpv.getPropertyBoolean("pause") ?: true),
.setPlaybackSuppressionReason(PLAYBACK_SUPPRESSION_REASON_NONE) Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST
)
.setPlaybackState(getMpvPlaybackState())
.setPlaybackSuppressionReason(Player.PLAYBACK_SUPPRESSION_REASON_NONE)
.setIsLoading(mpv.getPropertyBoolean("paused-for-cache") ?: false) .setIsLoading(mpv.getPropertyBoolean("paused-for-cache") ?: false)
.setPlaybackParameters(PlaybackParameters(speed)) .setPlaybackParameters(PlaybackParameters((mpv.getPropertyDouble("speed") ?: 1.0).toFloat().coerceAtLeast(0f)))
.setVolume(volume) .setVolume((mpv.getPropertyDouble("volume") ?: 100.0).coerceIn(0.0, 100.0).toFloat() / 100f)
.setSurfaceSize(surfaceSize) // .setTrackSelectionParameters(currentTrackSelectionParameters)
.setContentPositionMs(posMs) .setContentPositionMs(positionMs)
.setContentBufferedPositionMs { posMs + bufferedMs } .setContentBufferedPositionMs { cacheMs }
.setTotalBufferedDurationMs { bufferedMs } .setTotalBufferedDurationMs { cacheMs }
// .setPlaylistMetadata(currentMediaItem?.mediaMetadata ?: MediaMetadata.EMPTY)
val currentMediaItem = mediaItem // val item = currentMediaItem
if (currentMediaItem != null) { // if (item != null) {
val mediaItemData = MediaItemData.Builder(currentMediaItem.mediaId) // val itemBuilder = MediaItemData.Builder(item.mediaId)
.setMediaItem(currentMediaItem) // .setMediaItem(item)
.setMediaMetadata(currentMediaItem.mediaMetadata) // .setMediaMetadata(item.mediaMetadata)
.setIsSeekable(true) // .setIsSeekable(true)
.setDurationUs(if (durationMs > 0L) durationMs * 1000L else C.TIME_UNSET) // .setTracks(buildTracks())
.build() //
// if (durationMs > 0L) {
builder // itemBuilder.setDurationUs(durationMs * 1000L)
.setPlaylist(listOf(mediaItemData)) // }
.setCurrentMediaItemIndex(0) //
} // builder
// .setPlaylist(listOf(itemBuilder.build()))
// .setCurrentMediaItemIndex(0)
// }
return builder.build() return builder.build()
} }
override fun handlePrepare(): ListenableFuture<*> {
return Futures.immediateFuture(null)
}
override fun handleSetPlayWhenReady(playWhenReady: Boolean): ListenableFuture<*> { override fun handleSetPlayWhenReady(playWhenReady: Boolean): ListenableFuture<*> {
mpv.setPropertyBoolean("pause", !playWhenReady) mpv.setPropertyBoolean("pause", !playWhenReady)
invalidateState() return Futures.immediateVoidFuture()
return Futures.immediateFuture(null)
} }
override fun handleSetPlaybackParameters(playbackParameters: PlaybackParameters): ListenableFuture<*> { override fun handleSetPlaybackParameters(playbackParameters: PlaybackParameters): ListenableFuture<*> {
mpv.setPropertyDouble("speed", playbackParameters.speed.coerceAtLeast(0f).toDouble()) mpv.setPropertyDouble("speed", playbackParameters.speed.coerceAtLeast(0f).toDouble())
invalidateState() return Futures.immediateVoidFuture()
return Futures.immediateFuture(null)
} }
override fun handleSeek( override fun handleSeek(mediaItemIndex: Int, positionMs: Long, seekCommand: Int): ListenableFuture<*> {
mediaItemIndex: Int,
positionMs: Long,
seekCommand: Int
): ListenableFuture<*> {
if (mediaItemIndex != C.INDEX_UNSET && mediaItemIndex != 0) { if (mediaItemIndex != C.INDEX_UNSET && mediaItemIndex != 0) {
return Futures.immediateFuture(null) return Futures.immediateVoidFuture()
} }
val targetMs = if (positionMs == C.TIME_UNSET) 0L else positionMs.coerceAtLeast(0L) val targetMs = if (positionMs == C.TIME_UNSET) 0L else positionMs.coerceAtLeast(0L)
mpv.command(arrayOf("seek", (targetMs / 1000.0).toString(), "absolute")) mpv.command(arrayOf("seek", (targetMs / 1000.0).toString(), "absolute"))
invalidateState() return Futures.immediateVoidFuture()
return Futures.immediateFuture(null)
} }
override fun handleSetVolume(volume: Float): ListenableFuture<*> { override fun handleSetVolume(volume: Float): ListenableFuture<*> {
mpv.setPropertyDouble("volume", volume.coerceIn(0f, 1f) * 100.0) mpv.setPropertyDouble("volume", volume.coerceIn(0f, 1f) * 100.0)
invalidateState() return Futures.immediateVoidFuture()
return Futures.immediateFuture(null)
} }
override fun handleSetVideoOutput(videoOutput: Any): ListenableFuture<*> { override fun handleSetVideoOutput(videoOutput: Any): ListenableFuture<*> {
if (videoOutput is Surface) { when (videoOutput) {
setSurface(videoOutput) is Surface -> {
mpv.attachSurface(videoOutput)
mpv.setOptionString("force-window", "yes")
mpv.setOptionString("vo", "gpu-next")
}
is SurfaceHolder -> {
val surface = videoOutput.surface
if (surface != null && surface.isValid) {
mpv.attachSurface(surface)
mpv.setOptionString("force-window", "yes")
mpv.setOptionString("vo", "gpu-next")
val frame = videoOutput.surfaceFrame
val width = frame?.width() ?: 0
val height = frame?.height() ?: 0
if (width > 0 && height > 0) {
mpv.setPropertyString("android-surface-size", "${width}x${height}")
}
}
}
} }
return Futures.immediateFuture(null) return Futures.immediateVoidFuture()
} }
override fun handleClearVideoOutput(videoOutput: Any): ListenableFuture<*> { override fun handleClearVideoOutput(videoOutput: Any?): ListenableFuture<*> {
if (videoOutput is Surface) { mpv.setOptionString("vo", "null")
setSurface(null) mpv.setOptionString("force-window", "no")
} mpv.detachSurface()
return Futures.immediateFuture(null) return Futures.immediateVoidFuture()
} }
// override fun handleSetMediaItems(
// mediaItems: List<MediaItem>,
// startIndex: Int,
// startPositionMs: Long
// ): ListenableFuture<*> {
// val target = when {
// mediaItems.isEmpty() -> null
// startIndex in mediaItems.indices -> mediaItems[startIndex]
// else -> mediaItems[0]
// }
//
// currentMediaItem = target
// trackGroupsByType = emptyMap()
// mpv.command(arrayOf("stop"))
//
// if (target != null) {
// val uri = target.localConfiguration?.uri?.toString()
// if (!uri.isNullOrEmpty()) {
// val requestExtras = target.requestMetadata.extras
// applyRequestHeaders(requestExtras)
//
// val requestStartMs = requestExtras?.getLong(REQUEST_START_MS_KEY, C.TIME_UNSET) ?: C.TIME_UNSET
// val start = if (startPositionMs != C.TIME_UNSET) startPositionMs else requestStartMs
// if (start != C.TIME_UNSET && start >= 0L) {
// mpv.setPropertyDouble("start", start.toDouble() / 1000.0)
// }
//
// mpv.command(arrayOf("loadfile", uri, "replace"))
// target.localConfiguration?.subtitleConfigurations?.forEach { subtitle ->
// mpv.command(arrayOf("sub-add", subtitle.uri.toString(), "cached"))
// }
// }
// }
//
// return Futures.immediateVoidFuture()
// }
// override fun handleSetTrackSelectionParameters(trackSelectionParameters: TrackSelectionParameters): ListenableFuture<*> {
// currentTrackSelectionParameters = trackSelectionParameters
// applyTrackSelectionParameters(trackSelectionParameters)
// return Futures.immediateVoidFuture()
// }
override fun handleStop(): ListenableFuture<*> { override fun handleStop(): ListenableFuture<*> {
mpv.command(arrayOf("stop")) mpv.command(arrayOf("stop"))
invalidateState() return Futures.immediateVoidFuture()
return Futures.immediateFuture(null)
} }
override fun handleRelease(): ListenableFuture<*> { override fun handleRelease(): ListenableFuture<*> {
if (!released) { mpv.removeObserver(this)
released = true mpv.detachSurface()
mpv.removeObserver(this) mpv.destroy()
mpv.detachSurface() return Futures.immediateVoidFuture()
mpv.destroy()
}
return Futures.immediateFuture(null)
} }
override fun event(event: Int) { override fun event(event: Int) {
// trackGroupsByType = buildTrackGroupsByType()
invalidateState() invalidateState()
} }
override fun eventProperty(property: String) { override fun eventProperty(property: String) {
// if (property == "vid" || property == "aid" || property == "sid") {
// trackGroupsByType = buildTrackGroupsByType()
// }
invalidateState() invalidateState()
} }
override fun eventProperty(property: String, value: Long) { override fun eventProperty(property: String, value: Long) {
// if (property == "vid" || property == "aid" || property == "sid") {
// trackGroupsByType = buildTrackGroupsByType()
// }
invalidateState() invalidateState()
} }
@@ -385,4 +268,151 @@ class MpvPlayer(ctx: Context) : SimpleBasePlayer(Looper.getMainLooper()), MPVLib
override fun eventProperty(property: String, value: String) { override fun eventProperty(property: String, value: String) {
invalidateState() invalidateState()
} }
private fun getMpvPlaybackState(): 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
}
// private fun applyRequestHeaders(extras: Bundle?) {
// if (extras == null) {
// mpv.setPropertyString("http-header-fields", "")
// return
// }
// val names = extras.getStringArrayList(REQUEST_HEADER_NAMES_KEY)
// val values = extras.getStringArrayList(REQUEST_HEADER_VALUES_KEY)
// if (names.isNullOrEmpty() || values.isNullOrEmpty()) {
// mpv.setPropertyString("http-header-fields", "")
// return
// }
// val size = minOf(names.size, values.size)
// if (size <= 0) {
// mpv.setPropertyString("http-header-fields", "")
// return
// }
// val formatted = (0 until size).joinToString("\r\n") { idx -> "${names[idx]}: ${values[idx]}" }
// mpv.setPropertyString("http-header-fields", formatted)
// }
// private fun buildTrackGroupsByType(): Map<Int, List<TrackGroupBinding>> {
// val count = mpv.getPropertyInt("track-list/count") ?: 0
// if (count <= 0) return emptyMap()
//
// val grouped = LinkedHashMap<Int, MutableList<Pair<Int, Format>>>()
// for (i in 0 until count) {
// val base = "track-list/$i"
// val type = when (mpv.getPropertyString("$base/type")) {
// "video" -> C.TRACK_TYPE_VIDEO
// "audio" -> C.TRACK_TYPE_AUDIO
// "sub" -> C.TRACK_TYPE_TEXT
// else -> C.TRACK_TYPE_UNKNOWN
// }
// if (type == C.TRACK_TYPE_UNKNOWN) continue
//
// val id = mpv.getPropertyInt("$base/id") ?: continue
// val label = mpv.getPropertyString("$base/title") ?: mpv.getPropertyString("$base/codec")
// val language = mpv.getPropertyString("$base/lang")
// val codec = mpv.getPropertyString("$base/codec")
//
// val sampleMimeType = when (type) {
// C.TRACK_TYPE_VIDEO -> MimeTypes.VIDEO_UNKNOWN
// C.TRACK_TYPE_AUDIO -> MimeTypes.AUDIO_UNKNOWN
// C.TRACK_TYPE_TEXT -> MimeTypes.TEXT_UNKNOWN
// else -> null
// }
//
// val formatBuilder = Format.Builder()
// .setId(id)
// .setLabel(label)
// .setLanguage(language)
// .setCodecs(codec)
// sampleMimeType?.let { formatBuilder.setSampleMimeType(it) }
//
// grouped.getOrPut(type) { mutableListOf() }.add(id to formatBuilder.build())
// }
//
// val result = LinkedHashMap<Int, List<TrackGroupBinding>>()
// for ((type, entries) in grouped) {
// if (entries.isEmpty()) continue
// val formats = entries.map { it.second }.toTypedArray()
// val ids = entries.map { it.first }.toIntArray()
// val group = TrackGroup("mpv-$type", *formats)
// result[type] = listOf(TrackGroupBinding(group, ids))
// }
// return result
// }
// private fun buildTracks(): Tracks {
// if (trackGroupsByType.isEmpty()) {
// trackGroupsByType = buildTrackGroupsByType()
// }
// if (trackGroupsByType.isEmpty()) return Tracks.EMPTY
//
// val selectedVideo = mpv.getPropertyInt("vid")
// val selectedAudio = mpv.getPropertyInt("aid")
// val selectedSubtitle = mpv.getPropertyInt("sid")
//
// val groups = ArrayList<Tracks.Group>()
// for ((type, bindings) in trackGroupsByType) {
// val selectedId = when (type) {
// C.TRACK_TYPE_VIDEO -> selectedVideo
// C.TRACK_TYPE_AUDIO -> selectedAudio
// C.TRACK_TYPE_TEXT -> selectedSubtitle
// else -> null
// }
// for (binding in bindings) {
// val selected = BooleanArray(binding.mpvIds.size) { idx ->
// val id = binding.mpvIds[idx]
// selectedId != null && selectedId == id
// }
// val support = IntArray(binding.mpvIds.size) { C.FORMAT_HANDLED }
// groups.add(Tracks.Group(binding.group, false, support, selected))
// }
// }
//
// return Tracks(groups)
// }
// private fun applyTrackSelectionParameters(parameters: TrackSelectionParameters) {
// if (trackGroupsByType.isEmpty()) {
// trackGroupsByType = buildTrackGroupsByType()
// }
//
// if (parameters.disabledTrackTypes.contains(C.TRACK_TYPE_VIDEO)) {
// mpv.setPropertyString("vid", "no")
// }
// if (parameters.disabledTrackTypes.contains(C.TRACK_TYPE_AUDIO)) {
// mpv.setPropertyString("aid", "no")
// }
// if (parameters.disabledTrackTypes.contains(C.TRACK_TYPE_TEXT)) {
// mpv.setPropertyString("sid", "no")
// }
//
// for (override in parameters.overrides.values) {
// val binding = trackGroupsByType[override.getType()]
// ?.firstOrNull { it.group == override.mediaTrackGroup }
// ?: continue
// val selectedIndex = override.trackIndices.firstOrNull() ?: continue
// if (selectedIndex !in binding.mpvIds.indices) continue
// val selectedMpvId = binding.mpvIds[selectedIndex]
//
// when (override.getType()) {
// C.TRACK_TYPE_VIDEO -> mpv.setPropertyInt("vid", selectedMpvId)
// C.TRACK_TYPE_AUDIO -> mpv.setPropertyInt("aid", selectedMpvId)
// C.TRACK_TYPE_TEXT -> mpv.setPropertyInt("sid", selectedMpvId)
// }
// }
// }
// companion object {
// internal const val REQUEST_HEADER_NAMES_KEY = "omni.request.header_names"
// internal const val REQUEST_HEADER_VALUES_KEY = "omni.request.header_values"
// internal const val REQUEST_START_MS_KEY = "omni.request.start_ms"
// }
} }
@@ -1,84 +1,105 @@
package dev.zoriya.omni package dev.zoriya.omni
import android.util.Log import android.annotation.SuppressLint
import androidx.media3.common.Player import android.os.Bundle
import android.view.SurfaceHolder
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.TrackSelectionOverride
import androidx.media3.common.TrackSelectionParameters
import com.margelo.nitro.NitroModules import com.margelo.nitro.NitroModules
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.Source import com.margelo.nitro.omni.Source
import com.margelo.nitro.omni.Track import com.margelo.nitro.omni.Track
import dev.zoriya.omni.utils.deferredObservable import java.util.ArrayList
@SuppressLint("UnsafeOptInUsageError")
class OmniPlayer : HybridOmniPlayerSpec() { class OmniPlayer : HybridOmniPlayerSpec() {
val ctx = NitroModules.applicationContext ?: throw Error("No Context available!") private val ctx = NitroModules.applicationContext ?: throw Error("No Context available!")
val player = MpvPlayer(ctx) val player = MpvPlayer(ctx)
override val eventMap = EventMap(player.mpv) override val eventMap = EventMap(player.mpv)
private val mediaSessionBridge = OmniMediaSessionBridge(ctx, this) private var currentSource: Source? = null
override fun dispose() { override fun dispose() {
super.dispose() super.dispose()
mediaSessionBridge.dispose()
eventMap.dispose() eventMap.dispose()
player.release() player.release()
} }
override var source: Source by deferredObservable { _, _, new -> override var source: Source
Log.e("omni", "Chaning source") get() = currentSource
player.setSource(new) ?: throw IllegalStateException("source should be initialized before get")
mediaSessionBridge.onSourceChanged() set(value) {
currentSource = value
player.setMediaItem(buildMediaItem(value))
}
fun setSurface(holder: SurfaceHolder?) {
if (holder == null) {
player.clearVideoSurface()
} else {
player.setVideoSurfaceHolder(holder)
}
} }
fun setSurface(surface: android.view.Surface?) { override val hasPrev get() = player.hasPreviousMediaItem()
player.setSurface(surface) override val hasNext get() = player.hasNextMediaItem()
}
fun setSurfaceSize(width: Int, height: Int) {
player.setSurfaceSize(width, height)
}
override val hasPrev get() = player.hasPrevMetadata()
override val hasNext get() = player.hasNextMetadata()
override val status: PlayerStatus override val status: PlayerStatus
get() = when (player.playbackState) { get() = when (player.playbackState) {
Player.STATE_IDLE -> PlayerStatus.IDLE androidx.media3.common.Player.STATE_IDLE,
Player.STATE_ENDED -> PlayerStatus.IDLE androidx.media3.common.Player.STATE_ENDED -> PlayerStatus.IDLE
Player.STATE_BUFFERING -> PlayerStatus.LOADING
androidx.media3.common.Player.STATE_BUFFERING -> PlayerStatus.LOADING
else -> PlayerStatus.READYTOPLAY else -> PlayerStatus.READYTOPLAY
} }
override val isPlaying get() = player.isPlaying override val isPlaying get() = player.isPlaying
override var currentTime override var currentTime
get() = player.getCurrentTimeSeconds() get() = player.currentPosition.toDouble() / 1000.0
set(value) { set(value) {
player.setCurrentTimeSeconds(value) player.seekTo((value.coerceAtLeast(0.0) * 1000.0).toLong())
} }
override val buffered get() = player.getBufferedSeconds() override val buffered
override val duration get() = player.getDurationSeconds() get() = (player.totalBufferedDuration.toDouble() / 1000.0).coerceAtLeast(
0.0
)
override val duration
get() = if (player.duration == C.TIME_UNSET) 0.0 else (player.duration.toDouble() / 1000.0).coerceAtLeast(
0.0
)
override var playbackRate override var playbackRate
get() = player.getPlaybackRate() get() = player.playbackParameters.speed.toDouble()
set(value) { set(value) {
player.setPlaybackRate(value) player.setPlaybackSpeed(value.toFloat().coerceAtLeast(0f))
} }
override var muted var volumeMuted: Float? = null
get() = player.isMuted() override var muted: Boolean
get() = player.volume == 0.0f && volumeMuted != null
set(value) { set(value) {
player.setMuted(value) if (value) {
volumeMuted = player.volume
player.volume = 0.0f
} else {
player.volume = volumeMuted ?: 100.0f
volumeMuted = null
}
} }
override var volume override var volume
get() = player.getVolume01() get() = player.volume.toDouble()
set(value) { set(value) {
player.setVolume01(value) player.volume = value.toFloat().coerceIn(0f, 1f)
} }
override val videos get() = player.getTracks("video") override val videos get() = tracksByType(C.TRACK_TYPE_VIDEO)
override val audios get() = player.getTracks("audio") override val audios get() = tracksByType(C.TRACK_TYPE_AUDIO)
override val subtitles get() = player.getTracks("sub") override val subtitles get() = tracksByType(C.TRACK_TYPE_TEXT)
override val rendition: Array<Rendition> get() = emptyArray() override val rendition: Array<Rendition> get() = emptyArray()
override fun play() { override fun play() {
@@ -90,7 +111,8 @@ class OmniPlayer : HybridOmniPlayerSpec() {
} }
override fun seekBy(offset: Double) { override fun seekBy(offset: Double) {
player.seekBy(offset) val target = (player.currentPosition.toDouble() / 1000.0) + offset
player.seekTo((target.coerceAtLeast(0.0) * 1000.0).toLong())
} }
override fun playPrev() { override fun playPrev() {
@@ -102,17 +124,127 @@ class OmniPlayer : HybridOmniPlayerSpec() {
} }
override fun selectVideo(video: Track) { override fun selectVideo(video: Track) {
player.selectVideo(video) selectTrack(C.TRACK_TYPE_VIDEO, video)
} }
override fun selectAudio(audio: Track) { override fun selectAudio(audio: Track) {
player.selectAudio(audio) selectTrack(C.TRACK_TYPE_AUDIO, audio)
} }
override fun selectSubtitle(subtitle: Track?) { override fun selectSubtitle(subtitle: Track?) {
player.selectSubtitle(subtitle) // if (subtitle == null) {
// val params = player.trackSelectionParameters
// .buildUpon()
// .setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true)
// .build()
// player.trackSelectionParameters = params
// return
// }
//
// val params = player.trackSelectionParameters
// .buildUpon()
// .setTrackTypeDisabled(C.TRACK_TYPE_TEXT, false)
// .build()
// player.trackSelectionParameters = params
// selectTrack(C.TRACK_TYPE_TEXT, subtitle)
} }
override fun selectRendition(rendition: Rendition?) { override fun selectRendition(rendition: Rendition?) {
} }
private fun buildMediaItem(source: Source): MediaItem {
val src = source.src.firstOrNull() ?: return MediaItem.EMPTY
val mediaMetadata = MediaMetadata.Builder()
.setTitle(source.metadata?.title)
.setAlbumTitle(source.metadata?.album)
.setArtist(source.metadata?.artist)
.apply {
source.metadata?.imageLink?.let { setArtworkUri(android.net.Uri.parse(it)) }
}
.build()
// val subtitleConfigurations = source.subtitles.map { subtitle ->
// MediaItem.SubtitleConfiguration.Builder(android.net.Uri.parse(subtitle.link))
// .setId(subtitle.id)
// .setLanguage(subtitle.language)
// .setLabel(subtitle.label)
// .setMimeType(subtitle.mimeType)
// .build()
// }
// val headers = Bundle().apply {
// putStringArrayList(
// MpvPlayer.REQUEST_HEADER_NAMES_KEY,
// ArrayList(src.headers.keys)
// )
// putStringArrayList(
// MpvPlayer.REQUEST_HEADER_VALUES_KEY,
// ArrayList(src.headers.values)
// )
// source.startTime?.let {
// putLong(MpvPlayer.REQUEST_START_MS_KEY, (it.coerceAtLeast(0.0) * 1000.0).toLong())
// }
// }
val requestMetadata = MediaItem.RequestMetadata.Builder()
.setMediaUri(android.net.Uri.parse(src.uri))
// .setExtras(headers)
.build()
val itemBuilder = MediaItem.Builder()
.setUri(src.uri)
.setMimeType(src.mimeType)
.setMediaId(src.uri)
.setMediaMetadata(mediaMetadata)
// .setSubtitleConfigurations(subtitleConfigurations)
.setRequestMetadata(requestMetadata)
return itemBuilder.build()
}
private fun tracksByType(trackType: Int): Array<Track> {
return emptyArray()
// val groups = player.currentTracks.groups.filter { it.type == trackType }
// if (groups.isEmpty()) return emptyArray()
//
// val result = ArrayList<Track>()
// for (group in groups) {
// val mediaGroup = group.mediaTrackGroup
// for (i in 0 until group.length) {
// val format = group.getTrackFormat(i)
// result.add(
// Track(
// id = format.id ?: mediaGroup.id,
// label = format.label,
// language = format.language,
// selected = group.isTrackSelected(i)
// )
// )
// }
// }
// return result.toTypedArray()
}
private fun selectTrack(trackType: Int, track: Track) {
return
// val targetId = track.id
// val groups = player.currentTracks.groups.filter { it.type == trackType }
// for (group in groups) {
// val mediaGroup = group.mediaTrackGroup
// for (i in 0 until group.length) {
// val formatId = group.getTrackFormat(i).id ?: mediaGroup.id
// if (formatId != targetId) continue
//
// val override = TrackSelectionOverride(mediaGroup, i)
// val builder: TrackSelectionParameters.Builder = player.trackSelectionParameters
// .buildUpon()
// .setTrackTypeDisabled(trackType, false)
// .setOverrideForType(override)
// player.trackSelectionParameters = builder.build()
// return
// }
// }
}
} }
@@ -53,7 +53,7 @@ class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec(), SurfaceH
boundPlayer = omniPlayer boundPlayer = omniPlayer
if (surfaceReady) { if (surfaceReady) {
omniPlayer.setSurface(surfaceView.holder.surface) omniPlayer.setSurface(surfaceView.holder)
} }
if (autoplay == true && !omniPlayer.isPlaying) { if (autoplay == true && !omniPlayer.isPlaying) {
@@ -73,7 +73,7 @@ class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec(), SurfaceH
override fun surfaceCreated(holder: SurfaceHolder) { override fun surfaceCreated(holder: SurfaceHolder) {
surfaceReady = true surfaceReady = true
Log.e("omniView", "surface created") Log.e("omniView", "surface created")
boundPlayer?.setSurface(holder.surface) boundPlayer?.setSurface(holder)
} }
override fun surfaceChanged( override fun surfaceChanged(
@@ -82,7 +82,7 @@ class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec(), SurfaceH
width: Int, width: Int,
height: Int height: Int
) { ) {
boundPlayer?.setSurfaceSize(width, height) // Surface size is resolved from SurfaceHolder in MpvPlayer.
} }
override fun surfaceDestroyed(holder: SurfaceHolder) { override fun surfaceDestroyed(holder: SurfaceHolder) {