mirror of
https://github.com/zoriya/react-native-omni.git
synced 2026-08-05 05:36:17 +00:00
Properly implement BasePlayer interface
This commit is contained in:
@@ -1,15 +1,16 @@
|
||||
package dev.zoriya.omni
|
||||
|
||||
import android.os.SystemClock
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.Player
|
||||
import com.margelo.nitro.omni.BoolProperty
|
||||
import com.margelo.nitro.omni.HybridOmniEventMapSpec
|
||||
import com.margelo.nitro.omni.NumberProperty
|
||||
import com.margelo.nitro.omni.PlayerStatus
|
||||
import com.margelo.nitro.omni.Rendition
|
||||
import com.margelo.nitro.omni.Track
|
||||
import android.os.SystemClock
|
||||
import dev.jdtech.mpv.MPVLib
|
||||
|
||||
class EventMap(private val player: MPVLib) : HybridOmniEventMapSpec(), MPVLib.EventObserver {
|
||||
class EventMap(private val player: Player) : HybridOmniEventMapSpec(), Player.Listener {
|
||||
val onPrevListeners = mutableSetOf<() -> Unit>()
|
||||
val onNextListeners = mutableSetOf<() -> Unit>()
|
||||
private val onEndListeners = mutableSetOf<() -> Unit>()
|
||||
@@ -22,30 +23,16 @@ class EventMap(private val player: MPVLib) : HybridOmniEventMapSpec(), MPVLib.Ev
|
||||
private val stateListeners = mutableMapOf<NumberProperty, MutableSet<(Double) -> Unit>>()
|
||||
private val stateBoolListeners = mutableMapOf<BoolProperty, MutableSet<(Boolean) -> Unit>>()
|
||||
private val playerStatusListeners = mutableSetOf<(PlayerStatus) -> Unit>()
|
||||
private var isSeeking = false
|
||||
private var eofReached = false
|
||||
private var lastTimePosDispatchMs = 0L
|
||||
|
||||
init {
|
||||
player.addObserver(this)
|
||||
player.observeProperty("vid", MPVLib.MpvFormat.MPV_FORMAT_INT64)
|
||||
player.observeProperty("aid", MPVLib.MpvFormat.MPV_FORMAT_INT64)
|
||||
player.observeProperty("sid", MPVLib.MpvFormat.MPV_FORMAT_INT64)
|
||||
player.observeProperty("pause", MPVLib.MpvFormat.MPV_FORMAT_FLAG)
|
||||
player.observeProperty("mute", MPVLib.MpvFormat.MPV_FORMAT_FLAG)
|
||||
player.observeProperty("eof-reached", MPVLib.MpvFormat.MPV_FORMAT_FLAG)
|
||||
player.observeProperty("core-idle", MPVLib.MpvFormat.MPV_FORMAT_FLAG)
|
||||
player.observeProperty("paused-for-cache", MPVLib.MpvFormat.MPV_FORMAT_FLAG)
|
||||
player.observeProperty("time-pos", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE)
|
||||
player.observeProperty("demuxer-cache-time", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE)
|
||||
player.observeProperty("duration", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE)
|
||||
player.observeProperty("speed", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE)
|
||||
player.observeProperty("volume", MPVLib.MpvFormat.MPV_FORMAT_DOUBLE)
|
||||
player.addListener(this)
|
||||
emitCoreState(forceCurrentTime = true)
|
||||
}
|
||||
|
||||
private fun computePlayerStatus(): PlayerStatus {
|
||||
val loading = player.getPropertyBoolean("paused-for-cache") ?: false
|
||||
val idle = player.getPropertyBoolean("core-idle") ?: false
|
||||
val loading = player.isLoading
|
||||
val idle = player.playbackState == Player.STATE_IDLE || player.playbackState == Player.STATE_ENDED
|
||||
return when {
|
||||
loading -> PlayerStatus.LOADING
|
||||
idle -> PlayerStatus.IDLE
|
||||
@@ -53,122 +40,109 @@ class EventMap(private val player: MPVLib) : HybridOmniEventMapSpec(), MPVLib.Ev
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTrackById(type: String, id: Long): Track? {
|
||||
val count = player.getPropertyInt("track-list/count") ?: 0
|
||||
for (i in 0 until count) {
|
||||
val base = "track-list/$i"
|
||||
val trackType = player.getPropertyString("$base/type") ?: continue
|
||||
if (trackType != type) continue
|
||||
val trackId = player.getPropertyInt("$base/id") ?: continue
|
||||
if (trackId.toLong() != id) continue
|
||||
val selected = player.getPropertyBoolean("$base/selected") ?: false
|
||||
val label = player.getPropertyString("$base/title")
|
||||
?: player.getPropertyString("$base/codec")
|
||||
val language = player.getPropertyString("$base/lang")
|
||||
return Track(
|
||||
id = trackId.toString(),
|
||||
label = label,
|
||||
language = language,
|
||||
selected = selected
|
||||
)
|
||||
private fun selectedTrack(trackType: Int): Track? {
|
||||
val groups = player.currentTracks.groups.filter { it.type == trackType }
|
||||
for (group in groups) {
|
||||
val mediaGroup = group.mediaTrackGroup
|
||||
for (i in 0 until group.length) {
|
||||
if (!group.isTrackSelected(i)) continue
|
||||
val format = group.getTrackFormat(i)
|
||||
return Track(
|
||||
id = format.id ?: mediaGroup.id,
|
||||
label = format.label,
|
||||
language = format.language,
|
||||
selected = true
|
||||
)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun event(event: Int) {
|
||||
when (event) {
|
||||
MPVLib.MpvEvent.MPV_EVENT_START_FILE ->
|
||||
run {
|
||||
eofReached = false
|
||||
isSeeking = false
|
||||
playerStatusListeners.forEach { it(PlayerStatus.LOADING) }
|
||||
}
|
||||
|
||||
MPVLib.MpvEvent.MPV_EVENT_FILE_LOADED ->
|
||||
playerStatusListeners.forEach { it(PlayerStatus.READYTOPLAY) }
|
||||
|
||||
MPVLib.MpvEvent.MPV_EVENT_SEEK -> isSeeking = true
|
||||
MPVLib.MpvEvent.MPV_EVENT_PLAYBACK_RESTART -> isSeeking = false
|
||||
MPVLib.MpvEvent.MPV_EVENT_END_FILE -> {
|
||||
isSeeking = false
|
||||
if (eofReached) {
|
||||
onEndListeners.forEach { it() }
|
||||
} else {
|
||||
onErrorListeners.forEach {
|
||||
it("end_file", "playback ended before reaching EOF")
|
||||
}
|
||||
}
|
||||
playerStatusListeners.forEach { it(PlayerStatus.IDLE) }
|
||||
override fun onEvents(player: Player, events: Player.Events) {
|
||||
if (
|
||||
events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED) ||
|
||||
events.contains(Player.EVENT_IS_LOADING_CHANGED)
|
||||
) {
|
||||
val status = computePlayerStatus()
|
||||
playerStatusListeners.forEach { it(status) }
|
||||
if (player.playbackState == Player.STATE_ENDED) {
|
||||
onEndListeners.forEach { it() }
|
||||
}
|
||||
}
|
||||
|
||||
MPVLib.MpvEvent.MPV_EVENT_QUEUE_OVERFLOW -> onErrorListeners.forEach {
|
||||
it("queue_overflow", "mpv event queue overflow")
|
||||
if (
|
||||
events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED) ||
|
||||
events.contains(Player.EVENT_IS_PLAYING_CHANGED)
|
||||
) {
|
||||
val isPlaying = player.isPlaying
|
||||
onAudioFocusChangeListeners.forEach { it(if (isPlaying) "playing" else "paused") }
|
||||
stateBoolListeners[BoolProperty.ISPLAYING]?.forEach { it(isPlaying) }
|
||||
}
|
||||
|
||||
if (events.contains(Player.EVENT_VOLUME_CHANGED)) {
|
||||
val volume = player.volume.toDouble().coerceIn(0.0, 1.0)
|
||||
stateListeners[NumberProperty.VOLUME]?.forEach { it(volume) }
|
||||
stateBoolListeners[BoolProperty.MUTED]?.forEach { it(volume <= 0.0) }
|
||||
}
|
||||
|
||||
if (events.contains(Player.EVENT_PLAYBACK_PARAMETERS_CHANGED)) {
|
||||
stateListeners[NumberProperty.PLAYBACKRATE]?.forEach {
|
||||
it(player.playbackParameters.speed.toDouble())
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
events.contains(Player.EVENT_POSITION_DISCONTINUITY) ||
|
||||
events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED) ||
|
||||
events.contains(Player.EVENT_IS_LOADING_CHANGED)
|
||||
) {
|
||||
emitCoreState(forceCurrentTime = true)
|
||||
} else {
|
||||
emitCoreState(forceCurrentTime = false)
|
||||
}
|
||||
|
||||
if (events.contains(Player.EVENT_TRACKS_CHANGED)) {
|
||||
selectedTrack(C.TRACK_TYPE_VIDEO)?.let { track ->
|
||||
onVideoTrackChangeListeners.forEach { it(track) }
|
||||
}
|
||||
selectedTrack(C.TRACK_TYPE_AUDIO)?.let { track ->
|
||||
onAudioTrackChangeListeners.forEach { it(track) }
|
||||
}
|
||||
onSubtitleChangeListeners.forEach { it(selectedTrack(C.TRACK_TYPE_TEXT)) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String) = Unit
|
||||
|
||||
override fun eventProperty(property: String, value: Long) {
|
||||
when (property) {
|
||||
"vid" -> {
|
||||
val track = getTrackById("video", value)
|
||||
if (track != null) onVideoTrackChangeListeners.forEach { it(track) }
|
||||
}
|
||||
|
||||
"aid" -> {
|
||||
val track = getTrackById("audio", value)
|
||||
if (track != null) onAudioTrackChangeListeners.forEach { it(track) }
|
||||
}
|
||||
|
||||
"sid" -> {
|
||||
val track = getTrackById("sub", value)
|
||||
onSubtitleChangeListeners.forEach { it(track) }
|
||||
}
|
||||
override fun onPlayerError(error: androidx.media3.common.PlaybackException) {
|
||||
onErrorListeners.forEach {
|
||||
it(error.errorCodeName, error.message ?: "unknown message")
|
||||
}
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String, value: Double) {
|
||||
when (property) {
|
||||
"time-pos" -> if (isSeeking || SystemClock.elapsedRealtime() - lastTimePosDispatchMs >= 1000L) {
|
||||
lastTimePosDispatchMs = SystemClock.elapsedRealtime()
|
||||
stateListeners[NumberProperty.CURRENTTIME]?.forEach { it(value.coerceAtLeast(0.0)) }
|
||||
}
|
||||
override fun onPositionDiscontinuity(
|
||||
oldPosition: Player.PositionInfo,
|
||||
newPosition: Player.PositionInfo,
|
||||
reason: Int
|
||||
) {
|
||||
emitCurrentTime(force = true)
|
||||
}
|
||||
|
||||
"demuxer-cache-time" -> stateListeners[NumberProperty.BUFFERED]
|
||||
?.forEach { it(value.coerceAtLeast(0.0)) }
|
||||
|
||||
"duration" -> stateListeners[NumberProperty.DURATION]
|
||||
?.forEach { it(value.coerceAtLeast(0.0)) }
|
||||
|
||||
"speed" -> stateListeners[NumberProperty.PLAYBACKRATE]
|
||||
?.forEach { it(value) }
|
||||
|
||||
"volume" -> stateListeners[NumberProperty.VOLUME]
|
||||
?.forEach { it((value / 100.0).coerceIn(0.0, 1.0)) }
|
||||
private fun emitCoreState(forceCurrentTime: Boolean) {
|
||||
emitCurrentTime(forceCurrentTime)
|
||||
stateListeners[NumberProperty.BUFFERED]?.forEach {
|
||||
it((player.totalBufferedDuration.toDouble() / 1000.0).coerceAtLeast(0.0))
|
||||
}
|
||||
stateListeners[NumberProperty.DURATION]?.forEach {
|
||||
val duration = player.duration
|
||||
it(if (duration == C.TIME_UNSET) 0.0 else (duration.toDouble() / 1000.0).coerceAtLeast(0.0))
|
||||
}
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String, value: Boolean) {
|
||||
when (property) {
|
||||
"pause" -> {
|
||||
val isPlaying = !value
|
||||
onAudioFocusChangeListeners.forEach { it(if (isPlaying) "playing" else "paused") }
|
||||
stateBoolListeners[BoolProperty.ISPLAYING]?.forEach { it(isPlaying) }
|
||||
}
|
||||
|
||||
"mute" ->
|
||||
stateBoolListeners[BoolProperty.MUTED]?.forEach { it(value) }
|
||||
|
||||
"eof-reached" -> eofReached = value
|
||||
"core-idle", "paused-for-cache" ->
|
||||
playerStatusListeners.forEach { it(computePlayerStatus()) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String, value: String) {
|
||||
if (property == "sid" && value == "no") {
|
||||
onSubtitleChangeListeners.forEach { it(null) }
|
||||
private fun emitCurrentTime(force: Boolean) {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
if (!force && now - lastTimePosDispatchMs < 1000L) return
|
||||
lastTimePosDispatchMs = now
|
||||
stateListeners[NumberProperty.CURRENTTIME]?.forEach {
|
||||
it((player.currentPosition.toDouble() / 1000.0).coerceAtLeast(0.0))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,7 +243,7 @@ class EventMap(private val player: MPVLib) : HybridOmniEventMapSpec(), MPVLib.Ev
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
player.removeObserver(this)
|
||||
player.removeListener(this)
|
||||
super.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,29 +2,44 @@ package dev.zoriya.omni
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.Surface
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.SurfaceView
|
||||
import android.view.TextureView
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.BasePlayer
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.C.FORMAT_HANDLED
|
||||
import androidx.media3.common.C.INDEX_UNSET
|
||||
import androidx.media3.common.C.TIME_UNSET
|
||||
import androidx.media3.common.C.TRACK_TYPE_AUDIO
|
||||
import androidx.media3.common.C.TRACK_TYPE_TEXT
|
||||
import androidx.media3.common.C.TRACK_TYPE_UNKNOWN
|
||||
import androidx.media3.common.C.TRACK_TYPE_VIDEO
|
||||
import androidx.media3.common.DeviceInfo
|
||||
import androidx.media3.common.Format
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.PlaybackParameters
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.SimpleBasePlayer
|
||||
import androidx.media3.common.Timeline
|
||||
import androidx.media3.common.Timeline.Window.SINGLE_WINDOW_UID
|
||||
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 androidx.media3.common.Tracks.Group
|
||||
import androidx.media3.common.VideoSize
|
||||
import androidx.media3.common.text.CueGroup
|
||||
import androidx.media3.common.util.Size
|
||||
import dev.jdtech.mpv.MPVLib
|
||||
import java.util.concurrent.CopyOnWriteArraySet
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
class MpvPlayer(ctx: Context) : SimpleBasePlayer(Looper.getMainLooper()), MPVLib.EventObserver {
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
class MpvPlayer(ctx: Context) : BasePlayer(), MPVLib.EventObserver {
|
||||
private val applicationLooper: Looper = Looper.getMainLooper()
|
||||
private val listeners = CopyOnWriteArraySet<Player.Listener>()
|
||||
|
||||
internal val mpv = (MPVLib.create(ctx) ?: throw Error("Failed to initialize MPVLib")).apply {
|
||||
setOptionString("vo", "gpu-next")
|
||||
@@ -50,8 +65,6 @@ class MpvPlayer(ctx: Context) : SimpleBasePlayer(Looper.getMainLooper()), MPVLib
|
||||
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)
|
||||
@@ -60,18 +73,18 @@ class MpvPlayer(ctx: Context) : SimpleBasePlayer(Looper.getMainLooper()), MPVLib
|
||||
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 var currentMediaItem: MediaItem? = null
|
||||
private var currentTrackSelectionParameters = TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT
|
||||
private var playerError: PlaybackException? = null
|
||||
private var playlistMetadata: MediaMetadata = MediaMetadata.EMPTY
|
||||
|
||||
private val availableCommands: Player.Commands = Player.Commands.Builder()
|
||||
.add(COMMAND_PLAY_PAUSE)
|
||||
.add(COMMAND_PREPARE)
|
||||
.add(COMMAND_STOP)
|
||||
.add(COMMAND_RELEASE)
|
||||
.add(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM)
|
||||
.add(COMMAND_SEEK_TO_DEFAULT_POSITION)
|
||||
.add(COMMAND_SEEK_BACK)
|
||||
.add(COMMAND_SEEK_FORWARD)
|
||||
.add(COMMAND_SET_SPEED_AND_PITCH)
|
||||
@@ -86,357 +99,475 @@ class MpvPlayer(ctx: Context) : SimpleBasePlayer(Looper.getMainLooper()), MPVLib
|
||||
.add(COMMAND_SET_TRACK_SELECTION_PARAMETERS)
|
||||
.build()
|
||||
|
||||
override fun getState(): State {
|
||||
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()
|
||||
override fun getApplicationLooper(): Looper = applicationLooper
|
||||
|
||||
val builder = State.Builder()
|
||||
.setAvailableCommands(availableCommands)
|
||||
.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(
|
||||
(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 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 addListener(listener: Player.Listener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
override fun handleSetPlayWhenReady(playWhenReady: Boolean): ListenableFuture<*> {
|
||||
mpv.setPropertyBoolean("pause", !playWhenReady)
|
||||
return Futures.immediateVoidFuture()
|
||||
override fun removeListener(listener: Player.Listener) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
override fun handleSetPlaybackParameters(playbackParameters: PlaybackParameters): ListenableFuture<*> {
|
||||
mpv.setPropertyDouble("speed", playbackParameters.speed.coerceAtLeast(0f).toDouble())
|
||||
return Futures.immediateVoidFuture()
|
||||
override fun setMediaItems(mediaItems: List<MediaItem>, resetPosition: Boolean) {
|
||||
setMediaItems(
|
||||
mediaItems,
|
||||
if (resetPosition) 0 else C.INDEX_UNSET,
|
||||
if (resetPosition) 0L else C.TIME_UNSET
|
||||
)
|
||||
}
|
||||
|
||||
override fun handleSeek(
|
||||
mediaItemIndex: Int,
|
||||
positionMs: Long,
|
||||
seekCommand: Int
|
||||
): ListenableFuture<*> {
|
||||
if (mediaItemIndex != C.INDEX_UNSET && mediaItemIndex != 0) {
|
||||
return Futures.immediateVoidFuture()
|
||||
}
|
||||
val targetMs = if (positionMs == C.TIME_UNSET) 0L else positionMs.coerceAtLeast(0L)
|
||||
mpv.command(arrayOf("seek", (targetMs / 1000.0).toString(), "absolute"))
|
||||
return Futures.immediateVoidFuture()
|
||||
}
|
||||
|
||||
override fun handleSetVolume(volume: Float): ListenableFuture<*> {
|
||||
mpv.setPropertyDouble("volume", volume.coerceIn(0f, 1f) * 100.0)
|
||||
return Futures.immediateVoidFuture()
|
||||
}
|
||||
|
||||
override fun handleSetVideoOutput(videoOutput: Any): ListenableFuture<*> {
|
||||
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.immediateVoidFuture()
|
||||
}
|
||||
|
||||
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<*> {
|
||||
override fun setMediaItems(mediaItems: List<MediaItem>, startIndex: Int, startPositionMs: Long) {
|
||||
val target = when {
|
||||
mediaItems.isEmpty() -> null
|
||||
startIndex in mediaItems.indices -> mediaItems[startIndex]
|
||||
else -> mediaItems[0]
|
||||
}
|
||||
|
||||
// currentMediaItem = target
|
||||
// trackGroupsByType = emptyMap()
|
||||
currentMediaItem = target
|
||||
playlistMetadata = target?.mediaMetadata ?: MediaMetadata.EMPTY
|
||||
playerError = null
|
||||
|
||||
mpv.command(arrayOf("stop"))
|
||||
|
||||
val targetMs = if (startPositionMs == C.TIME_UNSET) 0L else startPositionMs.coerceAtLeast(0L)
|
||||
mpv.setPropertyDouble("start", targetMs / 1000.0)
|
||||
|
||||
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 addMediaItems(
|
||||
p0: Int,
|
||||
p1: List<MediaItem?>?
|
||||
) = Unit
|
||||
|
||||
override fun moveMediaItems(fromIndex: Int, toIndex: Int, newIndex: Int) = Unit
|
||||
|
||||
override fun replaceMediaItems(fromIndex: Int, toIndex: Int, mediaItems: List<MediaItem>) {
|
||||
setMediaItems(mediaItems)
|
||||
}
|
||||
|
||||
override fun handleStop(): ListenableFuture<*> {
|
||||
override fun removeMediaItems(fromIndex: Int, toIndex: Int) {
|
||||
currentMediaItem = null
|
||||
playlistMetadata = MediaMetadata.EMPTY
|
||||
mpv.command(arrayOf("stop"))
|
||||
return Futures.immediateVoidFuture()
|
||||
}
|
||||
|
||||
override fun handleRelease(): ListenableFuture<*> {
|
||||
override fun getAvailableCommands(): Player.Commands = availableCommands
|
||||
|
||||
override fun prepare() = Unit
|
||||
|
||||
override fun getPlaybackState(): Int {
|
||||
val ended = mpv.getPropertyBoolean("eof-reached") ?: false
|
||||
if (ended) return Player.STATE_ENDED
|
||||
|
||||
val idle = mpv.getPropertyBoolean("core-idle") ?: false
|
||||
if (idle) return Player.STATE_IDLE
|
||||
|
||||
val loading = mpv.getPropertyBoolean("paused-for-cache") ?: false
|
||||
return if (loading) Player.STATE_BUFFERING else Player.STATE_READY
|
||||
}
|
||||
|
||||
override fun getPlaybackSuppressionReason(): Int = Player.PLAYBACK_SUPPRESSION_REASON_NONE
|
||||
|
||||
override fun getPlayerError(): PlaybackException? = playerError
|
||||
|
||||
override fun setPlayWhenReady(playWhenReady: Boolean) {
|
||||
mpv.setPropertyBoolean("pause", !playWhenReady)
|
||||
}
|
||||
|
||||
override fun getPlayWhenReady(): Boolean = !(mpv.getPropertyBoolean("pause") ?: true)
|
||||
|
||||
override fun setRepeatMode(repeatMode: Int) = Unit
|
||||
|
||||
override fun getRepeatMode(): Int = Player.REPEAT_MODE_OFF
|
||||
|
||||
override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) = Unit
|
||||
|
||||
override fun getShuffleModeEnabled(): Boolean = false
|
||||
|
||||
override fun isLoading(): Boolean = mpv.getPropertyBoolean("paused-for-cache") ?: false
|
||||
|
||||
override fun seekTo(mediaItemIndex: Int, positionMs: Long, seekCommand: Int, isRepeatingCurrentItem: Boolean) {
|
||||
if (mediaItemIndex != C.INDEX_UNSET && mediaItemIndex != 0) return
|
||||
val targetMs = if (positionMs == C.TIME_UNSET) 0L else positionMs.coerceAtLeast(0L)
|
||||
mpv.command(arrayOf("seek", (targetMs / 1000.0).toString(), "absolute"))
|
||||
}
|
||||
|
||||
override fun getSeekBackIncrement(): Long = 5_000L
|
||||
|
||||
override fun getSeekForwardIncrement(): Long = 15_000L
|
||||
|
||||
override fun setPlaybackParameters(playbackParameters: PlaybackParameters) {
|
||||
mpv.setPropertyDouble("speed", playbackParameters.speed.coerceAtLeast(0f).toDouble())
|
||||
}
|
||||
|
||||
override fun getPlaybackParameters(): PlaybackParameters {
|
||||
return PlaybackParameters((mpv.getPropertyDouble("speed") ?: 1.0).toFloat().coerceAtLeast(0f))
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
mpv.command(arrayOf("stop"))
|
||||
}
|
||||
|
||||
override fun release() {
|
||||
mpv.removeObserver(this)
|
||||
mpv.detachSurface()
|
||||
clearVideoSurface()
|
||||
mpv.destroy()
|
||||
return Futures.immediateVoidFuture()
|
||||
}
|
||||
|
||||
override fun event(event: Int) {
|
||||
// trackGroupsByType = buildTrackGroupsByType()
|
||||
postInvalidateState()
|
||||
override fun getCurrentTracks(): Tracks {
|
||||
val count = mpv.getPropertyInt("track-list/count") ?: 0
|
||||
if (count <= 0) return Tracks.EMPTY
|
||||
|
||||
data class Entry(val id: Int, val format: Format)
|
||||
val grouped = LinkedHashMap<Int, MutableList<Entry>>()
|
||||
for (i in 0 until count) {
|
||||
val base = "track-list/$i"
|
||||
val type = when (mpv.getPropertyString("$base/type")) {
|
||||
"video" -> TRACK_TYPE_VIDEO
|
||||
"audio" -> TRACK_TYPE_AUDIO
|
||||
"sub" -> TRACK_TYPE_TEXT
|
||||
else -> TRACK_TYPE_UNKNOWN
|
||||
}
|
||||
if (type == TRACK_TYPE_UNKNOWN) continue
|
||||
|
||||
val id = mpv.getPropertyInt("$base/id") ?: continue
|
||||
val label = mpv.getPropertyString("$base/title")
|
||||
val language = mpv.getPropertyString("$base/lang")
|
||||
val codec = mpv.getPropertyString("$base/codec")
|
||||
|
||||
val format = Format.Builder()
|
||||
.setId(id.toString())
|
||||
.setLabel(label)
|
||||
.setLanguage(language)
|
||||
.setCodecs(codec)
|
||||
.build()
|
||||
|
||||
grouped.getOrPut<Int, MutableList<Entry>>(type) { mutableListOf<Entry>() }
|
||||
.add(Entry(id, format))
|
||||
}
|
||||
val selectedVideo = mpv.getPropertyInt("vid")
|
||||
val selectedAudio = mpv.getPropertyInt("aid")
|
||||
val selectedSubtitle = mpv.getPropertyInt("sid")
|
||||
val result = ArrayList<Group>()
|
||||
for ((type, entries) in grouped) {
|
||||
if (entries.isEmpty()) continue
|
||||
val group = TrackGroup("mpv-$type", *entries.map { it.format }.toTypedArray<Format>())
|
||||
val selected = BooleanArray(entries.size) { idx ->
|
||||
val id = entries[idx].id
|
||||
when (type) {
|
||||
TRACK_TYPE_VIDEO -> selectedVideo == id
|
||||
TRACK_TYPE_AUDIO -> selectedAudio == id
|
||||
TRACK_TYPE_TEXT -> selectedSubtitle == id
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
val support = IntArray(entries.size) { FORMAT_HANDLED }
|
||||
result.add(Group(group, false, support, selected))
|
||||
}
|
||||
return Tracks(result)
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String) {
|
||||
// if (property == "vid" || property == "aid" || property == "sid") {
|
||||
// trackGroupsByType = buildTrackGroupsByType()
|
||||
// }
|
||||
postInvalidateState()
|
||||
}
|
||||
override fun getTrackSelectionParameters(): TrackSelectionParameters = currentTrackSelectionParameters
|
||||
|
||||
override fun eventProperty(property: String, value: Long) {
|
||||
// if (property == "vid" || property == "aid" || property == "sid") {
|
||||
// trackGroupsByType = buildTrackGroupsByType()
|
||||
// }
|
||||
postInvalidateState()
|
||||
}
|
||||
override fun setTrackSelectionParameters(trackSelectionParameters: TrackSelectionParameters) {
|
||||
currentTrackSelectionParameters = trackSelectionParameters
|
||||
if (trackSelectionParameters.disabledTrackTypes.contains(TRACK_TYPE_VIDEO)) {
|
||||
mpv.setPropertyString("vid", "no")
|
||||
}
|
||||
if (trackSelectionParameters.disabledTrackTypes.contains(TRACK_TYPE_AUDIO)) {
|
||||
mpv.setPropertyString("aid", "no")
|
||||
}
|
||||
if (trackSelectionParameters.disabledTrackTypes.contains(TRACK_TYPE_TEXT)) {
|
||||
mpv.setPropertyString("sid", "no")
|
||||
}
|
||||
for (override in trackSelectionParameters.overrides.values) {
|
||||
val selectedIndex = override.trackIndices.firstOrNull() ?: continue
|
||||
if (selectedIndex !in 0 until override.mediaTrackGroup.length) continue
|
||||
val trackId =
|
||||
override.mediaTrackGroup.getFormat(selectedIndex).id?.toIntOrNull() ?: continue
|
||||
|
||||
override fun eventProperty(property: String, value: Double) {
|
||||
postInvalidateState()
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String, value: Boolean) {
|
||||
postInvalidateState()
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String, value: String) {
|
||||
postInvalidateState()
|
||||
}
|
||||
|
||||
private fun postInvalidateState() {
|
||||
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
invalidateState()
|
||||
} else {
|
||||
mainHandler.post { invalidateState() }
|
||||
when (override.type) {
|
||||
TRACK_TYPE_VIDEO -> mpv.setPropertyInt("vid", trackId)
|
||||
TRACK_TYPE_AUDIO -> mpv.setPropertyInt("aid", trackId)
|
||||
TRACK_TYPE_TEXT -> mpv.setPropertyInt("sid", trackId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMpvPlaybackState(): Int {
|
||||
val ended = mpv.getPropertyBoolean("eof-reached") ?: false
|
||||
if (ended) return STATE_ENDED
|
||||
override fun getMediaMetadata(): MediaMetadata = currentMediaItem?.mediaMetadata ?: MediaMetadata.EMPTY
|
||||
|
||||
val idle = mpv.getPropertyBoolean("core-idle") ?: false
|
||||
if (idle) return STATE_IDLE
|
||||
override fun getPlaylistMetadata(): MediaMetadata = playlistMetadata
|
||||
|
||||
val loading = mpv.getPropertyBoolean("paused-for-cache") ?: false
|
||||
return if (loading) STATE_BUFFERING else STATE_READY
|
||||
override fun setPlaylistMetadata(mediaMetadata: MediaMetadata) {
|
||||
playlistMetadata = mediaMetadata
|
||||
}
|
||||
|
||||
// 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)
|
||||
// }
|
||||
override fun getCurrentTimeline(): Timeline {
|
||||
val item = currentMediaItem ?: return Timeline.EMPTY
|
||||
val durationUs = if (duration == TIME_UNSET) TIME_UNSET else duration * 1000L
|
||||
return object : Timeline() {
|
||||
override fun getWindowCount(): Int = 1
|
||||
|
||||
// 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
|
||||
// }
|
||||
override fun getWindow(windowIndex: Int, window: Window, defaultPositionProjectionUs: Long): Window {
|
||||
check(windowIndex == 0)
|
||||
return window.set(
|
||||
Window.SINGLE_WINDOW_UID,
|
||||
item,
|
||||
null,
|
||||
C.TIME_UNSET,
|
||||
C.TIME_UNSET,
|
||||
C.TIME_UNSET,
|
||||
true,
|
||||
false,
|
||||
item.liveConfiguration,
|
||||
0L,
|
||||
durationUs,
|
||||
0,
|
||||
0,
|
||||
0L
|
||||
)
|
||||
}
|
||||
|
||||
// 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)
|
||||
// }
|
||||
override fun getPeriodCount(): Int = 1
|
||||
|
||||
override fun getPeriod(periodIndex: Int, period: Period, setIds: Boolean): Period {
|
||||
check(periodIndex == 0)
|
||||
val uid = Window.SINGLE_WINDOW_UID
|
||||
val id = if (setIds) uid else null
|
||||
return period.set(id, uid, 0, durationUs, 0L)
|
||||
}
|
||||
|
||||
override fun getIndexOfPeriod(uid: Any): Int {
|
||||
return if (uid == Window.SINGLE_WINDOW_UID) 0 else C.INDEX_UNSET
|
||||
}
|
||||
|
||||
override fun getUidOfPeriod(periodIndex: Int): Any {
|
||||
check(periodIndex == 0)
|
||||
return Window.SINGLE_WINDOW_UID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCurrentPeriodIndex() = if (currentMediaItem == null) C.INDEX_UNSET else 0
|
||||
|
||||
override fun getCurrentMediaItemIndex() =
|
||||
if (currentMediaItem == null) C.INDEX_UNSET else 0
|
||||
|
||||
override fun getDuration() =
|
||||
((mpv.getPropertyDouble("duration") ?: 0.0).coerceAtLeast(0.0) * 1000.0).toLong()
|
||||
|
||||
override fun getCurrentPosition() =
|
||||
((mpv.getPropertyDouble("time-pos") ?: 0.0).coerceAtLeast(0.0) * 1000.0).toLong()
|
||||
|
||||
override fun getBufferedPosition() =
|
||||
((mpv.getPropertyDouble("demuxer-cache-time") ?: 0.0).coerceAtLeast(0.0) * 1000.0).toLong()
|
||||
.coerceAtLeast(0L)
|
||||
|
||||
override fun getTotalBufferedDuration() = bufferedPosition
|
||||
|
||||
override fun isPlayingAd(): Boolean = false
|
||||
|
||||
override fun getCurrentAdGroupIndex(): Int = C.INDEX_UNSET
|
||||
|
||||
override fun getCurrentAdIndexInAdGroup(): Int = C.INDEX_UNSET
|
||||
|
||||
override fun getMaxSeekToPreviousPosition() = 3_000L
|
||||
|
||||
override fun getContentPosition() = getCurrentPosition()
|
||||
|
||||
override fun getContentBufferedPosition() = getBufferedPosition()
|
||||
|
||||
override fun getAudioAttributes(): AudioAttributes = AudioAttributes.DEFAULT
|
||||
|
||||
override fun setVolume(volume: Float) {
|
||||
mpv.setPropertyDouble("volume", volume.coerceIn(0f, 1f) * 100.0)
|
||||
}
|
||||
|
||||
override fun getVolume(): Float {
|
||||
return ((mpv.getPropertyDouble("volume") ?: 100.0).coerceIn(0.0, 100.0) / 100.0).toFloat()
|
||||
}
|
||||
|
||||
override fun clearVideoSurface() {
|
||||
mpv.setOptionString("vo", "null")
|
||||
mpv.setOptionString("force-window", "no")
|
||||
mpv.detachSurface()
|
||||
}
|
||||
|
||||
override fun clearVideoSurface(surface: Surface?) {
|
||||
clearVideoSurface()
|
||||
}
|
||||
|
||||
override fun setVideoSurface(surface: Surface?) {
|
||||
mpv.attachSurface(surface ?: return clearVideoSurface() )
|
||||
mpv.setOptionString("force-window", "yes")
|
||||
mpv.setOptionString("vo", "gpu-next")
|
||||
}
|
||||
|
||||
override fun setVideoSurfaceHolder(surfaceHolder: SurfaceHolder?) {
|
||||
val holder = surfaceHolder ?: return clearVideoSurface()
|
||||
val surface = holder.surface
|
||||
if (surface != null && surface.isValid) {
|
||||
setVideoSurface(surface)
|
||||
val frame = holder.surfaceFrame
|
||||
val width = frame?.width() ?: 0
|
||||
val height = frame?.height() ?: 0
|
||||
if (width > 0 && height > 0) {
|
||||
mpv.setPropertyString("android-surface-size", "${width}x${height}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearVideoSurfaceHolder(surfaceHolder: SurfaceHolder?) {
|
||||
clearVideoSurface()
|
||||
}
|
||||
|
||||
override fun setVideoSurfaceView(surfaceView: SurfaceView?) {
|
||||
val view = surfaceView ?: return clearVideoSurface()
|
||||
setVideoSurfaceHolder(view.holder)
|
||||
}
|
||||
|
||||
override fun clearVideoSurfaceView(surfaceView: SurfaceView?) {
|
||||
val view = surfaceView ?: return clearVideoSurface()
|
||||
clearVideoSurfaceHolder(view.holder)
|
||||
}
|
||||
|
||||
override fun setVideoTextureView(textureView: TextureView?) = Unit
|
||||
|
||||
override fun clearVideoTextureView(textureView: TextureView?) = Unit
|
||||
|
||||
override fun getVideoSize(): VideoSize = VideoSize.UNKNOWN
|
||||
|
||||
override fun getSurfaceSize(): Size = Size.UNKNOWN
|
||||
|
||||
override fun getCurrentCues(): CueGroup = CueGroup.EMPTY_TIME_ZERO
|
||||
|
||||
override fun getDeviceInfo(): DeviceInfo = DeviceInfo.UNKNOWN
|
||||
|
||||
override fun getDeviceVolume(): Int = 0
|
||||
|
||||
override fun isDeviceMuted(): Boolean = false
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun setDeviceVolume(volume: Int) = Unit
|
||||
|
||||
override fun setDeviceVolume(volume: Int, flags: Int) = Unit
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun increaseDeviceVolume() = Unit
|
||||
|
||||
override fun increaseDeviceVolume(flags: Int) = Unit
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun decreaseDeviceVolume() = Unit
|
||||
|
||||
override fun decreaseDeviceVolume(flags: Int) = Unit
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun setDeviceMuted(muted: Boolean) = Unit
|
||||
|
||||
override fun setDeviceMuted(muted: Boolean, flags: Int) = Unit
|
||||
|
||||
override fun setAudioAttributes(audioAttributes: AudioAttributes, handleAudioFocus: Boolean) = Unit
|
||||
|
||||
override fun event(eventId: Int) {
|
||||
when (eventId) {
|
||||
MPVLib.MpvEvent.MPV_EVENT_SEEK,
|
||||
MPVLib.MpvEvent.MPV_EVENT_PLAYBACK_RESTART -> {
|
||||
val positionMs = getCurrentPosition()
|
||||
val position = Player.PositionInfo(
|
||||
SINGLE_WINDOW_UID,
|
||||
getCurrentMediaItemIndex(),
|
||||
getCurrentMediaItem(),
|
||||
SINGLE_WINDOW_UID,
|
||||
getCurrentPeriodIndex(),
|
||||
positionMs,
|
||||
positionMs,
|
||||
INDEX_UNSET,
|
||||
INDEX_UNSET
|
||||
)
|
||||
listeners.forEach {
|
||||
it.onPositionDiscontinuity(position, position, Player.DISCONTINUITY_REASON_SEEK)
|
||||
}
|
||||
}
|
||||
|
||||
MPVLib.MpvEvent.MPV_EVENT_QUEUE_OVERFLOW -> {
|
||||
playerError = PlaybackException(
|
||||
"mpv event queue overflow",
|
||||
null,
|
||||
PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK
|
||||
)
|
||||
listeners.forEach {
|
||||
it.onPlayerErrorChanged(playerError)
|
||||
it.onPlayerError(playerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String) = Unit
|
||||
|
||||
override fun eventProperty(property: String, value: Long) {
|
||||
when (property) {
|
||||
"vid", "aid", "sid" -> {
|
||||
listeners.forEach { it.onTracksChanged(getCurrentTracks()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String, value: Double) {
|
||||
when (property) {
|
||||
"duration" ->
|
||||
listeners.forEach {
|
||||
it.onTimelineChanged(currentTimeline, TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
|
||||
}
|
||||
|
||||
"speed" ->
|
||||
listeners.forEach {
|
||||
it.onPlaybackParametersChanged(PlaybackParameters(value.toFloat().coerceAtLeast(0f)))
|
||||
}
|
||||
|
||||
"volume" ->
|
||||
listeners.forEach {
|
||||
it.onVolumeChanged((value.coerceIn(0.0, 100.0) / 100.0).toFloat())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String, value: Boolean) {
|
||||
when (property) {
|
||||
"pause" ->
|
||||
listeners.forEach {
|
||||
it.onPlayWhenReadyChanged(
|
||||
!value,
|
||||
PLAY_WHEN_READY_CHANGE_REASON_REMOTE
|
||||
)
|
||||
it.onIsPlayingChanged(!value)
|
||||
}
|
||||
|
||||
"core-idle", "eof-reached" ->
|
||||
listeners.forEach { it.onPlaybackStateChanged(getPlaybackState())
|
||||
it.onIsPlayingChanged(false) }
|
||||
|
||||
"paused-for-cache" -> {
|
||||
listeners.forEach {
|
||||
it.onIsLoadingChanged(value)
|
||||
it.onPlaybackStateChanged(getPlaybackState())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String, value: String) = Unit
|
||||
|
||||
// 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,27 +1,24 @@
|
||||
package dev.zoriya.omni
|
||||
|
||||
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 java.util.ArrayList
|
||||
import androidx.core.net.toUri
|
||||
import androidx.media3.common.TrackSelectionOverride
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
class OmniPlayer : HybridOmniPlayerSpec() {
|
||||
private val ctx = NitroModules.applicationContext ?: throw Error("No Context available!")
|
||||
val player = MpvPlayer(ctx)
|
||||
override val eventMap = EventMap(player.mpv)
|
||||
private var currentSource: Source? = null
|
||||
override val eventMap = EventMap(player)
|
||||
|
||||
override fun dispose() {
|
||||
super.dispose()
|
||||
@@ -30,6 +27,7 @@ class OmniPlayer : HybridOmniPlayerSpec() {
|
||||
player.release()
|
||||
}
|
||||
|
||||
private var currentSource: Source? = null
|
||||
override var source: Source
|
||||
get() = currentSource
|
||||
?: throw IllegalStateException("source should be initialized before get")
|
||||
@@ -130,21 +128,14 @@ class OmniPlayer : HybridOmniPlayerSpec() {
|
||||
}
|
||||
|
||||
override fun selectSubtitle(subtitle: Track?) {
|
||||
// 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)
|
||||
if (subtitle == null) {
|
||||
player.trackSelectionParameters = player.trackSelectionParameters
|
||||
.buildUpon()
|
||||
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true)
|
||||
.build()
|
||||
return
|
||||
}
|
||||
selectTrack(C.TRACK_TYPE_TEXT, subtitle)
|
||||
}
|
||||
|
||||
override fun selectRendition(rendition: Rendition?) {
|
||||
@@ -158,18 +149,18 @@ class OmniPlayer : HybridOmniPlayerSpec() {
|
||||
.setAlbumTitle(source.metadata?.album)
|
||||
.setArtist(source.metadata?.artist)
|
||||
.apply {
|
||||
source.metadata?.imageLink?.let { setArtworkUri(android.net.Uri.parse(it)) }
|
||||
source.metadata?.imageLink?.let { setArtworkUri(it.toUri()) }
|
||||
}
|
||||
.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 subtitleConfigurations = source.subtitles.map { subtitle ->
|
||||
MediaItem.SubtitleConfiguration.Builder(subtitle.link.toUri())
|
||||
.setId(subtitle.id)
|
||||
.setLanguage(subtitle.language)
|
||||
.setLabel(subtitle.label)
|
||||
.setMimeType(subtitle.mimeType)
|
||||
.build()
|
||||
}
|
||||
|
||||
// val headers = Bundle().apply {
|
||||
// putStringArrayList(
|
||||
@@ -186,7 +177,7 @@ class OmniPlayer : HybridOmniPlayerSpec() {
|
||||
// }
|
||||
|
||||
val requestMetadata = MediaItem.RequestMetadata.Builder()
|
||||
.setMediaUri(android.net.Uri.parse(src.uri))
|
||||
.setMediaUri(src.uri.toUri())
|
||||
// .setExtras(headers)
|
||||
.build()
|
||||
|
||||
@@ -195,54 +186,49 @@ class OmniPlayer : HybridOmniPlayerSpec() {
|
||||
.setMimeType(src.mimeType)
|
||||
.setMediaId(src.uri)
|
||||
.setMediaMetadata(mediaMetadata)
|
||||
// .setSubtitleConfigurations(subtitleConfigurations)
|
||||
.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 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()
|
||||
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
|
||||
// }
|
||||
// }
|
||||
val groups = player.currentTracks.groups.filter { it.type == trackType }
|
||||
for (group in groups) {
|
||||
for (i in 0 until group.length) {
|
||||
val formatId = group.getTrackFormat(i).id ?: group.mediaTrackGroup.id
|
||||
if (formatId != track.id) continue
|
||||
|
||||
player.trackSelectionParameters = player.trackSelectionParameters
|
||||
.buildUpon()
|
||||
.setTrackTypeDisabled(trackType, false)
|
||||
.setOverrideForType(TrackSelectionOverride(group.mediaTrackGroup, i))
|
||||
.build()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user