mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Update MPV playback threading model (#481)
There's no user facing changes in this PR This PR updates `MpvPlayer` to use a threading model more similar to [ExoPlayer's model](https://developer.android.com/reference/androidx/media3/exoplayer/ExoPlayer#threading-model). There's now a dedicated thread looper message queue between the UI interactions on the main thread and calls to `libmpv`. This is necessary because previously, the main thread would make the calls into `libmpv`. `libmpv` uses internal mutex locking for some operations which meant that the main thread would be blocked momentarily. Now, the UI issues a command (eg seek to X position) and the command is added to the thread looper queue and processed on that separate thread. This uses the standard Android Handler/Looper mechanisms. Related to #235
This commit is contained in:
parent
7320c12f0c
commit
8b40e89b8a
5 changed files with 411 additions and 182 deletions
|
|
@ -187,4 +187,14 @@ suspend fun upgradeApp(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (previous.isEqualOrBefore(Version.fromString("0.3.6-52-g0"))) {
|
||||||
|
if (Build.MODEL.equals("shield android tv", ignoreCase = true)) {
|
||||||
|
appPreferences.updateData {
|
||||||
|
it.updateMpvOptions {
|
||||||
|
useGpuNext = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package com.github.damontecres.wholphin.util.mpv
|
||||||
|
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class MpvLogger : MPVLib.LogObserver {
|
||||||
|
override fun logMessage(
|
||||||
|
prefix: String,
|
||||||
|
level: Int,
|
||||||
|
text: String,
|
||||||
|
) {
|
||||||
|
// https://github.com/mpv-player/mpv/blob/122abdfec3124bfc92a2918a70ca8150eee68338/include/mpv/client.h#L1423
|
||||||
|
when {
|
||||||
|
level <= 10 -> Timber.wtf("%s, %s", prefix, text)
|
||||||
|
level <= 20 -> Timber.e("%s, %s", prefix, text)
|
||||||
|
level <= 30 -> Timber.w("%s, %s", prefix, text)
|
||||||
|
level <= 40 -> Timber.i("%s, %s", prefix, text)
|
||||||
|
level <= 50 -> Timber.d("%s, %s", prefix, text)
|
||||||
|
else -> Timber.v("%s, %s", prefix, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,10 @@ package com.github.damontecres.wholphin.util.mpv
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
|
import android.os.HandlerThread
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.os.Message
|
||||||
|
import android.os.Process
|
||||||
import android.view.Surface
|
import android.view.Surface
|
||||||
import android.view.SurfaceHolder
|
import android.view.SurfaceHolder
|
||||||
import android.view.SurfaceView
|
import android.view.SurfaceView
|
||||||
|
|
@ -35,6 +38,7 @@ import androidx.media3.common.util.Util
|
||||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||||
import androidx.media3.exoplayer.trackselection.TrackSelector
|
import androidx.media3.exoplayer.trackselection.TrackSelector
|
||||||
import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter
|
import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter
|
||||||
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEndFileReason.MPV_END_FILE_REASON_EOF
|
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEndFileReason.MPV_END_FILE_REASON_EOF
|
||||||
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEndFileReason.MPV_END_FILE_REASON_ERROR
|
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEndFileReason.MPV_END_FILE_REASON_ERROR
|
||||||
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEndFileReason.MPV_END_FILE_REASON_STOP
|
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEndFileReason.MPV_END_FILE_REASON_STOP
|
||||||
|
|
@ -42,9 +46,12 @@ import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEvent.MPV_EVENT_AUDIO_
|
||||||
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEvent.MPV_EVENT_END_FILE
|
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEvent.MPV_EVENT_END_FILE
|
||||||
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEvent.MPV_EVENT_FILE_LOADED
|
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEvent.MPV_EVENT_FILE_LOADED
|
||||||
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEvent.MPV_EVENT_PLAYBACK_RESTART
|
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEvent.MPV_EVENT_PLAYBACK_RESTART
|
||||||
|
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEvent.MPV_EVENT_START_FILE
|
||||||
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEvent.MPV_EVENT_VIDEO_RECONFIG
|
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEvent.MPV_EVENT_VIDEO_RECONFIG
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import kotlin.concurrent.atomics.AtomicReference
|
||||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||||
|
import kotlin.concurrent.atomics.update
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
|
@ -57,20 +64,22 @@ import kotlin.time.Duration.Companion.seconds
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
class MpvPlayer(
|
class MpvPlayer(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
enableHardwareDecoding: Boolean,
|
private val enableHardwareDecoding: Boolean,
|
||||||
useGpuNext: Boolean,
|
private val useGpuNext: Boolean,
|
||||||
) : BasePlayer(),
|
) : BasePlayer(),
|
||||||
MPVLib.EventObserver,
|
MPVLib.EventObserver,
|
||||||
TrackSelector.InvalidationListener {
|
TrackSelector.InvalidationListener,
|
||||||
|
Handler.Callback {
|
||||||
companion object {
|
companion object {
|
||||||
private const val DEBUG = false
|
private const val DEBUG = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isPaused: Boolean = true
|
|
||||||
private var surface: Surface? = null
|
private var surface: Surface? = null
|
||||||
|
private val playbackState = AtomicReference<PlaybackState>(PlaybackState.EMPTY)
|
||||||
|
|
||||||
|
// This looper will sent events to the main thread
|
||||||
private val looper = Util.getCurrentOrMainLooper()
|
private val looper = Util.getCurrentOrMainLooper()
|
||||||
private val handler = Handler(looper)
|
private val mainHandler = Handler(looper)
|
||||||
private val listeners =
|
private val listeners =
|
||||||
ListenerSet<Player.Listener>(
|
ListenerSet<Player.Listener>(
|
||||||
looper,
|
looper,
|
||||||
|
|
@ -81,19 +90,16 @@ class MpvPlayer(
|
||||||
private val availableCommands: Player.Commands
|
private val availableCommands: Player.Commands
|
||||||
private val trackSelector = DefaultTrackSelector(context)
|
private val trackSelector = DefaultTrackSelector(context)
|
||||||
|
|
||||||
private var mediaItem: MediaItem? = null
|
// This thread/looper will receive commands from the main thread to execute
|
||||||
private var startPositionMs: Long = 0L
|
private val thread: HandlerThread =
|
||||||
private var durationMs: Long = 0L
|
HandlerThread("MpvPlayer:Playback", Process.THREAD_PRIORITY_AUDIO)
|
||||||
private var positionMs: Long = -1L
|
.also { it.start() }
|
||||||
private var playbackState: Int = STATE_READY
|
private val internalHandler: Handler = Handler(thread.looper, this)
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
var isReleased = false
|
var isReleased = false
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@Volatile
|
|
||||||
private var isLoadingFile = false
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Timber.v("config-dir=${context.filesDir.path}")
|
Timber.v("config-dir=${context.filesDir.path}")
|
||||||
MPVLib.create(context)
|
MPVLib.create(context)
|
||||||
|
|
@ -120,8 +126,11 @@ class MpvPlayer(
|
||||||
MPVLib.setOptionString("idle", "yes")
|
MPVLib.setOptionString("idle", "yes")
|
||||||
// MPVLib.setOptionString("sub-fonts-dir", File(context.filesDir, "fonts").absolutePath)
|
// MPVLib.setOptionString("sub-fonts-dir", File(context.filesDir, "fonts").absolutePath)
|
||||||
|
|
||||||
MPVLib.addObserver(this)
|
internalHandler.post {
|
||||||
MPVProperty.observedProperties.forEach(MPVLib::observeProperty)
|
MPVLib.addObserver(this@MpvPlayer)
|
||||||
|
MPVProperty.observedProperties.forEach(MPVLib::observeProperty)
|
||||||
|
MPVLib.addLogObserver(MpvLogger())
|
||||||
|
}
|
||||||
|
|
||||||
availableCommands =
|
availableCommands =
|
||||||
Player.Commands
|
Player.Commands
|
||||||
|
|
@ -167,13 +176,11 @@ class MpvPlayer(
|
||||||
mediaItems: List<MediaItem>,
|
mediaItems: List<MediaItem>,
|
||||||
resetPosition: Boolean,
|
resetPosition: Boolean,
|
||||||
) {
|
) {
|
||||||
throwIfReleased()
|
|
||||||
|
|
||||||
if (DEBUG) Timber.v("setMediaItems")
|
if (DEBUG) Timber.v("setMediaItems")
|
||||||
|
throwIfReleased()
|
||||||
mediaItems.firstOrNull()?.let {
|
mediaItems.firstOrNull()?.let {
|
||||||
mediaItem = it
|
|
||||||
if (surface != null) {
|
if (surface != null) {
|
||||||
loadFile(it)
|
sendCommand(MpvCommand.LOAD_FILE, MediaAndPosition(it, C.TIME_UNSET))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -184,8 +191,11 @@ class MpvPlayer(
|
||||||
startPositionMs: Long,
|
startPositionMs: Long,
|
||||||
) {
|
) {
|
||||||
if (DEBUG) Timber.v("setMediaItems")
|
if (DEBUG) Timber.v("setMediaItems")
|
||||||
this.startPositionMs = startPositionMs
|
throwIfReleased()
|
||||||
setMediaItems(mediaItems.subList(startIndex, mediaItems.size), false)
|
sendCommand(
|
||||||
|
MpvCommand.LOAD_FILE,
|
||||||
|
MediaAndPosition(mediaItems[startIndex], startPositionMs),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addMediaItems(
|
override fun addMediaItems(
|
||||||
|
|
@ -214,17 +224,19 @@ class MpvPlayer(
|
||||||
|
|
||||||
override fun prepare() {
|
override fun prepare() {
|
||||||
if (DEBUG) Timber.v("prepare")
|
if (DEBUG) Timber.v("prepare")
|
||||||
durationMs = 0L
|
playbackState.update {
|
||||||
positionMs = -1L
|
it.copy(
|
||||||
playbackState = STATE_READY
|
state = Player.STATE_READY,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPlaybackState(): Int {
|
override fun getPlaybackState(): Int {
|
||||||
if (DEBUG) Timber.v("getPlaybackState")
|
if (DEBUG) Timber.v("getPlaybackState")
|
||||||
return playbackState
|
return playbackState.load().state
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPlaybackSuppressionReason(): Int = Player.PLAYBACK_SUPPRESSION_REASON_NONE
|
override fun getPlaybackSuppressionReason(): Int = PLAYBACK_SUPPRESSION_REASON_NONE
|
||||||
|
|
||||||
override fun getPlayerError(): PlaybackException? {
|
override fun getPlayerError(): PlaybackException? {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
|
|
@ -232,32 +244,21 @@ class MpvPlayer(
|
||||||
|
|
||||||
override fun setPlayWhenReady(playWhenReady: Boolean) {
|
override fun setPlayWhenReady(playWhenReady: Boolean) {
|
||||||
if (isReleased) return
|
if (isReleased) return
|
||||||
if (DEBUG) Timber.v("setPlayWhenReady")
|
if (DEBUG) Timber.v("setPlayWhenReady: $playWhenReady")
|
||||||
if (playWhenReady) {
|
sendCommand(MpvCommand.PLAY_PAUSE, playWhenReady)
|
||||||
MPVLib.setPropertyBoolean("pause", false)
|
|
||||||
} else {
|
|
||||||
MPVLib.setPropertyBoolean("pause", true)
|
|
||||||
}
|
|
||||||
notifyListeners(EVENT_PLAY_WHEN_READY_CHANGED) {
|
|
||||||
onPlayWhenReadyChanged(
|
|
||||||
playWhenReady,
|
|
||||||
PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPlayWhenReady(): Boolean {
|
override fun getPlayWhenReady(): Boolean {
|
||||||
if (DEBUG) Timber.v("getPlayWhenReady")
|
if (DEBUG) Timber.v("getPlayWhenReady")
|
||||||
if (isReleased) return false
|
if (isReleased) return false
|
||||||
val isPaused = MPVLib.getPropertyBoolean("pause") ?: this.isPaused
|
return !playbackState.load().isPaused
|
||||||
return !isPaused
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setRepeatMode(repeatMode: Int) {
|
override fun setRepeatMode(repeatMode: Int) {
|
||||||
if (DEBUG) Timber.v("setRepeatMode")
|
if (DEBUG) Timber.v("setRepeatMode")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getRepeatMode(): Int = Player.REPEAT_MODE_OFF
|
override fun getRepeatMode(): Int = REPEAT_MODE_OFF
|
||||||
|
|
||||||
override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) {
|
override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) {
|
||||||
if (DEBUG) Timber.v("setShuffleModeEnabled")
|
if (DEBUG) Timber.v("setShuffleModeEnabled")
|
||||||
|
|
@ -265,7 +266,7 @@ class MpvPlayer(
|
||||||
|
|
||||||
override fun getShuffleModeEnabled(): Boolean = false
|
override fun getShuffleModeEnabled(): Boolean = false
|
||||||
|
|
||||||
override fun isLoading(): Boolean = isLoadingFile
|
override fun isLoading(): Boolean = playbackState.load().isLoadingFile
|
||||||
|
|
||||||
override fun getSeekBackIncrement(): Long = 10_000
|
override fun getSeekBackIncrement(): Long = 10_000
|
||||||
|
|
||||||
|
|
@ -275,30 +276,34 @@ class MpvPlayer(
|
||||||
|
|
||||||
override fun setPlaybackParameters(playbackParameters: PlaybackParameters) {
|
override fun setPlaybackParameters(playbackParameters: PlaybackParameters) {
|
||||||
if (DEBUG) Timber.v("setPlaybackParameters")
|
if (DEBUG) Timber.v("setPlaybackParameters")
|
||||||
MPVLib.setPropertyDouble("speed", playbackParameters.speed.toDouble())
|
sendCommand(MpvCommand.SET_SPEED, playbackParameters.speed)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPlaybackParameters(): PlaybackParameters {
|
override fun getPlaybackParameters(): PlaybackParameters {
|
||||||
if (DEBUG) Timber.v("getPlaybackParameters")
|
if (DEBUG) Timber.v("getPlaybackParameters")
|
||||||
val speed = MPVLib.getPropertyDouble("speed")?.toFloat() ?: 1f
|
return PlaybackParameters(playbackState.load().speed)
|
||||||
return PlaybackParameters(speed)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
if (DEBUG) Timber.v("stop")
|
if (DEBUG) Timber.v("stop")
|
||||||
if (isReleased) return
|
if (isReleased) return
|
||||||
pause()
|
pause()
|
||||||
mediaItem = null
|
internalHandler.removeCallbacks(updatePlaybackState)
|
||||||
positionMs = -1L
|
playbackState.update {
|
||||||
durationMs = 0L
|
PlaybackState.EMPTY
|
||||||
playbackState = STATE_IDLE
|
}
|
||||||
notifyListeners(EVENT_IS_PLAYING_CHANGED) { onIsPlayingChanged(false) }
|
notifyListeners(EVENT_IS_PLAYING_CHANGED) { onIsPlayingChanged(false) }
|
||||||
notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) { onPlaybackStateChanged(STATE_IDLE) }
|
notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) { onPlaybackStateChanged(STATE_IDLE) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun release() {
|
override fun release() {
|
||||||
Timber.i("release")
|
Timber.i("release")
|
||||||
|
internalHandler.removeCallbacks(updatePlaybackState)
|
||||||
|
playbackState.update {
|
||||||
|
PlaybackState.EMPTY
|
||||||
|
}
|
||||||
if (!isReleased) {
|
if (!isReleased) {
|
||||||
|
thread.quit()
|
||||||
MPVLib.removeObserver(this)
|
MPVLib.removeObserver(this)
|
||||||
clearVideoSurfaceView(null)
|
clearVideoSurfaceView(null)
|
||||||
MPVLib.destroy()
|
MPVLib.destroy()
|
||||||
|
|
@ -309,7 +314,7 @@ class MpvPlayer(
|
||||||
override fun getCurrentTracks(): Tracks {
|
override fun getCurrentTracks(): Tracks {
|
||||||
if (DEBUG) Timber.v("getCurrentTracks")
|
if (DEBUG) Timber.v("getCurrentTracks")
|
||||||
if (isReleased) return Tracks.EMPTY
|
if (isReleased) return Tracks.EMPTY
|
||||||
return getTracks()
|
return playbackState.load().tracks
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTrackSelectionParameters(): TrackSelectionParameters {
|
override fun getTrackSelectionParameters(): TrackSelectionParameters {
|
||||||
|
|
@ -323,16 +328,16 @@ class MpvPlayer(
|
||||||
override fun setTrackSelectionParameters(parameters: TrackSelectionParameters) {
|
override fun setTrackSelectionParameters(parameters: TrackSelectionParameters) {
|
||||||
Timber.v("TrackSelection: setTrackSelectionParameters %s", parameters)
|
Timber.v("TrackSelection: setTrackSelectionParameters %s", parameters)
|
||||||
if (isReleased) return
|
if (isReleased) return
|
||||||
val tracks = getTracks()
|
val tracks = playbackState.load().tracks
|
||||||
if (C.TRACK_TYPE_TEXT in parameters.disabledTrackTypes) {
|
if (C.TRACK_TYPE_TEXT in parameters.disabledTrackTypes) {
|
||||||
// Subtitles disabled
|
// Subtitles disabled
|
||||||
Timber.v("TrackSelection: disabling subtitles")
|
Timber.v("TrackSelection: disabling subtitles")
|
||||||
MPVLib.setPropertyString("sid", "no")
|
sendCommand(MpvCommand.SET_TRACK_SELECTION, TrackSelection("sid", "no"))
|
||||||
}
|
}
|
||||||
if (C.TRACK_TYPE_AUDIO in parameters.disabledTrackTypes) {
|
if (C.TRACK_TYPE_AUDIO in parameters.disabledTrackTypes) {
|
||||||
// Audio disabled
|
// Audio disabled
|
||||||
Timber.v("TrackSelection: disabling audio")
|
Timber.v("TrackSelection: disabling audio")
|
||||||
MPVLib.setPropertyString("aid", "no")
|
sendCommand(MpvCommand.SET_TRACK_SELECTION, TrackSelection("aid", "no"))
|
||||||
}
|
}
|
||||||
Timber.v("TrackSelection: Got ${parameters.overrides.size} overrides")
|
Timber.v("TrackSelection: Got ${parameters.overrides.size} overrides")
|
||||||
parameters.overrides.forEach { (trackGroup, trackSelectionOverride) ->
|
parameters.overrides.forEach { (trackGroup, trackSelectionOverride) ->
|
||||||
|
|
@ -350,7 +355,10 @@ class MpvPlayer(
|
||||||
}
|
}
|
||||||
Timber.v("TrackSelection: activating %s %s '%s'", propertyName, trackId, id)
|
Timber.v("TrackSelection: activating %s %s '%s'", propertyName, trackId, id)
|
||||||
if (trackId != null && propertyName != null) {
|
if (trackId != null && propertyName != null) {
|
||||||
MPVLib.setPropertyString(propertyName, trackId)
|
sendCommand(
|
||||||
|
MpvCommand.SET_TRACK_SELECTION,
|
||||||
|
TrackSelection(propertyName, trackId),
|
||||||
|
)
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
|
@ -397,24 +405,13 @@ class MpvPlayer(
|
||||||
|
|
||||||
override fun getDuration(): Long {
|
override fun getDuration(): Long {
|
||||||
if (DEBUG) Timber.v("getDuration")
|
if (DEBUG) Timber.v("getDuration")
|
||||||
if (isReleased) {
|
return playbackState.load().durationMs
|
||||||
return durationMs
|
|
||||||
}
|
|
||||||
val duration =
|
|
||||||
MPVLib.getPropertyDouble("duration/full")?.seconds?.inWholeMilliseconds
|
|
||||||
?: durationMs
|
|
||||||
return duration
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCurrentPosition(): Long {
|
override fun getCurrentPosition(): Long {
|
||||||
if (DEBUG) Timber.v("getCurrentPosition")
|
if (DEBUG) Timber.v("getCurrentPosition")
|
||||||
if (isReleased) {
|
val state = playbackState.load()
|
||||||
return positionMs
|
return state.positionMs
|
||||||
}
|
|
||||||
val position =
|
|
||||||
MPVLib.getPropertyDouble("time-pos/full")?.seconds?.inWholeMilliseconds
|
|
||||||
?: positionMs
|
|
||||||
return position
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getBufferedPosition(): Long {
|
override fun getBufferedPosition(): Long {
|
||||||
|
|
@ -425,7 +422,7 @@ class MpvPlayer(
|
||||||
override fun getTotalBufferedDuration(): Long {
|
override fun getTotalBufferedDuration(): Long {
|
||||||
if (DEBUG) Timber.v("getTotalBufferedDuration")
|
if (DEBUG) Timber.v("getTotalBufferedDuration")
|
||||||
if (isReleased) return 0
|
if (isReleased) return 0
|
||||||
return MPVLib.getPropertyDouble("demuxer-cache-duration")?.seconds?.inWholeMilliseconds ?: 0
|
return playbackState.load().bufferMs
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isPlayingAd(): Boolean {
|
override fun isPlayingAd(): Boolean {
|
||||||
|
|
@ -475,9 +472,8 @@ class MpvPlayer(
|
||||||
MPVLib.attachSurface(surface)
|
MPVLib.attachSurface(surface)
|
||||||
MPVLib.setOptionString("force-window", "yes")
|
MPVLib.setOptionString("force-window", "yes")
|
||||||
Timber.d("Attached surface")
|
Timber.d("Attached surface")
|
||||||
mediaItem?.let(::loadFile)
|
playbackState.load().media?.let {
|
||||||
if (mediaItem == null) {
|
sendCommand(MpvCommand.LOAD_FILE, it)
|
||||||
Timber.w("mediaItem is null in setVideoSurfaceView")
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
clearVideoSurfaceView(null)
|
clearVideoSurfaceView(null)
|
||||||
|
|
@ -485,11 +481,14 @@ class MpvPlayer(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun clearVideoSurfaceView(surfaceView: SurfaceView?) {
|
override fun clearVideoSurfaceView(surfaceView: SurfaceView?) {
|
||||||
Timber.d("clearVideoSurfaceView")
|
if (surface == surfaceView?.holder?.surface) {
|
||||||
MPVLib.detachSurface()
|
Timber.d("clearVideoSurfaceView")
|
||||||
MPVLib.setPropertyString("vo", "null")
|
MPVLib.detachSurface()
|
||||||
MPVLib.setPropertyString("force-window", "no")
|
MPVLib.setPropertyString("vo", "null")
|
||||||
mediaItem = null
|
MPVLib.setPropertyString("force-window", "no")
|
||||||
|
} else {
|
||||||
|
Timber.w("clearVideoSurfaceView called with different surface")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setVideoTextureView(textureView: TextureView?): Unit = throw UnsupportedOperationException()
|
override fun setVideoTextureView(textureView: TextureView?): Unit = throw UnsupportedOperationException()
|
||||||
|
|
@ -499,13 +498,7 @@ class MpvPlayer(
|
||||||
override fun getVideoSize(): VideoSize {
|
override fun getVideoSize(): VideoSize {
|
||||||
if (DEBUG) Timber.v("getVideoSize")
|
if (DEBUG) Timber.v("getVideoSize")
|
||||||
if (isReleased) return VideoSize.UNKNOWN
|
if (isReleased) return VideoSize.UNKNOWN
|
||||||
val width = MPVLib.getPropertyInt("width")
|
return playbackState.load().videoSize
|
||||||
val height = MPVLib.getPropertyInt("height")
|
|
||||||
return if (width != null && height != null) {
|
|
||||||
VideoSize(width, height)
|
|
||||||
} else {
|
|
||||||
VideoSize.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getSurfaceSize(): Size = throw UnsupportedOperationException()
|
override fun getSurfaceSize(): Size = throw UnsupportedOperationException()
|
||||||
|
|
@ -566,7 +559,11 @@ class MpvPlayer(
|
||||||
if (mediaItemIndex == C.INDEX_UNSET) {
|
if (mediaItemIndex == C.INDEX_UNSET) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
MPVLib.setPropertyDouble("time-pos", positionMs / 1000.0)
|
sendCommand(MpvCommand.SEEK, positionMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTrackSelectionsInvalidated() {
|
||||||
|
// no-op
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun eventProperty(property: String) {
|
override fun eventProperty(property: String) {
|
||||||
|
|
@ -579,7 +576,11 @@ class MpvPlayer(
|
||||||
) {
|
) {
|
||||||
if (DEBUG) Timber.v("eventPropertyLong: $property=$value")
|
if (DEBUG) Timber.v("eventPropertyLong: $property=$value")
|
||||||
when (property) {
|
when (property) {
|
||||||
MPVProperty.POSITION -> positionMs = value.seconds.inWholeMilliseconds
|
MPVProperty.POSITION -> {
|
||||||
|
playbackState.update {
|
||||||
|
it.copy(positionMs = value.seconds.inWholeMilliseconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -590,7 +591,9 @@ class MpvPlayer(
|
||||||
if (DEBUG) Timber.v("eventPropertyBoolean: $property=$value")
|
if (DEBUG) Timber.v("eventPropertyBoolean: $property=$value")
|
||||||
when (property) {
|
when (property) {
|
||||||
MPVProperty.PAUSED -> {
|
MPVProperty.PAUSED -> {
|
||||||
isPaused = value
|
playbackState.update {
|
||||||
|
it.copy(isPaused = value)
|
||||||
|
}
|
||||||
notifyListeners(EVENT_IS_PLAYING_CHANGED) { onIsPlayingChanged(!value) }
|
notifyListeners(EVENT_IS_PLAYING_CHANGED) { onIsPlayingChanged(!value) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -609,52 +612,58 @@ class MpvPlayer(
|
||||||
) {
|
) {
|
||||||
Timber.v("eventPropertyDouble: $property=$value")
|
Timber.v("eventPropertyDouble: $property=$value")
|
||||||
when (property) {
|
when (property) {
|
||||||
MPVProperty.DURATION -> durationMs = value.seconds.inWholeMilliseconds
|
MPVProperty.DURATION -> {
|
||||||
|
playbackState.update {
|
||||||
|
it.copy(durationMs = value.seconds.inWholeMilliseconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun event(eventId: Int) {
|
override fun event(eventId: Int) {
|
||||||
|
Timber.v("event: thread=${Thread.currentThread().name}, eventId=$eventId")
|
||||||
when (eventId) {
|
when (eventId) {
|
||||||
// MPV_EVENT_START_FILE -> {
|
MPV_EVENT_START_FILE -> {
|
||||||
// }
|
internalHandler.post(updatePlaybackState)
|
||||||
|
}
|
||||||
|
|
||||||
MPV_EVENT_FILE_LOADED -> {
|
MPV_EVENT_FILE_LOADED -> {
|
||||||
isLoadingFile = false
|
playbackState.update {
|
||||||
|
it.copy(isLoadingFile = false)
|
||||||
|
}
|
||||||
notifyListeners(EVENT_IS_LOADING_CHANGED) { onIsLoadingChanged(false) }
|
notifyListeners(EVENT_IS_LOADING_CHANGED) { onIsLoadingChanged(false) }
|
||||||
Timber.d("event: MPV_EVENT_FILE_LOADED")
|
Timber.d("event: MPV_EVENT_FILE_LOADED")
|
||||||
mediaItem?.let {
|
internalHandler.post(updatePlaybackState)
|
||||||
it.localConfiguration?.subtitleConfigurations?.forEach {
|
playbackState.load().media?.mediaItem?.let { media ->
|
||||||
|
media.localConfiguration?.subtitleConfigurations?.forEach {
|
||||||
val url = it.uri.toString()
|
val url = it.uri.toString()
|
||||||
val title = it.label ?: "External Subtitles"
|
val title = it.label ?: "External Subtitles"
|
||||||
Timber.v("Adding external subtitle track '$title'")
|
Timber.v("Adding external subtitle track '$title'")
|
||||||
MPVLib.command(arrayOf("sub-add", url, "auto", title))
|
if (it.language.isNotNullOrBlank()) {
|
||||||
|
MPVLib.command(arrayOf("sub-add", url, "auto", title, it.language!!))
|
||||||
|
} else {
|
||||||
|
MPVLib.command(arrayOf("sub-add", url, "auto", title))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
notifyListeners(EVENT_RENDERED_FIRST_FRAME) { onRenderedFirstFrame() }
|
notifyListeners(EVENT_RENDERED_FIRST_FRAME) { onRenderedFirstFrame() }
|
||||||
notifyListeners(EVENT_IS_PLAYING_CHANGED) { onIsPlayingChanged(true) }
|
notifyListeners(EVENT_IS_PLAYING_CHANGED) { onIsPlayingChanged(true) }
|
||||||
getTracks().let {
|
updateTracksAndNotify()
|
||||||
notifyListeners(EVENT_TRACKS_CHANGED) { onTracksChanged(it) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MPV_EVENT_PLAYBACK_RESTART -> {
|
MPV_EVENT_PLAYBACK_RESTART -> {
|
||||||
Timber.d("event: MPV_EVENT_PLAYBACK_RESTART")
|
Timber.d("event: MPV_EVENT_PLAYBACK_RESTART")
|
||||||
getTracks().let {
|
updateTracksAndNotify()
|
||||||
notifyListeners(EVENT_TRACKS_CHANGED) { onTracksChanged(it) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MPV_EVENT_AUDIO_RECONFIG -> {
|
MPV_EVENT_AUDIO_RECONFIG -> {
|
||||||
Timber.d("event: MPV_EVENT_AUDIO_RECONFIG")
|
Timber.d("event: MPV_EVENT_AUDIO_RECONFIG")
|
||||||
getTracks().let {
|
updateTracksAndNotify()
|
||||||
notifyListeners(EVENT_TRACKS_CHANGED) { onTracksChanged(it) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MPV_EVENT_VIDEO_RECONFIG -> {
|
MPV_EVENT_VIDEO_RECONFIG -> {
|
||||||
Timber.d("event: MPV_EVENT_VIDEO_RECONFIG")
|
Timber.d("event: MPV_EVENT_VIDEO_RECONFIG")
|
||||||
getTracks().let {
|
updateTracksAndNotify()
|
||||||
notifyListeners(EVENT_TRACKS_CHANGED) { onTracksChanged(it) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MPV_EVENT_END_FILE -> {
|
MPV_EVENT_END_FILE -> {
|
||||||
|
|
@ -676,6 +685,9 @@ class MpvPlayer(
|
||||||
notifyListeners(EVENT_IS_PLAYING_CHANGED) { onIsPlayingChanged(false) }
|
notifyListeners(EVENT_IS_PLAYING_CHANGED) { onIsPlayingChanged(false) }
|
||||||
when (reason) {
|
when (reason) {
|
||||||
MPV_END_FILE_REASON_EOF -> {
|
MPV_END_FILE_REASON_EOF -> {
|
||||||
|
playbackState.update {
|
||||||
|
it.copy(state = Player.STATE_ENDED)
|
||||||
|
}
|
||||||
notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) {
|
notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) {
|
||||||
onPlaybackStateChanged(STATE_ENDED)
|
onPlaybackStateChanged(STATE_ENDED)
|
||||||
}
|
}
|
||||||
|
|
@ -704,25 +716,44 @@ class MpvPlayer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadFile(mediaItem: MediaItem) {
|
private fun updateTracksAndNotify() {
|
||||||
isLoadingFile = true
|
val tracks = createTracks()
|
||||||
|
playbackState.update {
|
||||||
|
it.copy(tracks = tracks)
|
||||||
|
}
|
||||||
|
notifyListeners(EVENT_TRACKS_CHANGED) { onTracksChanged(tracks) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadFile(media: MediaAndPosition) {
|
||||||
|
Timber.v("loadFile: media=$media")
|
||||||
|
playbackState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoadingFile = true,
|
||||||
|
media = media,
|
||||||
|
)
|
||||||
|
}
|
||||||
notifyListeners(EVENT_IS_LOADING_CHANGED) { onIsLoadingChanged(true) }
|
notifyListeners(EVENT_IS_LOADING_CHANGED) { onIsLoadingChanged(true) }
|
||||||
val url = mediaItem.localConfiguration?.uri.toString()
|
val url =
|
||||||
if (startPositionMs > 0) {
|
media.mediaItem.localConfiguration
|
||||||
|
?.uri
|
||||||
|
.toString()
|
||||||
|
if (media.startPositionMs > 0) {
|
||||||
MPVLib.command(
|
MPVLib.command(
|
||||||
arrayOf(
|
arrayOf(
|
||||||
"loadfile",
|
"loadfile",
|
||||||
url,
|
url,
|
||||||
"replace",
|
"replace",
|
||||||
"-1",
|
"-1",
|
||||||
"start=${startPositionMs / 1000.0}",
|
"start=${media.startPositionMs / 1000.0}",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
MPVLib.command(arrayOf("loadfile", url, "replace", "-1"))
|
MPVLib.command(arrayOf("loadfile", url, "replace", "-1"))
|
||||||
}
|
}
|
||||||
|
|
||||||
MPVLib.setPropertyString("vo", "gpu")
|
if (enableHardwareDecoding) {
|
||||||
|
MPVLib.setOptionString("vo", if (useGpuNext) "gpu-next" else "gpu")
|
||||||
|
}
|
||||||
Timber.d("Called loadfile")
|
Timber.d("Called loadfile")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -736,7 +767,7 @@ class MpvPlayer(
|
||||||
eventId: Int,
|
eventId: Int,
|
||||||
block: Player.Listener.() -> Unit,
|
block: Player.Listener.() -> Unit,
|
||||||
) {
|
) {
|
||||||
handler.post {
|
mainHandler.post {
|
||||||
listeners.queueEvent(eventId) {
|
listeners.queueEvent(eventId) {
|
||||||
block.invoke(it)
|
block.invoke(it)
|
||||||
}
|
}
|
||||||
|
|
@ -744,78 +775,14 @@ class MpvPlayer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getTracks(): Tracks {
|
|
||||||
val trackCount = MPVLib.getPropertyInt("track-list/count") ?: return Tracks.EMPTY
|
|
||||||
val groups =
|
|
||||||
(0..<trackCount).mapNotNull { idx ->
|
|
||||||
val type = MPVLib.getPropertyString("track-list/$idx/type")
|
|
||||||
val id = MPVLib.getPropertyInt("track-list/$idx/id")
|
|
||||||
val lang = MPVLib.getPropertyString("track-list/$idx/lang")
|
|
||||||
val codec = MPVLib.getPropertyString("track-list/$idx/codec")
|
|
||||||
val codecDescription = MPVLib.getPropertyString("track-list/$idx/codec-desc")
|
|
||||||
val isDefault = MPVLib.getPropertyBoolean("track-list/$idx/default") ?: false
|
|
||||||
val isForced = MPVLib.getPropertyBoolean("track-list/$idx/forced") ?: false
|
|
||||||
val isExternal = MPVLib.getPropertyBoolean("track-list/$idx/external") ?: false
|
|
||||||
val isSelected = MPVLib.getPropertyBoolean("track-list/$idx/selected") ?: false
|
|
||||||
val channelCount = MPVLib.getPropertyInt("track-list/$idx/demux-channel-count")
|
|
||||||
val title = MPVLib.getPropertyString("track-list/$idx/title")
|
|
||||||
|
|
||||||
if (type != null && id != null) {
|
|
||||||
// TODO do we need the real mimetypes?
|
|
||||||
val mimeType =
|
|
||||||
when (type) {
|
|
||||||
"video" -> MimeTypes.BASE_TYPE_VIDEO + "/todo"
|
|
||||||
"audio" -> MimeTypes.BASE_TYPE_AUDIO + "/todo"
|
|
||||||
"sub" -> MimeTypes.BASE_TYPE_TEXT + "/todo"
|
|
||||||
else -> "unknown/todo"
|
|
||||||
}
|
|
||||||
var flags = 0
|
|
||||||
if (isDefault) flags = flags or C.SELECTION_FLAG_DEFAULT
|
|
||||||
if (isForced) flags = flags or C.SELECTION_FLAG_FORCED
|
|
||||||
val builder =
|
|
||||||
Format
|
|
||||||
.Builder()
|
|
||||||
.setId("$idx:$id")
|
|
||||||
.setCodecs(codec)
|
|
||||||
.setSampleMimeType(mimeType)
|
|
||||||
.setLanguage(lang)
|
|
||||||
.setLabel(listOfNotNull(title, codecDescription).joinToString(","))
|
|
||||||
.setSelectionFlags(flags)
|
|
||||||
if (type == "video" && isSelected) {
|
|
||||||
builder.setWidth(MPVLib.getPropertyInt("width") ?: -1)
|
|
||||||
builder.setHeight(MPVLib.getPropertyInt("height") ?: -1)
|
|
||||||
}
|
|
||||||
channelCount?.let(builder::setChannelCount)
|
|
||||||
val format = builder.build()
|
|
||||||
|
|
||||||
val trackGroup = TrackGroup(format)
|
|
||||||
val group =
|
|
||||||
Tracks.Group(
|
|
||||||
trackGroup,
|
|
||||||
false,
|
|
||||||
intArrayOf(C.FORMAT_HANDLED),
|
|
||||||
booleanArrayOf(isSelected),
|
|
||||||
)
|
|
||||||
group
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Tracks(groups)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTrackSelectionsInvalidated() {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
|
|
||||||
var subtitleDelaySeconds: Double
|
var subtitleDelaySeconds: Double
|
||||||
get() {
|
get() {
|
||||||
if (isReleased) return 0.0
|
if (isReleased) return 0.0
|
||||||
return MPVLib.getPropertyDouble("sub-delay") ?: 0.0
|
return playbackState.load().subtitleDelay
|
||||||
}
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
if (isReleased) return
|
if (isReleased) return
|
||||||
MPVLib.setPropertyDouble("sub-delay", value)
|
sendCommand(MpvCommand.SET_SUBTITLE_DELAY, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var subtitleDelay: Duration
|
var subtitleDelay: Duration
|
||||||
|
|
@ -827,11 +794,230 @@ class MpvPlayer(
|
||||||
if (isReleased) return
|
if (isReleased) return
|
||||||
subtitleDelaySeconds = value.inWholeMilliseconds / 1000.0
|
subtitleDelaySeconds = value.inWholeMilliseconds / 1000.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val updatePlaybackState: Runnable =
|
||||||
|
Runnable {
|
||||||
|
if (playbackState.load().media == null) {
|
||||||
|
return@Runnable
|
||||||
|
}
|
||||||
|
val positionMs =
|
||||||
|
MPVLib.getPropertyDouble("time-pos/full")?.seconds?.inWholeMilliseconds
|
||||||
|
?: C.TIME_UNSET
|
||||||
|
val bufferMs =
|
||||||
|
MPVLib.getPropertyDouble("demuxer-cache-duration")?.seconds?.inWholeMilliseconds
|
||||||
|
?: C.TIME_UNSET
|
||||||
|
val durationMs =
|
||||||
|
MPVLib.getPropertyDouble("duration/full")?.seconds?.inWholeMilliseconds
|
||||||
|
?: C.TIME_UNSET
|
||||||
|
val speed = MPVLib.getPropertyDouble("speed")?.toFloat() ?: 1f
|
||||||
|
val paused = MPVLib.getPropertyBoolean("pause") ?: false
|
||||||
|
val width = MPVLib.getPropertyInt("width")
|
||||||
|
val height = MPVLib.getPropertyInt("height")
|
||||||
|
val videoSize =
|
||||||
|
if (width != null && height != null) {
|
||||||
|
VideoSize(width, height)
|
||||||
|
} else {
|
||||||
|
VideoSize.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
playbackState.update {
|
||||||
|
it.copy(
|
||||||
|
timestamp = System.currentTimeMillis(),
|
||||||
|
positionMs = positionMs,
|
||||||
|
bufferMs = bufferMs,
|
||||||
|
durationMs = durationMs,
|
||||||
|
speed = speed,
|
||||||
|
isPaused = paused,
|
||||||
|
videoSize = videoSize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendCommand(
|
||||||
|
cmd: MpvCommand,
|
||||||
|
obj: Any?,
|
||||||
|
) {
|
||||||
|
internalHandler.obtainMessage(cmd.ordinal, obj).sendToTarget()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleMessage(msg: Message): Boolean {
|
||||||
|
val cmd = MpvCommand.entries[msg.what]
|
||||||
|
Timber.v("handleMessage: cmd=$cmd")
|
||||||
|
when (cmd) {
|
||||||
|
MpvCommand.PLAY_PAUSE -> {
|
||||||
|
val playWhenReady = msg.obj as Boolean
|
||||||
|
MPVLib.setPropertyBoolean("pause", !playWhenReady)
|
||||||
|
playbackState.update {
|
||||||
|
it.copy(isPaused = !playWhenReady)
|
||||||
|
}
|
||||||
|
notifyListeners(EVENT_PLAY_WHEN_READY_CHANGED) {
|
||||||
|
onPlayWhenReadyChanged(
|
||||||
|
playWhenReady,
|
||||||
|
PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MpvCommand.SET_TRACK_SELECTION -> {
|
||||||
|
val (propertyName, trackId) = msg.obj as TrackSelection
|
||||||
|
MPVLib.setPropertyString(propertyName, trackId)
|
||||||
|
updateTracksAndNotify()
|
||||||
|
}
|
||||||
|
|
||||||
|
MpvCommand.SEEK -> {
|
||||||
|
val positionMs = msg.obj as Long
|
||||||
|
MPVLib.setPropertyDouble("time-pos", positionMs / 1000.0)
|
||||||
|
playbackState.update {
|
||||||
|
it.copy(positionMs = positionMs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MpvCommand.SET_SPEED -> {
|
||||||
|
val value = msg.obj as Float
|
||||||
|
MPVLib.setPropertyDouble("speed", value.toDouble())
|
||||||
|
playbackState.update {
|
||||||
|
it.copy(speed = value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MpvCommand.SET_SUBTITLE_DELAY -> {
|
||||||
|
val value = msg.obj as Double
|
||||||
|
MPVLib.setPropertyDouble("sub-delay", value)
|
||||||
|
playbackState.update {
|
||||||
|
it.copy(subtitleDelay = value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MpvCommand.LOAD_FILE -> {
|
||||||
|
loadFile(msg.obj as MediaAndPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MPVLib.setPropertyColor(
|
fun MPVLib.setPropertyColor(
|
||||||
property: String,
|
property: String,
|
||||||
color: Color,
|
color: Color,
|
||||||
) = MPVLib.setPropertyString(property, color.mpvFormat)
|
) = setPropertyString(property, color.mpvFormat)
|
||||||
|
|
||||||
private val Color.mpvFormat: String get() = "$red/$green/$blue/$alpha"
|
private val Color.mpvFormat: String get() = "$red/$green/$blue/$alpha"
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
private fun createTracks(): Tracks {
|
||||||
|
val trackCount = MPVLib.getPropertyInt("track-list/count") ?: return Tracks.EMPTY
|
||||||
|
val groups =
|
||||||
|
(0..<trackCount).mapNotNull { idx ->
|
||||||
|
val type = MPVLib.getPropertyString("track-list/$idx/type")
|
||||||
|
val id = MPVLib.getPropertyInt("track-list/$idx/id")
|
||||||
|
val lang = MPVLib.getPropertyString("track-list/$idx/lang")
|
||||||
|
val codec = MPVLib.getPropertyString("track-list/$idx/codec")
|
||||||
|
val codecDescription = MPVLib.getPropertyString("track-list/$idx/codec-desc")
|
||||||
|
val isDefault = MPVLib.getPropertyBoolean("track-list/$idx/default") ?: false
|
||||||
|
val isForced = MPVLib.getPropertyBoolean("track-list/$idx/forced") ?: false
|
||||||
|
val isExternal = MPVLib.getPropertyBoolean("track-list/$idx/external") ?: false
|
||||||
|
val isSelected = MPVLib.getPropertyBoolean("track-list/$idx/selected") ?: false
|
||||||
|
val channelCount = MPVLib.getPropertyInt("track-list/$idx/demux-channel-count")
|
||||||
|
val title = MPVLib.getPropertyString("track-list/$idx/title")
|
||||||
|
|
||||||
|
if (type != null && id != null) {
|
||||||
|
// TODO do we need the real mimetypes?
|
||||||
|
val mimeType =
|
||||||
|
when (type) {
|
||||||
|
"video" -> MimeTypes.BASE_TYPE_VIDEO + "/todo"
|
||||||
|
"audio" -> MimeTypes.BASE_TYPE_AUDIO + "/todo"
|
||||||
|
"sub" -> MimeTypes.BASE_TYPE_TEXT + "/todo"
|
||||||
|
else -> "unknown/todo"
|
||||||
|
}
|
||||||
|
var flags = 0
|
||||||
|
if (isDefault) flags = flags or C.SELECTION_FLAG_DEFAULT
|
||||||
|
if (isForced) flags = flags or C.SELECTION_FLAG_FORCED
|
||||||
|
val builder =
|
||||||
|
Format
|
||||||
|
.Builder()
|
||||||
|
.apply {
|
||||||
|
if (isExternal) {
|
||||||
|
setId("$idx:e:$id")
|
||||||
|
} else {
|
||||||
|
setId("$idx:$id")
|
||||||
|
}
|
||||||
|
}.setCodecs(codec)
|
||||||
|
.setSampleMimeType(mimeType)
|
||||||
|
.setLanguage(lang)
|
||||||
|
.setLabel(listOfNotNull(title, codecDescription).joinToString(","))
|
||||||
|
.setSelectionFlags(flags)
|
||||||
|
if (type == "video" && isSelected) {
|
||||||
|
builder.setWidth(MPVLib.getPropertyInt("width") ?: -1)
|
||||||
|
builder.setHeight(MPVLib.getPropertyInt("height") ?: -1)
|
||||||
|
}
|
||||||
|
channelCount?.let(builder::setChannelCount)
|
||||||
|
val format = builder.build()
|
||||||
|
// Timber.v("$idx=$format")
|
||||||
|
|
||||||
|
val trackGroup = TrackGroup(format)
|
||||||
|
val group =
|
||||||
|
Tracks.Group(
|
||||||
|
trackGroup,
|
||||||
|
false,
|
||||||
|
intArrayOf(C.FORMAT_HANDLED),
|
||||||
|
booleanArrayOf(isSelected),
|
||||||
|
)
|
||||||
|
group
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Tracks(groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class PlaybackState(
|
||||||
|
val timestamp: Long,
|
||||||
|
val isLoadingFile: Boolean,
|
||||||
|
val media: MediaAndPosition?,
|
||||||
|
val positionMs: Long,
|
||||||
|
val bufferMs: Long,
|
||||||
|
val durationMs: Long,
|
||||||
|
val isPaused: Boolean,
|
||||||
|
val speed: Float,
|
||||||
|
val subtitleDelay: Double,
|
||||||
|
val videoSize: VideoSize,
|
||||||
|
@param:Player.State val state: Int,
|
||||||
|
val tracks: Tracks,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
val EMPTY =
|
||||||
|
PlaybackState(
|
||||||
|
timestamp = C.TIME_UNSET,
|
||||||
|
isLoadingFile = false,
|
||||||
|
media = null,
|
||||||
|
positionMs = C.TIME_UNSET,
|
||||||
|
durationMs = C.TIME_UNSET,
|
||||||
|
tracks = Tracks.EMPTY,
|
||||||
|
bufferMs = C.TIME_UNSET,
|
||||||
|
isPaused = false,
|
||||||
|
speed = 1f,
|
||||||
|
videoSize = VideoSize.UNKNOWN,
|
||||||
|
state = Player.STATE_IDLE,
|
||||||
|
subtitleDelay = 0.0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class TrackSelection(
|
||||||
|
val property: String,
|
||||||
|
val trackId: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class MediaAndPosition(
|
||||||
|
val mediaItem: MediaItem,
|
||||||
|
val startPositionMs: Long,
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class MpvCommand {
|
||||||
|
PLAY_PAUSE,
|
||||||
|
SEEK,
|
||||||
|
SET_TRACK_SELECTION,
|
||||||
|
SET_SPEED,
|
||||||
|
SET_SUBTITLE_DELAY,
|
||||||
|
LOAD_FILE,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
#include <android/log.h>
|
#include <android/log.h>
|
||||||
|
|
||||||
#define DEBUG 1
|
#define DEBUG 0
|
||||||
|
|
||||||
#define LOG_TAG "mpv"
|
#define LOG_TAG "mpv"
|
||||||
#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,16 @@ This scripts are adapted from https://github.com/mpv-android/mpv-android/tree/ae
|
||||||
|
|
||||||
## Instructions
|
## Instructions
|
||||||
|
|
||||||
TODO
|
```bash
|
||||||
|
cd scripts/mpv
|
||||||
|
./get_dependencies
|
||||||
|
|
||||||
|
export NDK_PATH=... # Such as ~/Library/Android/sdk/ndk/29.0.14206865
|
||||||
|
# Build arm64
|
||||||
|
PATH="$PATH:$NDK_PATH/toolchains/llvm/prebuilt/darwin-x86_64/bin" ./buildall.sh --clean --arch arm64 mpv
|
||||||
|
# Build arm32
|
||||||
|
PATH="$PATH:$NDK_PATH/toolchains/llvm/prebuilt/darwin-x86_64/bin" ./buildall.sh mpv
|
||||||
|
|
||||||
|
cd ../..
|
||||||
|
env PREFIX32="$(realpath scripts/mpv/prefix/armv7l)" PREFIX64="$(realpath scripts/mpv/prefix/arm64)" "$NDK_PATH/ndk-build" -C app/src/main -j && cp -fr app/src/main/libs/ app/src/main/jnilibs/
|
||||||
|
```
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue