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.content.Context
import android.net.Uri
import android.os.Bundle
import android.os.Looper
import android.view.Surface
import android.view.SurfaceHolder
import androidx.media3.common.C
import androidx.media3.common.Format
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.MimeTypes
import androidx.media3.common.PlaybackParameters
import androidx.media3.common.Player
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.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")
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
private var mediaItem: MediaItem? = null
private var released = false
private var surfaceSize: Size = Size.UNKNOWN
setOptionString("cache", "yes")
setOptionString("cache-pause-initial", "yes")
setOptionString("demuxer-max-bytes", "150MiB")
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()
.add(COMMAND_PLAY_PAUSE)
.add(COMMAND_PREPARE)
.add(COMMAND_STOP)
.add(COMMAND_RELEASE)
.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_SET_VOLUME)
.add(COMMAND_SET_VIDEO_SURFACE)
.add(COMMAND_SET_MEDIA_ITEM)
.add(COMMAND_GET_CURRENT_MEDIA_ITEM)
.add(COMMAND_GET_METADATA)
.add(COMMAND_GET_TIMELINE)
.add(Player.COMMAND_GET_MEDIA_ITEMS_METADATA)
.add(COMMAND_GET_TRACKS)
.add(COMMAND_SET_TRACK_SELECTION_PARAMETERS)
.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 positionMs = ((mpv.getPropertyDouble("time-pos") ?: 0.0).coerceAtLeast(0.0) * 1000.0).toLong()
val cacheMs = ((mpv.getPropertyDouble("demuxer-cache-time") ?: 0.0).coerceAtLeast(0.0) * 1000.0).toLong()
val durationMs = ((mpv.getPropertyDouble("duration") ?: 0.0).coerceAtLeast(0.0) * 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)
.setPlayWhenReady(
!(mpv.getPropertyBoolean("pause") ?: true),
Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST
)
.setPlaybackState(getMpvPlaybackState())
.setPlaybackSuppressionReason(Player.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 }
.setPlaybackParameters(PlaybackParameters((mpv.getPropertyDouble("speed") ?: 1.0).toFloat().coerceAtLeast(0f)))
.setVolume((mpv.getPropertyDouble("volume") ?: 100.0).coerceIn(0.0, 100.0).toFloat() / 100f)
// .setTrackSelectionParameters(currentTrackSelectionParameters)
.setContentPositionMs(positionMs)
.setContentBufferedPositionMs { cacheMs }
.setTotalBufferedDurationMs { cacheMs }
// .setPlaylistMetadata(currentMediaItem?.mediaMetadata ?: MediaMetadata.EMPTY)
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)
}
// val item = currentMediaItem
// if (item != null) {
// val itemBuilder = MediaItemData.Builder(item.mediaId)
// .setMediaItem(item)
// .setMediaMetadata(item.mediaMetadata)
// .setIsSeekable(true)
// .setTracks(buildTracks())
//
// if (durationMs > 0L) {
// itemBuilder.setDurationUs(durationMs * 1000L)
// }
//
// builder
// .setPlaylist(listOf(itemBuilder.build()))
// .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)
return Futures.immediateVoidFuture()
}
override fun handleSetPlaybackParameters(playbackParameters: PlaybackParameters): ListenableFuture<*> {
mpv.setPropertyDouble("speed", playbackParameters.speed.coerceAtLeast(0f).toDouble())
invalidateState()
return Futures.immediateFuture(null)
return Futures.immediateVoidFuture()
}
override fun handleSeek(
mediaItemIndex: Int,
positionMs: Long,
seekCommand: Int
): ListenableFuture<*> {
override fun handleSeek(mediaItemIndex: Int, positionMs: Long, seekCommand: Int): ListenableFuture<*> {
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)
mpv.command(arrayOf("seek", (targetMs / 1000.0).toString(), "absolute"))
invalidateState()
return Futures.immediateFuture(null)
return Futures.immediateVoidFuture()
}
override fun handleSetVolume(volume: Float): ListenableFuture<*> {
mpv.setPropertyDouble("volume", volume.coerceIn(0f, 1f) * 100.0)
invalidateState()
return Futures.immediateFuture(null)
return Futures.immediateVoidFuture()
}
override fun handleSetVideoOutput(videoOutput: Any): ListenableFuture<*> {
if (videoOutput is Surface) {
setSurface(videoOutput)
when (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<*> {
if (videoOutput is Surface) {
setSurface(null)
}
return Futures.immediateFuture(null)
override fun handleClearVideoOutput(videoOutput: Any?): ListenableFuture<*> {
mpv.setOptionString("vo", "null")
mpv.setOptionString("force-window", "no")
mpv.detachSurface()
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<*> {
mpv.command(arrayOf("stop"))
invalidateState()
return Futures.immediateFuture(null)
return Futures.immediateVoidFuture()
}
override fun handleRelease(): ListenableFuture<*> {
if (!released) {
released = true
mpv.removeObserver(this)
mpv.detachSurface()
mpv.destroy()
}
return Futures.immediateFuture(null)
mpv.removeObserver(this)
mpv.detachSurface()
mpv.destroy()
return Futures.immediateVoidFuture()
}
override fun event(event: Int) {
// trackGroupsByType = buildTrackGroupsByType()
invalidateState()
}
override fun eventProperty(property: String) {
// if (property == "vid" || property == "aid" || property == "sid") {
// trackGroupsByType = buildTrackGroupsByType()
// }
invalidateState()
}
override fun eventProperty(property: String, value: Long) {
// if (property == "vid" || property == "aid" || property == "sid") {
// trackGroupsByType = buildTrackGroupsByType()
// }
invalidateState()
}
@@ -385,4 +268,151 @@ class MpvPlayer(ctx: Context) : SimpleBasePlayer(Looper.getMainLooper()), MPVLib
override fun eventProperty(property: String, value: String) {
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
import android.util.Log
import androidx.media3.common.Player
import android.annotation.SuppressLint
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.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.zoriya.omni.utils.deferredObservable
import java.util.ArrayList
@SuppressLint("UnsafeOptInUsageError")
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)
override val eventMap = EventMap(player.mpv)
private val mediaSessionBridge = OmniMediaSessionBridge(ctx, this)
private var currentSource: Source? = null
override fun dispose() {
super.dispose()
mediaSessionBridge.dispose()
eventMap.dispose()
player.release()
}
override var source: Source by deferredObservable { _, _, new ->
Log.e("omni", "Chaning source")
player.setSource(new)
mediaSessionBridge.onSourceChanged()
override var source: Source
get() = currentSource
?: throw IllegalStateException("source should be initialized before get")
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?) {
player.setSurface(surface)
}
fun setSurfaceSize(width: Int, height: Int) {
player.setSurfaceSize(width, height)
}
override val hasPrev get() = player.hasPrevMetadata()
override val hasNext get() = player.hasNextMetadata()
override val hasPrev get() = player.hasPreviousMediaItem()
override val hasNext get() = player.hasNextMediaItem()
override val status: PlayerStatus
get() = when (player.playbackState) {
Player.STATE_IDLE -> PlayerStatus.IDLE
Player.STATE_ENDED -> PlayerStatus.IDLE
Player.STATE_BUFFERING -> PlayerStatus.LOADING
androidx.media3.common.Player.STATE_IDLE,
androidx.media3.common.Player.STATE_ENDED -> PlayerStatus.IDLE
androidx.media3.common.Player.STATE_BUFFERING -> PlayerStatus.LOADING
else -> PlayerStatus.READYTOPLAY
}
override val isPlaying get() = player.isPlaying
override var currentTime
get() = player.getCurrentTimeSeconds()
get() = player.currentPosition.toDouble() / 1000.0
set(value) {
player.setCurrentTimeSeconds(value)
player.seekTo((value.coerceAtLeast(0.0) * 1000.0).toLong())
}
override val buffered get() = player.getBufferedSeconds()
override val duration get() = player.getDurationSeconds()
override val buffered
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
get() = player.getPlaybackRate()
get() = player.playbackParameters.speed.toDouble()
set(value) {
player.setPlaybackRate(value)
player.setPlaybackSpeed(value.toFloat().coerceAtLeast(0f))
}
override var muted
get() = player.isMuted()
var volumeMuted: Float? = null
override var muted: Boolean
get() = player.volume == 0.0f && volumeMuted != null
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
get() = player.getVolume01()
get() = player.volume.toDouble()
set(value) {
player.setVolume01(value)
player.volume = value.toFloat().coerceIn(0f, 1f)
}
override val videos get() = player.getTracks("video")
override val audios get() = player.getTracks("audio")
override val subtitles get() = player.getTracks("sub")
override val videos get() = tracksByType(C.TRACK_TYPE_VIDEO)
override val audios get() = tracksByType(C.TRACK_TYPE_AUDIO)
override val subtitles get() = tracksByType(C.TRACK_TYPE_TEXT)
override val rendition: Array<Rendition> get() = emptyArray()
override fun play() {
@@ -90,7 +111,8 @@ class OmniPlayer : HybridOmniPlayerSpec() {
}
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() {
@@ -102,17 +124,127 @@ class OmniPlayer : HybridOmniPlayerSpec() {
}
override fun selectVideo(video: Track) {
player.selectVideo(video)
selectTrack(C.TRACK_TYPE_VIDEO, video)
}
override fun selectAudio(audio: Track) {
player.selectAudio(audio)
selectTrack(C.TRACK_TYPE_AUDIO, audio)
}
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?) {
}
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
if (surfaceReady) {
omniPlayer.setSurface(surfaceView.holder.surface)
omniPlayer.setSurface(surfaceView.holder)
}
if (autoplay == true && !omniPlayer.isPlaying) {
@@ -73,7 +73,7 @@ class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec(), SurfaceH
override fun surfaceCreated(holder: SurfaceHolder) {
surfaceReady = true
Log.e("omniView", "surface created")
boundPlayer?.setSurface(holder.surface)
boundPlayer?.setSurface(holder)
}
override fun surfaceChanged(
@@ -82,7 +82,7 @@ class OmniView(val context: ThemedReactContext) : HybridOmniViewSpec(), SurfaceH
width: Int,
height: Int
) {
boundPlayer?.setSurfaceSize(width, height)
// Surface size is resolved from SurfaceHolder in MpvPlayer.
}
override fun surfaceDestroyed(holder: SurfaceHolder) {