diff --git a/android/src/main/java/dev/zoriya/omni/MpvPlayer.kt b/android/src/main/java/dev/zoriya/omni/MpvPlayer.kt index e2b3e56..998adbf 100644 --- a/android/src/main/java/dev/zoriya/omni/MpvPlayer.kt +++ b/android/src/main/java/dev/zoriya/omni/MpvPlayer.kt @@ -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> = 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 { - val count = mpv.getPropertyInt("track-list/count") ?: 0 - if (count <= 0) return emptyArray() - - val tracks = ArrayList(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, +// 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> { +// val count = mpv.getPropertyInt("track-list/count") ?: 0 +// if (count <= 0) return emptyMap() +// +// val grouped = LinkedHashMap>>() +// 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>() +// 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() +// 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" +// } } diff --git a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt index 17bd486..4471103 100644 --- a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt +++ b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt @@ -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 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 { + return emptyArray() +// val groups = player.currentTracks.groups.filter { it.type == trackType } +// if (groups.isEmpty()) return emptyArray() +// +// val result = ArrayList() +// 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 +// } +// } + } + } diff --git a/android/src/main/java/dev/zoriya/omni/OmniView.kt b/android/src/main/java/dev/zoriya/omni/OmniView.kt index a3182c2..77ac5fe 100755 --- a/android/src/main/java/dev/zoriya/omni/OmniView.kt +++ b/android/src/main/java/dev/zoriya/omni/OmniView.kt @@ -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) {