mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 16:11:20 +02:00
Add experimental MPV player backend (#161)
Experimental MPV player backend Related to #14, #22, & #85 You can switch to MPV in advanced settings and toggle using hardware decoding or not. This uses code and buildscripts from https://github.com/mpv-android/mpv-android, plus other third party libraries to build MPV
This commit is contained in:
parent
d7b333e519
commit
6be2662d4e
84 changed files with 3987 additions and 289 deletions
|
|
@ -0,0 +1,334 @@
|
|||
package com.github.damontecres.wholphin.util.mpv
|
||||
|
||||
/*
|
||||
This file is copied from https://github.com/mpv-android/mpv-android/blob/master/app/src/main/java/is/xyz/mpv/MPVLib.kt
|
||||
|
||||
Copyright (c) 2016 Ilya Zhuravlev
|
||||
Copyright (c) 2016 sfan5 <sfan5@live.de>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
||||
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or
|
||||
substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
||||
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.view.Surface
|
||||
|
||||
// Wrapper for native library
|
||||
|
||||
@Suppress("unused")
|
||||
object MPVLib {
|
||||
init {
|
||||
val libs = arrayOf("mpv", "player")
|
||||
for (lib in libs) {
|
||||
System.loadLibrary(lib)
|
||||
}
|
||||
}
|
||||
|
||||
external fun create(appctx: Context)
|
||||
|
||||
external fun init()
|
||||
|
||||
external fun destroy()
|
||||
|
||||
external fun attachSurface(surface: Surface)
|
||||
|
||||
external fun detachSurface()
|
||||
|
||||
external fun command(cmd: Array<out String>)
|
||||
|
||||
external fun setOptionString(
|
||||
name: String,
|
||||
value: String,
|
||||
): Int
|
||||
|
||||
external fun grabThumbnail(dimension: Int): Bitmap?
|
||||
|
||||
external fun getPropertyInt(property: String): Int?
|
||||
|
||||
external fun setPropertyInt(
|
||||
property: String,
|
||||
value: Int,
|
||||
)
|
||||
|
||||
external fun getPropertyDouble(property: String): Double?
|
||||
|
||||
external fun setPropertyDouble(
|
||||
property: String,
|
||||
value: Double,
|
||||
)
|
||||
|
||||
external fun getPropertyBoolean(property: String): Boolean?
|
||||
|
||||
external fun setPropertyBoolean(
|
||||
property: String,
|
||||
value: Boolean,
|
||||
)
|
||||
|
||||
external fun getPropertyString(property: String): String?
|
||||
|
||||
external fun setPropertyString(
|
||||
property: String,
|
||||
value: String,
|
||||
)
|
||||
|
||||
external fun observeProperty(
|
||||
property: String,
|
||||
format: Int,
|
||||
)
|
||||
|
||||
private val observers = mutableListOf<EventObserver>()
|
||||
|
||||
@JvmStatic
|
||||
fun addObserver(o: EventObserver) {
|
||||
synchronized(observers) {
|
||||
observers.add(o)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun removeObserver(o: EventObserver) {
|
||||
synchronized(observers) {
|
||||
observers.remove(o)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun eventProperty(
|
||||
property: String,
|
||||
value: Long,
|
||||
) {
|
||||
synchronized(observers) {
|
||||
for (o in observers) {
|
||||
o.eventProperty(property, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun eventProperty(
|
||||
property: String,
|
||||
value: Boolean,
|
||||
) {
|
||||
synchronized(observers) {
|
||||
for (o in observers) {
|
||||
o.eventProperty(property, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun eventProperty(
|
||||
property: String,
|
||||
value: Double,
|
||||
) {
|
||||
synchronized(observers) {
|
||||
for (o in observers) {
|
||||
o.eventProperty(property, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun eventProperty(
|
||||
property: String,
|
||||
value: String,
|
||||
) {
|
||||
synchronized(observers) {
|
||||
for (o in observers) {
|
||||
o.eventProperty(property, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun eventProperty(property: String) {
|
||||
synchronized(observers) {
|
||||
for (o in observers) {
|
||||
o.eventProperty(property)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun event(eventId: Int) {
|
||||
synchronized(observers) {
|
||||
for (o in observers) {
|
||||
o.event(eventId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun eventEndFile(
|
||||
reason: Int,
|
||||
error: Int,
|
||||
) {
|
||||
synchronized(observers) {
|
||||
for (o in observers) {
|
||||
o.eventEndFile(reason, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val log_observers = mutableListOf<LogObserver>()
|
||||
|
||||
@JvmStatic
|
||||
fun addLogObserver(o: LogObserver) {
|
||||
synchronized(log_observers) {
|
||||
log_observers.add(o)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun removeLogObserver(o: LogObserver) {
|
||||
synchronized(log_observers) {
|
||||
log_observers.remove(o)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun logMessage(
|
||||
prefix: String,
|
||||
level: Int,
|
||||
text: String,
|
||||
) {
|
||||
synchronized(log_observers) {
|
||||
for (o in log_observers) {
|
||||
o.logMessage(prefix, level, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface EventObserver {
|
||||
fun eventProperty(property: String)
|
||||
|
||||
fun eventProperty(
|
||||
property: String,
|
||||
value: Long,
|
||||
)
|
||||
|
||||
fun eventProperty(
|
||||
property: String,
|
||||
value: Boolean,
|
||||
)
|
||||
|
||||
fun eventProperty(
|
||||
property: String,
|
||||
value: String,
|
||||
)
|
||||
|
||||
fun eventProperty(
|
||||
property: String,
|
||||
value: Double,
|
||||
)
|
||||
|
||||
fun event(eventId: Int)
|
||||
|
||||
fun eventEndFile(
|
||||
reason: Int,
|
||||
error: Int,
|
||||
)
|
||||
}
|
||||
|
||||
interface LogObserver {
|
||||
fun logMessage(
|
||||
prefix: String,
|
||||
level: Int,
|
||||
text: String,
|
||||
)
|
||||
}
|
||||
|
||||
object MpvFormat {
|
||||
const val MPV_FORMAT_NONE: Int = 0
|
||||
const val MPV_FORMAT_STRING: Int = 1
|
||||
const val MPV_FORMAT_OSD_STRING: Int = 2
|
||||
const val MPV_FORMAT_FLAG: Int = 3
|
||||
const val MPV_FORMAT_INT64: Int = 4
|
||||
const val MPV_FORMAT_DOUBLE: Int = 5
|
||||
const val MPV_FORMAT_NODE: Int = 6
|
||||
const val MPV_FORMAT_NODE_ARRAY: Int = 7
|
||||
const val MPV_FORMAT_NODE_MAP: Int = 8
|
||||
const val MPV_FORMAT_BYTE_ARRAY: Int = 9
|
||||
}
|
||||
|
||||
object MpvEvent {
|
||||
const val MPV_EVENT_NONE: Int = 0
|
||||
const val MPV_EVENT_SHUTDOWN: Int = 1
|
||||
const val MPV_EVENT_LOG_MESSAGE: Int = 2
|
||||
const val MPV_EVENT_GET_PROPERTY_REPLY: Int = 3
|
||||
const val MPV_EVENT_SET_PROPERTY_REPLY: Int = 4
|
||||
const val MPV_EVENT_COMMAND_REPLY: Int = 5
|
||||
const val MPV_EVENT_START_FILE: Int = 6
|
||||
const val MPV_EVENT_END_FILE: Int = 7
|
||||
const val MPV_EVENT_FILE_LOADED: Int = 8
|
||||
|
||||
@Deprecated("")
|
||||
const val MPV_EVENT_IDLE: Int = 11
|
||||
|
||||
@Deprecated("")
|
||||
const val MPV_EVENT_TICK: Int = 14
|
||||
const val MPV_EVENT_CLIENT_MESSAGE: Int = 16
|
||||
const val MPV_EVENT_VIDEO_RECONFIG: Int = 17
|
||||
const val MPV_EVENT_AUDIO_RECONFIG: Int = 18
|
||||
const val MPV_EVENT_SEEK: Int = 20
|
||||
const val MPV_EVENT_PLAYBACK_RESTART: Int = 21
|
||||
const val MPV_EVENT_PROPERTY_CHANGE: Int = 22
|
||||
const val MPV_EVENT_QUEUE_OVERFLOW: Int = 24
|
||||
const val MPV_EVENT_HOOK: Int = 25
|
||||
}
|
||||
|
||||
object MpvLogLevel {
|
||||
const val MPV_LOG_LEVEL_NONE: Int = 0
|
||||
const val MPV_LOG_LEVEL_FATAL: Int = 10
|
||||
const val MPV_LOG_LEVEL_ERROR: Int = 20
|
||||
const val MPV_LOG_LEVEL_WARN: Int = 30
|
||||
const val MPV_LOG_LEVEL_INFO: Int = 40
|
||||
const val MPV_LOG_LEVEL_V: Int = 50
|
||||
const val MPV_LOG_LEVEL_DEBUG: Int = 60
|
||||
const val MPV_LOG_LEVEL_TRACE: Int = 70
|
||||
}
|
||||
|
||||
object MpvEndFileReason {
|
||||
const val MPV_END_FILE_REASON_EOF: Int = 0
|
||||
const val MPV_END_FILE_REASON_STOP: Int = 2
|
||||
const val MPV_END_FILE_REASON_QUIT: Int = 3
|
||||
const val MPV_END_FILE_REASON_ERROR: Int = 4
|
||||
const val MPV_END_FILE_REASON_REDIRECT: Int = 5
|
||||
}
|
||||
|
||||
object MpvError {
|
||||
const val MPV_ERROR_SUCCESS = 0
|
||||
const val MPV_ERROR_EVENT_QUEUE_FULL = -1
|
||||
const val MPV_ERROR_NOMEM = -2
|
||||
const val MPV_ERROR_UNINITIALIZED = -3
|
||||
const val MPV_ERROR_INVALID_PARAMETER = -4
|
||||
const val MPV_ERROR_OPTION_NOT_FOUND = -5
|
||||
const val MPV_ERROR_OPTION_FORMAT = -6
|
||||
const val MPV_ERROR_OPTION_ERROR = -7
|
||||
const val MPV_ERROR_PROPERTY_NOT_FOUND = -8
|
||||
const val MPV_ERROR_PROPERTY_FORMAT = -9
|
||||
const val MPV_ERROR_PROPERTY_UNAVAILABLE = -10
|
||||
const val MPV_ERROR_PROPERTY_ERROR = -11
|
||||
const val MPV_ERROR_COMMAND = -12
|
||||
const val MPV_ERROR_LOADING_FAILED = -13
|
||||
const val MPV_ERROR_AO_INIT_FAILED = -14
|
||||
const val MPV_ERROR_VO_INIT_FAILED = -15
|
||||
const val MPV_ERROR_NOTHING_TO_PLAY = -16
|
||||
const val MPV_ERROR_UNKNOWN_FORMAT = -17
|
||||
const val MPV_ERROR_UNSUPPORTED = -18
|
||||
const val MPV_ERROR_NOT_IMPLEMENTED = -19
|
||||
const val MPV_ERROR_GENERIC = -20
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.github.damontecres.wholphin.util.mpv
|
||||
|
||||
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvFormat.MPV_FORMAT_DOUBLE
|
||||
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvFormat.MPV_FORMAT_FLAG
|
||||
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvFormat.MPV_FORMAT_INT64
|
||||
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvFormat.MPV_FORMAT_NONE
|
||||
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvFormat.MPV_FORMAT_STRING
|
||||
|
||||
object MPVProperty {
|
||||
const val POSITION = "time-pos"
|
||||
const val POSITION_FULL = "time-pos/full"
|
||||
const val DURATION = "duration/full"
|
||||
const val PAUSED = "pause"
|
||||
const val PAUSED_FOR_CACHE = "paused-for-cache"
|
||||
const val SPEED = "speed"
|
||||
const val TRACK_LIST = "track-list"
|
||||
const val ASPECT = "video-params/aspect"
|
||||
const val ROTATE = "video-params/rotate"
|
||||
const val TRACK_VIDEO = "current-tracks/video/image"
|
||||
const val METADATA = "metadata"
|
||||
const val HWDEC = "hwdec-current"
|
||||
const val MUTE = "mute"
|
||||
const val TRACK_AUDIO = "current-tracks/audio/selected"
|
||||
const val SEEKABLE = "seekable"
|
||||
const val SUBTITLE_TEXT = "sub-text"
|
||||
|
||||
val observedProperties =
|
||||
mapOf(
|
||||
POSITION to MPV_FORMAT_INT64,
|
||||
// POSITION_FULL to MPV_FORMAT_DOUBLE,
|
||||
DURATION to MPV_FORMAT_DOUBLE,
|
||||
PAUSED to MPV_FORMAT_FLAG,
|
||||
PAUSED_FOR_CACHE to MPV_FORMAT_FLAG,
|
||||
SPEED to MPV_FORMAT_STRING,
|
||||
TRACK_LIST to MPV_FORMAT_NONE,
|
||||
ASPECT to MPV_FORMAT_DOUBLE,
|
||||
ROTATE to MPV_FORMAT_DOUBLE,
|
||||
TRACK_VIDEO to MPV_FORMAT_NONE,
|
||||
METADATA to MPV_FORMAT_NONE,
|
||||
HWDEC to MPV_FORMAT_NONE,
|
||||
MUTE to MPV_FORMAT_FLAG,
|
||||
TRACK_AUDIO to MPV_FORMAT_NONE,
|
||||
SEEKABLE to MPV_FORMAT_FLAG,
|
||||
SUBTITLE_TEXT to MPV_FORMAT_STRING,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package com.github.damontecres.wholphin.util.mpv
|
||||
|
||||
import com.github.damontecres.wholphin.util.profile.Codec
|
||||
import com.github.damontecres.wholphin.util.profile.subtitleProfile
|
||||
import com.github.damontecres.wholphin.util.profile.supportedAudioCodecs
|
||||
import org.jellyfin.sdk.model.api.DlnaProfileType
|
||||
import org.jellyfin.sdk.model.api.EncodingContext
|
||||
import org.jellyfin.sdk.model.api.MediaStreamProtocol
|
||||
import org.jellyfin.sdk.model.deviceprofile.buildDeviceProfile
|
||||
|
||||
val mpvDeviceProfile =
|
||||
buildDeviceProfile {
|
||||
name = "mpv"
|
||||
|
||||
// TODO
|
||||
// maxStaticBitrate = maxBitrate
|
||||
// maxStreamingBitrate = maxBitrate
|
||||
transcodingProfile {
|
||||
type = DlnaProfileType.VIDEO
|
||||
context = EncodingContext.STREAMING
|
||||
|
||||
container = Codec.Container.TS
|
||||
protocol = MediaStreamProtocol.HLS
|
||||
|
||||
videoCodec(Codec.Video.HEVC)
|
||||
videoCodec(Codec.Video.H264)
|
||||
|
||||
audioCodec(*supportedAudioCodecs)
|
||||
|
||||
copyTimestamps = false
|
||||
enableSubtitlesInManifest = true
|
||||
}
|
||||
directPlayProfile {
|
||||
type = DlnaProfileType.VIDEO
|
||||
|
||||
container(
|
||||
Codec.Container.ASF,
|
||||
Codec.Container.AVI,
|
||||
Codec.Container.HLS,
|
||||
Codec.Container.M4V,
|
||||
Codec.Container.MPEG,
|
||||
Codec.Container.MPEGTS,
|
||||
Codec.Container.MKV,
|
||||
Codec.Container.MOV,
|
||||
Codec.Container.MP4,
|
||||
Codec.Container.MPG,
|
||||
Codec.Container.OGM,
|
||||
Codec.Container.OGV,
|
||||
Codec.Container.TS,
|
||||
Codec.Container.VOB,
|
||||
Codec.Container.WEBM,
|
||||
Codec.Container.WMV,
|
||||
Codec.Container.XVID,
|
||||
)
|
||||
|
||||
videoCodec(
|
||||
Codec.Video.AV1,
|
||||
Codec.Video.H264,
|
||||
Codec.Video.HEVC,
|
||||
Codec.Video.MPEG,
|
||||
Codec.Video.MPEG2VIDEO,
|
||||
Codec.Video.VP8,
|
||||
Codec.Video.VP9,
|
||||
)
|
||||
|
||||
audioCodec(*supportedAudioCodecs, Codec.Audio.WAV, Codec.Audio.OGG)
|
||||
}
|
||||
|
||||
subtitleProfile(Codec.Subtitle.VTT, embedded = true, hls = true, external = true)
|
||||
subtitleProfile(Codec.Subtitle.WEBVTT, embedded = true, hls = true, external = true)
|
||||
subtitleProfile(Codec.Subtitle.SRT, embedded = true, external = true)
|
||||
subtitleProfile(Codec.Subtitle.SUBRIP, embedded = true, external = true)
|
||||
subtitleProfile(Codec.Subtitle.TTML, embedded = true, external = true)
|
||||
subtitleProfile(Codec.Subtitle.DVBSUB, embedded = true, encode = true)
|
||||
subtitleProfile(Codec.Subtitle.DVDSUB, embedded = true, encode = true)
|
||||
subtitleProfile(Codec.Subtitle.IDX, embedded = true, encode = true)
|
||||
subtitleProfile(Codec.Subtitle.PGS, embedded = true, encode = true)
|
||||
subtitleProfile(Codec.Subtitle.PGSSUB, embedded = true, encode = true)
|
||||
subtitleProfile(Codec.Subtitle.ASS, encode = true, embedded = true, external = true)
|
||||
subtitleProfile(Codec.Subtitle.SSA, encode = true, embedded = true, external = true)
|
||||
}
|
||||
|
|
@ -0,0 +1,817 @@
|
|||
package com.github.damontecres.wholphin.util.mpv
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
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.annotation.OptIn
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.BasePlayer
|
||||
import androidx.media3.common.C
|
||||
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.Timeline
|
||||
import androidx.media3.common.TrackGroup
|
||||
import androidx.media3.common.TrackSelectionParameters
|
||||
import androidx.media3.common.Tracks
|
||||
import androidx.media3.common.VideoSize
|
||||
import androidx.media3.common.text.CueGroup
|
||||
import androidx.media3.common.util.Clock
|
||||
import androidx.media3.common.util.ListenerSet
|
||||
import androidx.media3.common.util.Size
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.common.util.Util
|
||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||
import androidx.media3.exoplayer.trackselection.TrackSelector
|
||||
import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter
|
||||
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_STOP
|
||||
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEvent.MPV_EVENT_AUDIO_RECONFIG
|
||||
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_PLAYBACK_RESTART
|
||||
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEvent.MPV_EVENT_VIDEO_RECONFIG
|
||||
import timber.log.Timber
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* This is barebones implementation of a [Player] which plays content using libmpv
|
||||
*
|
||||
* It doesn't support every feature or emit every event
|
||||
*/
|
||||
@kotlin.OptIn(ExperimentalAtomicApi::class)
|
||||
@OptIn(UnstableApi::class)
|
||||
class MpvPlayer(
|
||||
private val context: Context,
|
||||
enableHardwareDecoding: Boolean,
|
||||
) : BasePlayer(),
|
||||
MPVLib.EventObserver,
|
||||
TrackSelector.InvalidationListener {
|
||||
companion object {
|
||||
private const val DEBUG = false
|
||||
}
|
||||
|
||||
private var isPaused: Boolean = true
|
||||
private var surface: Surface? = null
|
||||
|
||||
private val looper = Util.getCurrentOrMainLooper()
|
||||
private val handler = Handler(looper)
|
||||
private val listeners =
|
||||
ListenerSet<Player.Listener>(
|
||||
looper,
|
||||
Clock.DEFAULT,
|
||||
) { listener, eventFlags ->
|
||||
listener.onEvents(this@MpvPlayer, Player.Events(eventFlags))
|
||||
}
|
||||
private val availableCommands: Player.Commands
|
||||
private val trackSelector = DefaultTrackSelector(context)
|
||||
|
||||
private var mediaItem: MediaItem? = null
|
||||
private var startPositionMs: Long = 0L
|
||||
private var durationMs: Long = 0L
|
||||
private var positionMs: Long = -1L
|
||||
private var playbackState: Int = STATE_READY
|
||||
|
||||
@Volatile
|
||||
var isReleased = false
|
||||
private set
|
||||
|
||||
@Volatile
|
||||
private var isLoadingFile = false
|
||||
|
||||
init {
|
||||
Timber.v("config-dir=${context.filesDir.path}")
|
||||
MPVLib.create(context)
|
||||
MPVLib.setOptionString("config", "yes")
|
||||
MPVLib.setOptionString("config-dir", context.filesDir.path)
|
||||
|
||||
if (enableHardwareDecoding) {
|
||||
MPVLib.setOptionString("hwdec", "mediacodec,mediacodec-copy")
|
||||
MPVLib.setOptionString("vo", "gpu")
|
||||
} else {
|
||||
MPVLib.setOptionString("hwdec", "no")
|
||||
}
|
||||
MPVLib.setOptionString("gpu-context", "android")
|
||||
|
||||
MPVLib.setOptionString("opengl-es", "yes")
|
||||
MPVLib.setOptionString("hwdec-codecs", "h264,hevc,mpeg4,mpeg2video,vp8,vp9,av1")
|
||||
val cacheMegs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) 64 else 32
|
||||
MPVLib.setOptionString("demuxer-max-bytes", "${cacheMegs * 1024 * 1024}")
|
||||
MPVLib.setOptionString("demuxer-max-back-bytes", "${cacheMegs * 1024 * 1024}")
|
||||
|
||||
MPVLib.init()
|
||||
|
||||
MPVLib.setOptionString("force-window", "no")
|
||||
MPVLib.setOptionString("idle", "yes")
|
||||
// MPVLib.setOptionString("sub-fonts-dir", File(context.filesDir, "fonts").absolutePath)
|
||||
|
||||
MPVLib.addObserver(this)
|
||||
MPVProperty.observedProperties.forEach(MPVLib::observeProperty)
|
||||
|
||||
availableCommands =
|
||||
Player.Commands
|
||||
.Builder()
|
||||
.addAll(
|
||||
COMMAND_PLAY_PAUSE,
|
||||
COMMAND_PREPARE,
|
||||
COMMAND_STOP,
|
||||
COMMAND_SET_SPEED_AND_PITCH,
|
||||
COMMAND_SET_SHUFFLE_MODE,
|
||||
COMMAND_SET_REPEAT_MODE,
|
||||
COMMAND_GET_CURRENT_MEDIA_ITEM,
|
||||
COMMAND_GET_TIMELINE,
|
||||
COMMAND_GET_METADATA,
|
||||
// COMMAND_SET_PLAYLIST_METADATA,
|
||||
COMMAND_SET_MEDIA_ITEM,
|
||||
// COMMAND_CHANGE_MEDIA_ITEMS,
|
||||
COMMAND_GET_TRACKS,
|
||||
// COMMAND_GET_AUDIO_ATTRIBUTES,
|
||||
// COMMAND_SET_AUDIO_ATTRIBUTES,
|
||||
// COMMAND_GET_VOLUME,
|
||||
// COMMAND_SET_VOLUME,
|
||||
COMMAND_SET_VIDEO_SURFACE,
|
||||
// COMMAND_GET_TEXT,
|
||||
COMMAND_RELEASE,
|
||||
).build()
|
||||
trackSelector.init(this, DefaultBandwidthMeter.getSingletonInstance(context))
|
||||
}
|
||||
|
||||
override fun getApplicationLooper(): Looper = looper
|
||||
|
||||
override fun addListener(listener: Player.Listener) {
|
||||
if (DEBUG) Timber.v("addListener")
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
override fun removeListener(listener: Player.Listener) {
|
||||
if (DEBUG) Timber.v("removeListener")
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
override fun setMediaItems(
|
||||
mediaItems: List<MediaItem>,
|
||||
resetPosition: Boolean,
|
||||
) {
|
||||
throwIfReleased()
|
||||
|
||||
if (DEBUG) Timber.v("setMediaItems")
|
||||
mediaItems.firstOrNull()?.let {
|
||||
mediaItem = it
|
||||
if (surface != null) {
|
||||
loadFile(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setMediaItems(
|
||||
mediaItems: List<MediaItem>,
|
||||
startIndex: Int,
|
||||
startPositionMs: Long,
|
||||
) {
|
||||
if (DEBUG) Timber.v("setMediaItems")
|
||||
this.startPositionMs = startPositionMs
|
||||
setMediaItems(mediaItems.subList(startIndex, mediaItems.size), false)
|
||||
}
|
||||
|
||||
override fun addMediaItems(
|
||||
index: Int,
|
||||
mediaItems: List<MediaItem>,
|
||||
): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun moveMediaItems(
|
||||
fromIndex: Int,
|
||||
toIndex: Int,
|
||||
newIndex: Int,
|
||||
): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun replaceMediaItems(
|
||||
fromIndex: Int,
|
||||
toIndex: Int,
|
||||
mediaItems: List<MediaItem>,
|
||||
): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun removeMediaItems(
|
||||
fromIndex: Int,
|
||||
toIndex: Int,
|
||||
): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun getAvailableCommands(): Player.Commands = availableCommands
|
||||
|
||||
override fun prepare() {
|
||||
if (DEBUG) Timber.v("prepare")
|
||||
durationMs = 0L
|
||||
positionMs = -1L
|
||||
playbackState = STATE_READY
|
||||
}
|
||||
|
||||
override fun getPlaybackState(): Int {
|
||||
if (DEBUG) Timber.v("getPlaybackState")
|
||||
return playbackState
|
||||
}
|
||||
|
||||
override fun getPlaybackSuppressionReason(): Int = Player.PLAYBACK_SUPPRESSION_REASON_NONE
|
||||
|
||||
override fun getPlayerError(): PlaybackException? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setPlayWhenReady(playWhenReady: Boolean) {
|
||||
if (isReleased) return
|
||||
if (DEBUG) Timber.v("setPlayWhenReady")
|
||||
if (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 {
|
||||
if (DEBUG) Timber.v("getPlayWhenReady")
|
||||
if (isReleased) return false
|
||||
val isPaused = MPVLib.getPropertyBoolean("pause") ?: this.isPaused
|
||||
return !isPaused
|
||||
}
|
||||
|
||||
override fun setRepeatMode(repeatMode: Int) {
|
||||
if (DEBUG) Timber.v("setRepeatMode")
|
||||
}
|
||||
|
||||
override fun getRepeatMode(): Int = Player.REPEAT_MODE_OFF
|
||||
|
||||
override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) {
|
||||
if (DEBUG) Timber.v("setShuffleModeEnabled")
|
||||
}
|
||||
|
||||
override fun getShuffleModeEnabled(): Boolean = false
|
||||
|
||||
override fun isLoading(): Boolean = isLoadingFile
|
||||
|
||||
override fun getSeekBackIncrement(): Long = 10_000
|
||||
|
||||
override fun getSeekForwardIncrement(): Long = 30_000
|
||||
|
||||
override fun getMaxSeekToPreviousPosition(): Long = 10_000
|
||||
|
||||
override fun setPlaybackParameters(playbackParameters: PlaybackParameters) {
|
||||
if (DEBUG) Timber.v("setPlaybackParameters")
|
||||
MPVLib.setPropertyDouble("speed", playbackParameters.speed.toDouble())
|
||||
}
|
||||
|
||||
override fun getPlaybackParameters(): PlaybackParameters {
|
||||
if (DEBUG) Timber.v("getPlaybackParameters")
|
||||
val speed = MPVLib.getPropertyDouble("speed")?.toFloat() ?: 1f
|
||||
return PlaybackParameters(speed)
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
if (DEBUG) Timber.v("stop")
|
||||
if (isReleased) return
|
||||
pause()
|
||||
mediaItem = null
|
||||
positionMs = -1L
|
||||
durationMs = 0L
|
||||
playbackState = STATE_IDLE
|
||||
notifyListeners(EVENT_IS_PLAYING_CHANGED) { onIsPlayingChanged(false) }
|
||||
notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) { onPlaybackStateChanged(STATE_IDLE) }
|
||||
}
|
||||
|
||||
override fun release() {
|
||||
Timber.i("release")
|
||||
if (!isReleased) {
|
||||
MPVLib.removeObserver(this)
|
||||
clearVideoSurfaceView(null)
|
||||
MPVLib.destroy()
|
||||
}
|
||||
isReleased = true
|
||||
}
|
||||
|
||||
override fun getCurrentTracks(): Tracks {
|
||||
if (DEBUG) Timber.v("getCurrentTracks")
|
||||
if (isReleased) return Tracks.EMPTY
|
||||
return getTracks()
|
||||
}
|
||||
|
||||
override fun getTrackSelectionParameters(): TrackSelectionParameters {
|
||||
if (DEBUG) Timber.v("getTrackSelectionParameters")
|
||||
|
||||
return TrackSelectionParameters
|
||||
.Builder()
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun setTrackSelectionParameters(parameters: TrackSelectionParameters) {
|
||||
Timber.v("TrackSelection: setTrackSelectionParameters %s", parameters)
|
||||
if (isReleased) return
|
||||
val tracks = getTracks()
|
||||
if (C.TRACK_TYPE_TEXT in parameters.disabledTrackTypes) {
|
||||
// Subtitles disabled
|
||||
Timber.v("TrackSelection: disabling subtitles")
|
||||
MPVLib.setPropertyString("sid", "no")
|
||||
}
|
||||
if (C.TRACK_TYPE_AUDIO in parameters.disabledTrackTypes) {
|
||||
// Audio disabled
|
||||
Timber.v("TrackSelection: disabling audio")
|
||||
MPVLib.setPropertyString("aid", "no")
|
||||
}
|
||||
Timber.v("TrackSelection: Got ${parameters.overrides.size} overrides")
|
||||
parameters.overrides.forEach { (trackGroup, trackSelectionOverride) ->
|
||||
val result =
|
||||
tracks.groups.firstOrNull { it.mediaTrackGroup == trackGroup }?.let {
|
||||
val id = it.mediaTrackGroup.getFormat(0).id
|
||||
val splits = id?.split(":")
|
||||
val trackId = splits?.getOrNull(1)
|
||||
val propertyName =
|
||||
when (it.mediaTrackGroup.type) {
|
||||
C.TRACK_TYPE_AUDIO -> "aid"
|
||||
C.TRACK_TYPE_VIDEO -> "vid"
|
||||
C.TRACK_TYPE_TEXT -> "sid"
|
||||
else -> null
|
||||
}
|
||||
Timber.v("TrackSelection: activating %s %s '%s'", propertyName, trackId, id)
|
||||
if (trackId != null && propertyName != null) {
|
||||
MPVLib.setPropertyString(propertyName, trackId)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
if (result != true) {
|
||||
Timber.w(
|
||||
"Did not find track to select for type=%s, id=%s",
|
||||
trackGroup.type,
|
||||
trackGroup.getFormat(0).id,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMediaMetadata(): MediaMetadata {
|
||||
if (DEBUG) Timber.v("getMediaMetadata")
|
||||
return MediaMetadata.EMPTY
|
||||
}
|
||||
|
||||
override fun getPlaylistMetadata(): MediaMetadata {
|
||||
if (DEBUG) Timber.v("getPlaylistMetadata")
|
||||
return MediaMetadata.EMPTY
|
||||
}
|
||||
|
||||
override fun setPlaylistMetadata(mediaMetadata: MediaMetadata): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun getCurrentTimeline(): Timeline {
|
||||
if (DEBUG) Timber.v("getCurrentTimeline")
|
||||
// TODO
|
||||
return Timeline.EMPTY
|
||||
}
|
||||
|
||||
override fun getCurrentPeriodIndex(): Int {
|
||||
if (DEBUG) Timber.v("getCurrentPeriodIndex")
|
||||
// TODO
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun getCurrentMediaItemIndex(): Int {
|
||||
if (DEBUG) Timber.v("getCurrentMediaItemIndex")
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun getDuration(): Long {
|
||||
if (DEBUG) Timber.v("getDuration")
|
||||
if (isReleased) {
|
||||
return durationMs
|
||||
}
|
||||
val duration =
|
||||
MPVLib.getPropertyDouble("duration/full")?.seconds?.inWholeMilliseconds
|
||||
?: durationMs
|
||||
return duration
|
||||
}
|
||||
|
||||
override fun getCurrentPosition(): Long {
|
||||
if (DEBUG) Timber.v("getCurrentPosition")
|
||||
if (isReleased) {
|
||||
return positionMs
|
||||
}
|
||||
val position =
|
||||
MPVLib.getPropertyDouble("time-pos/full")?.seconds?.inWholeMilliseconds
|
||||
?: positionMs
|
||||
return position
|
||||
}
|
||||
|
||||
override fun getBufferedPosition(): Long {
|
||||
if (DEBUG) Timber.v("getBufferedPosition")
|
||||
return currentPosition + totalBufferedDuration
|
||||
}
|
||||
|
||||
override fun getTotalBufferedDuration(): Long {
|
||||
if (DEBUG) Timber.v("getTotalBufferedDuration")
|
||||
if (isReleased) return 0
|
||||
return MPVLib.getPropertyDouble("demuxer-cache-duration")?.seconds?.inWholeMilliseconds ?: 0
|
||||
}
|
||||
|
||||
override fun isPlayingAd(): Boolean {
|
||||
if (DEBUG) Timber.v("isPlayingAd")
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getCurrentAdGroupIndex(): Int = C.INDEX_UNSET
|
||||
|
||||
override fun getCurrentAdIndexInAdGroup(): Int = C.INDEX_UNSET
|
||||
|
||||
override fun getContentPosition(): Long = currentPosition
|
||||
|
||||
override fun getContentBufferedPosition(): Long = bufferedPosition
|
||||
|
||||
override fun getAudioAttributes(): AudioAttributes = throw UnsupportedOperationException()
|
||||
|
||||
override fun setVolume(volume: Float): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun getVolume(): Float = 1f
|
||||
|
||||
override fun clearVideoSurface(): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun clearVideoSurface(surface: Surface?): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun setVideoSurface(surface: Surface?): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun setVideoSurfaceHolder(surfaceHolder: SurfaceHolder?): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun clearVideoSurfaceHolder(surfaceHolder: SurfaceHolder?): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun setVideoSurfaceView(surfaceView: SurfaceView?) {
|
||||
throwIfReleased()
|
||||
if (DEBUG) Timber.v("setVideoSurfaceView")
|
||||
val surface = surfaceView?.holder?.surface
|
||||
if (surface != null) {
|
||||
this.surface = surface
|
||||
Timber.v("Queued attach")
|
||||
MPVLib.attachSurface(surface)
|
||||
MPVLib.setOptionString("force-window", "yes")
|
||||
Timber.d("Attached surface")
|
||||
mediaItem?.let(::loadFile)
|
||||
if (mediaItem == null) {
|
||||
Timber.w("mediaItem is null in setVideoSurfaceView")
|
||||
}
|
||||
} else {
|
||||
clearVideoSurfaceView(null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearVideoSurfaceView(surfaceView: SurfaceView?) {
|
||||
Timber.d("clearVideoSurfaceView")
|
||||
MPVLib.detachSurface()
|
||||
MPVLib.setPropertyString("vo", "null")
|
||||
MPVLib.setPropertyString("force-window", "no")
|
||||
mediaItem = null
|
||||
}
|
||||
|
||||
override fun setVideoTextureView(textureView: TextureView?): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun clearVideoTextureView(textureView: TextureView?): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun getVideoSize(): VideoSize {
|
||||
if (DEBUG) Timber.v("getVideoSize")
|
||||
if (isReleased) return VideoSize.UNKNOWN
|
||||
val width = MPVLib.getPropertyInt("width")
|
||||
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 getCurrentCues(): CueGroup = CueGroup.EMPTY_TIME_ZERO
|
||||
|
||||
override fun getDeviceInfo(): DeviceInfo {
|
||||
if (DEBUG) Timber.v("getDeviceInfo")
|
||||
return DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE).build()
|
||||
}
|
||||
|
||||
override fun getDeviceVolume(): Int = throw UnsupportedOperationException()
|
||||
|
||||
override fun isDeviceMuted(): Boolean = throw UnsupportedOperationException()
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun setDeviceVolume(volume: Int): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun setDeviceVolume(
|
||||
volume: Int,
|
||||
flags: Int,
|
||||
): Unit = throw UnsupportedOperationException()
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun increaseDeviceVolume(): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun increaseDeviceVolume(flags: Int): Unit = throw UnsupportedOperationException()
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun decreaseDeviceVolume(): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun decreaseDeviceVolume(flags: Int): Unit = throw UnsupportedOperationException()
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun setDeviceMuted(muted: Boolean): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun setDeviceMuted(
|
||||
muted: Boolean,
|
||||
flags: Int,
|
||||
): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun setAudioAttributes(
|
||||
audioAttributes: AudioAttributes,
|
||||
handleAudioFocus: Boolean,
|
||||
): Unit = throw UnsupportedOperationException()
|
||||
|
||||
override fun seekTo(
|
||||
mediaItemIndex: Int,
|
||||
positionMs: Long,
|
||||
seekCommand: Int,
|
||||
isRepeatingCurrentItem: Boolean,
|
||||
) {
|
||||
if (DEBUG) Timber.v("seekTo")
|
||||
if (isReleased) {
|
||||
Timber.w("seekTo called after release")
|
||||
return
|
||||
}
|
||||
if (mediaItemIndex == C.INDEX_UNSET) {
|
||||
return
|
||||
}
|
||||
MPVLib.setPropertyDouble("time-pos", positionMs / 1000.0)
|
||||
}
|
||||
|
||||
override fun eventProperty(property: String) {
|
||||
if (DEBUG) Timber.v("eventProperty: $property")
|
||||
}
|
||||
|
||||
override fun eventProperty(
|
||||
property: String,
|
||||
value: Long,
|
||||
) {
|
||||
if (DEBUG) Timber.v("eventPropertyLong: $property=$value")
|
||||
when (property) {
|
||||
MPVProperty.POSITION -> positionMs = value.seconds.inWholeMilliseconds
|
||||
}
|
||||
}
|
||||
|
||||
override fun eventProperty(
|
||||
property: String,
|
||||
value: Boolean,
|
||||
) {
|
||||
if (DEBUG) Timber.v("eventPropertyBoolean: $property=$value")
|
||||
when (property) {
|
||||
MPVProperty.PAUSED -> {
|
||||
isPaused = value
|
||||
notifyListeners(EVENT_IS_PLAYING_CHANGED) { onIsPlayingChanged(!value) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun eventProperty(
|
||||
property: String,
|
||||
value: String,
|
||||
) {
|
||||
if (DEBUG) Timber.v("eventPropertyString: $property=$value")
|
||||
}
|
||||
|
||||
override fun eventProperty(
|
||||
property: String,
|
||||
value: Double,
|
||||
) {
|
||||
Timber.v("eventPropertyDouble: $property=$value")
|
||||
when (property) {
|
||||
MPVProperty.DURATION -> durationMs = value.seconds.inWholeMilliseconds
|
||||
}
|
||||
}
|
||||
|
||||
override fun event(eventId: Int) {
|
||||
when (eventId) {
|
||||
// MPV_EVENT_START_FILE -> {
|
||||
// }
|
||||
MPV_EVENT_FILE_LOADED -> {
|
||||
isLoadingFile = false
|
||||
notifyListeners(EVENT_IS_LOADING_CHANGED) { onIsLoadingChanged(false) }
|
||||
Timber.d("event: MPV_EVENT_FILE_LOADED")
|
||||
mediaItem?.let {
|
||||
it.localConfiguration?.subtitleConfigurations?.forEach {
|
||||
val url = it.uri.toString()
|
||||
val title = it.label ?: "External Subtitles"
|
||||
Timber.v("Adding external subtitle track '$title'")
|
||||
MPVLib.command(arrayOf("sub-add", url, "auto", title))
|
||||
}
|
||||
}
|
||||
notifyListeners(EVENT_RENDERED_FIRST_FRAME) { onRenderedFirstFrame() }
|
||||
notifyListeners(EVENT_IS_PLAYING_CHANGED) { onIsPlayingChanged(true) }
|
||||
getTracks().let {
|
||||
notifyListeners(EVENT_TRACKS_CHANGED) { onTracksChanged(it) }
|
||||
}
|
||||
}
|
||||
|
||||
MPV_EVENT_PLAYBACK_RESTART -> {
|
||||
Timber.d("event: MPV_EVENT_PLAYBACK_RESTART")
|
||||
getTracks().let {
|
||||
notifyListeners(EVENT_TRACKS_CHANGED) { onTracksChanged(it) }
|
||||
}
|
||||
}
|
||||
|
||||
MPV_EVENT_AUDIO_RECONFIG -> {
|
||||
Timber.d("event: MPV_EVENT_AUDIO_RECONFIG")
|
||||
getTracks().let {
|
||||
notifyListeners(EVENT_TRACKS_CHANGED) { onTracksChanged(it) }
|
||||
}
|
||||
}
|
||||
|
||||
MPV_EVENT_VIDEO_RECONFIG -> {
|
||||
Timber.d("event: MPV_EVENT_VIDEO_RECONFIG")
|
||||
getTracks().let {
|
||||
notifyListeners(EVENT_TRACKS_CHANGED) { onTracksChanged(it) }
|
||||
}
|
||||
}
|
||||
|
||||
MPV_EVENT_END_FILE -> {
|
||||
Timber.d("event: MPV_EVENT_END_FILE")
|
||||
// Handled by eventEndFile
|
||||
}
|
||||
|
||||
else -> {
|
||||
Timber.v("event: $eventId")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun eventEndFile(
|
||||
reason: Int,
|
||||
error: Int,
|
||||
) {
|
||||
Timber.d("MPV_EVENT_END_FILE: %s %s", reason, error)
|
||||
notifyListeners(EVENT_IS_PLAYING_CHANGED) { onIsPlayingChanged(false) }
|
||||
when (reason) {
|
||||
MPV_END_FILE_REASON_EOF -> {
|
||||
notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) {
|
||||
onPlaybackStateChanged(STATE_ENDED)
|
||||
}
|
||||
}
|
||||
|
||||
MPV_END_FILE_REASON_STOP -> {
|
||||
// User initiated (eg stop, play next, etc)
|
||||
}
|
||||
|
||||
MPV_END_FILE_REASON_ERROR -> {
|
||||
Timber.e("libmpv error, error=%s", error)
|
||||
notifyListeners(EVENT_PLAYER_ERROR) {
|
||||
onPlayerError(
|
||||
PlaybackException(
|
||||
"libmpv error",
|
||||
null,
|
||||
error,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadFile(mediaItem: MediaItem) {
|
||||
isLoadingFile = true
|
||||
notifyListeners(EVENT_IS_LOADING_CHANGED) { onIsLoadingChanged(true) }
|
||||
val url = mediaItem.localConfiguration?.uri.toString()
|
||||
if (startPositionMs > 0) {
|
||||
MPVLib.command(
|
||||
arrayOf(
|
||||
"loadfile",
|
||||
url,
|
||||
"replace",
|
||||
"-1",
|
||||
"start=${startPositionMs / 1000.0}",
|
||||
),
|
||||
)
|
||||
} else {
|
||||
MPVLib.command(arrayOf("loadfile", url, "replace", "-1"))
|
||||
}
|
||||
|
||||
MPVLib.setPropertyString("vo", "gpu")
|
||||
Timber.d("Called loadfile")
|
||||
}
|
||||
|
||||
private fun throwIfReleased() {
|
||||
if (isReleased) {
|
||||
throw IllegalStateException("Cannot access MpvPlayer after it is released")
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyListeners(
|
||||
eventId: Int,
|
||||
block: Player.Listener.() -> Unit,
|
||||
) {
|
||||
handler.post {
|
||||
listeners.queueEvent(eventId) {
|
||||
block.invoke(it)
|
||||
}
|
||||
listeners.flushEvents()
|
||||
}
|
||||
}
|
||||
|
||||
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 subtitleDelay: Double
|
||||
get() {
|
||||
if (isReleased) return 0.0
|
||||
return MPVLib.getPropertyDouble("sub-delay") ?: 0.0
|
||||
}
|
||||
set(value) {
|
||||
if (isReleased) return
|
||||
MPVLib.setPropertyDouble("sub-delay", value)
|
||||
}
|
||||
}
|
||||
|
||||
fun MPVLib.setPropertyColor(
|
||||
property: String,
|
||||
color: Color,
|
||||
) = MPVLib.setPropertyString(property, color.mpvFormat)
|
||||
|
||||
private val Color.mpvFormat: String get() = "$red/$green/$blue/$alpha"
|
||||
|
|
@ -22,7 +22,7 @@ private val downmixSupportedAudioCodecs =
|
|||
Codec.Audio.MP3,
|
||||
)
|
||||
|
||||
private val supportedAudioCodecs =
|
||||
val supportedAudioCodecs =
|
||||
arrayOf(
|
||||
Codec.Audio.AAC,
|
||||
Codec.Audio.AAC_LATM,
|
||||
|
|
@ -495,7 +495,7 @@ fun createDeviceProfile(
|
|||
}
|
||||
|
||||
// Little helper function to more easily define subtitle profiles
|
||||
private fun DeviceProfileBuilder.subtitleProfile(
|
||||
fun DeviceProfileBuilder.subtitleProfile(
|
||||
format: String,
|
||||
embedded: Boolean = false,
|
||||
external: Boolean = false,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue