mirror of
https://github.com/zoriya/react-native-omni.git
synced 2026-08-05 05:36:17 +00:00
Rework mpv player to use media3 interface
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user