1580 lines
61 KiB
Kotlin
1580 lines
61 KiB
Kotlin
package dev.jdtech.jellyfin.mpv
|
|
|
|
import `is`.xyz.libmpv.MPVLib
|
|
import android.annotation.SuppressLint
|
|
import android.app.Application
|
|
import android.content.Context
|
|
import android.content.res.AssetManager
|
|
import android.media.AudioManager
|
|
import android.os.Handler
|
|
import android.os.Looper
|
|
import android.os.Parcelable
|
|
import android.view.Surface
|
|
import android.view.SurfaceHolder
|
|
import android.view.SurfaceView
|
|
import android.view.TextureView
|
|
import androidx.core.content.getSystemService
|
|
import com.google.android.exoplayer2.*
|
|
import com.google.android.exoplayer2.Player.Commands
|
|
import com.google.android.exoplayer2.audio.AudioAttributes
|
|
import com.google.android.exoplayer2.source.MediaSource
|
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
|
import com.google.android.exoplayer2.source.TrackGroup
|
|
import com.google.android.exoplayer2.source.TrackGroupArray
|
|
import com.google.android.exoplayer2.text.Cue
|
|
import com.google.android.exoplayer2.trackselection.TrackSelectionArray
|
|
import com.google.android.exoplayer2.trackselection.TrackSelectionParameters
|
|
import com.google.android.exoplayer2.upstream.DataSource
|
|
import com.google.android.exoplayer2.util.*
|
|
import com.google.android.exoplayer2.video.VideoSize
|
|
import kotlinx.parcelize.Parcelize
|
|
import org.json.JSONArray
|
|
import org.json.JSONException
|
|
import org.json.JSONObject
|
|
import java.io.File
|
|
import java.io.FileOutputStream
|
|
import java.util.concurrent.CopyOnWriteArraySet
|
|
|
|
@Suppress("SpellCheckingInspection")
|
|
class MPVPlayer(
|
|
context: Context,
|
|
requestAudioFocus: Boolean,
|
|
preferredLanguages: Map<String, String>,
|
|
disableHardwareDecoding: Boolean
|
|
) : BasePlayer(), MPVLib.EventObserver, AudioManager.OnAudioFocusChangeListener {
|
|
|
|
private val audioManager: AudioManager by lazy { context.getSystemService()!! }
|
|
private var audioFocusCallback: () -> Unit = {}
|
|
private var currentIndex = 0
|
|
private var audioFocusRequest = AudioManager.AUDIOFOCUS_REQUEST_FAILED
|
|
private val handler = Handler(context.mainLooper)
|
|
|
|
init {
|
|
require(context is Application)
|
|
val mpvDir = File(context.getExternalFilesDir(null) ?: context.filesDir, "mpv")
|
|
if (!mpvDir.exists()) {
|
|
mpvDir.mkdirs()
|
|
}
|
|
arrayOf("mpv.conf", "subfont.ttf").forEach { fileName ->
|
|
val file = File(mpvDir, fileName)
|
|
Log.i("mpv", "File ${file.absolutePath}")
|
|
if (!file.exists()) {
|
|
context.assets.open(fileName, AssetManager.ACCESS_STREAMING)
|
|
.copyTo(FileOutputStream(file))
|
|
}
|
|
}
|
|
MPVLib.create(context)
|
|
|
|
// General
|
|
MPVLib.setOptionString("config-dir", mpvDir.path)
|
|
MPVLib.setOptionString("vo", "gpu")
|
|
MPVLib.setOptionString("gpu-context", "android")
|
|
MPVLib.setOptionString("ao", "audiotrack,opensles")
|
|
|
|
// Hardware video decoding
|
|
if (disableHardwareDecoding) {
|
|
MPVLib.setOptionString("hwdec", "no")
|
|
} else {
|
|
MPVLib.setOptionString("hwdec", "mediacodec-copy")
|
|
}
|
|
MPVLib.setOptionString("hwdec-codecs", "h264,hevc,mpeg4,mpeg2video,vp8,vp9,av1")
|
|
|
|
// TLS
|
|
MPVLib.setOptionString("tls-verify", "no")
|
|
|
|
// Cache
|
|
MPVLib.setOptionString("cache", "yes")
|
|
MPVLib.setOptionString("cache-pause-initial", "yes")
|
|
MPVLib.setOptionString("demuxer-max-bytes", "32MiB")
|
|
MPVLib.setOptionString("demuxer-max-back-bytes", "32MiB")
|
|
|
|
// Subs
|
|
MPVLib.setOptionString("sub-scale-with-window", "no")
|
|
MPVLib.setOptionString("sub-use-margins", "no")
|
|
|
|
// Other options
|
|
MPVLib.setOptionString("force-window", "no")
|
|
MPVLib.setOptionString("keep-open", "always")
|
|
MPVLib.setOptionString("save-position-on-quit", "no")
|
|
MPVLib.setOptionString("sub-font-provider", "none")
|
|
MPVLib.setOptionString("ytdl", "no")
|
|
|
|
MPVLib.init()
|
|
|
|
for (preferredLanguage in preferredLanguages) {
|
|
when (preferredLanguage.key) {
|
|
TrackType.AUDIO -> {
|
|
MPVLib.setOptionString("alang", preferredLanguage.value)
|
|
}
|
|
TrackType.SUBTITLE -> {
|
|
MPVLib.setOptionString("slang", preferredLanguage.value)
|
|
}
|
|
}
|
|
}
|
|
|
|
MPVLib.addObserver(this)
|
|
|
|
// Observe properties
|
|
data class Property(val name: String, @MPVLib.Format val format: Int)
|
|
arrayOf(
|
|
Property("track-list", MPVLib.MPV_FORMAT_STRING),
|
|
Property("paused-for-cache", MPVLib.MPV_FORMAT_FLAG),
|
|
Property("eof-reached", MPVLib.MPV_FORMAT_FLAG),
|
|
Property("seekable", MPVLib.MPV_FORMAT_FLAG),
|
|
Property("time-pos", MPVLib.MPV_FORMAT_INT64),
|
|
Property("duration", MPVLib.MPV_FORMAT_INT64),
|
|
Property("demuxer-cache-time", MPVLib.MPV_FORMAT_INT64),
|
|
Property("speed", MPVLib.MPV_FORMAT_DOUBLE)
|
|
).forEach { (name, format) ->
|
|
MPVLib.observeProperty(name, format)
|
|
}
|
|
|
|
if (requestAudioFocus) {
|
|
@Suppress("DEPRECATION")
|
|
audioFocusRequest = audioManager.requestAudioFocus(
|
|
/* listener= */ this,
|
|
/* streamType= */ AudioManager.STREAM_MUSIC,
|
|
/* durationHint= */ AudioManager.AUDIOFOCUS_GAIN
|
|
)
|
|
if (audioFocusRequest != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
|
MPVLib.setPropertyBoolean("pause", true)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Listeners and notification.
|
|
private val listeners: ListenerSet<Player.Listener> = ListenerSet(
|
|
context.mainLooper,
|
|
Clock.DEFAULT
|
|
) { listener: Player.Listener, flags: FlagSet ->
|
|
listener.onEvents( /* player= */this, Player.Events(flags))
|
|
}
|
|
private val videoListeners =
|
|
CopyOnWriteArraySet<Player.Listener>()
|
|
|
|
// Internal state.
|
|
private var internalMediaItems: List<MediaItem>? = null
|
|
private var internalMediaItem: MediaItem? = null
|
|
|
|
@Player.State
|
|
private var playbackState: Int = Player.STATE_IDLE
|
|
private var currentPlayWhenReady: Boolean = false
|
|
|
|
@Player.RepeatMode
|
|
private val repeatMode: Int = REPEAT_MODE_OFF
|
|
private var tracksInfo: TracksInfo = TracksInfo.EMPTY
|
|
private var playbackParameters: PlaybackParameters = PlaybackParameters.DEFAULT
|
|
|
|
// MPV Custom
|
|
private var isPlayerReady: Boolean = false
|
|
private var isSeekable: Boolean = false
|
|
private var currentPositionMs: Long? = null
|
|
private var currentDurationMs: Long? = null
|
|
private var currentCacheDurationMs: Long? = null
|
|
var currentTracks: List<Track> = emptyList()
|
|
private var initialCommands = mutableListOf<Array<String>>()
|
|
private var initialSeekTo: Long = 0L
|
|
|
|
// mpv events
|
|
override fun eventProperty(property: String) {
|
|
// Nothing to do...
|
|
}
|
|
|
|
override fun eventProperty(property: String, value: String) {
|
|
handler.post {
|
|
when (property) {
|
|
"track-list" -> {
|
|
val (tracks, newTracksInfo) = getMPVTracks(value)
|
|
tracks.forEach { Log.i("mpv", "${it.ffIndex} ${it.type} ${it.codec}") }
|
|
currentTracks = tracks
|
|
if (isPlayerReady) {
|
|
if (newTracksInfo != tracksInfo) {
|
|
tracksInfo = newTracksInfo
|
|
listeners.sendEvent(Player.EVENT_TRACKS_CHANGED) { listener ->
|
|
listener.onTracksInfoChanged(currentTracksInfo)
|
|
}
|
|
}
|
|
} else {
|
|
tracksInfo = newTracksInfo
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun eventProperty(property: String, value: Boolean) {
|
|
handler.post {
|
|
when (property) {
|
|
"eof-reached" -> {
|
|
if (value && isPlayerReady) {
|
|
if (currentIndex < internalMediaItems?.size ?: 0) {
|
|
currentIndex += 1
|
|
prepareMediaItem(currentIndex)
|
|
play()
|
|
} else {
|
|
setPlayerStateAndNotifyIfChanged(
|
|
playWhenReady = false,
|
|
playWhenReadyChangeReason = Player.PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM,
|
|
playbackState = Player.STATE_ENDED
|
|
)
|
|
resetInternalState()
|
|
}
|
|
}
|
|
}
|
|
"paused-for-cache" -> {
|
|
if (isPlayerReady) {
|
|
if (value) {
|
|
setPlayerStateAndNotifyIfChanged(playbackState = Player.STATE_BUFFERING)
|
|
} else {
|
|
setPlayerStateAndNotifyIfChanged(playbackState = Player.STATE_READY)
|
|
}
|
|
}
|
|
}
|
|
"seekable" -> {
|
|
if (isSeekable != value) {
|
|
isSeekable = value
|
|
listeners.sendEvent(Player.EVENT_TIMELINE_CHANGED) { listener ->
|
|
listener.onTimelineChanged(
|
|
timeline,
|
|
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun eventProperty(property: String, value: Long) {
|
|
handler.post {
|
|
when (property) {
|
|
"time-pos" -> currentPositionMs = value * C.MILLIS_PER_SECOND
|
|
"duration" -> {
|
|
if (currentDurationMs != value * C.MILLIS_PER_SECOND) {
|
|
currentDurationMs = value * C.MILLIS_PER_SECOND
|
|
listeners.sendEvent(Player.EVENT_TIMELINE_CHANGED) { listener ->
|
|
listener.onTimelineChanged(
|
|
timeline,
|
|
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE
|
|
)
|
|
}
|
|
}
|
|
}
|
|
"demuxer-cache-time" -> currentCacheDurationMs = value * C.MILLIS_PER_SECOND
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun eventProperty(property: String, value: Double) {
|
|
handler.post {
|
|
when (property) {
|
|
"speed" -> {
|
|
playbackParameters = getPlaybackParameters().withSpeed(value.toFloat())
|
|
listeners.sendEvent(Player.EVENT_PLAYBACK_PARAMETERS_CHANGED) { listener ->
|
|
listener.onPlaybackParametersChanged(getPlaybackParameters())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@SuppressLint("SwitchIntDef")
|
|
override fun event(@MPVLib.Event eventId: Int) {
|
|
handler.post {
|
|
when (eventId) {
|
|
MPVLib.MPV_EVENT_START_FILE -> {
|
|
if (!isPlayerReady) {
|
|
for (command in initialCommands) {
|
|
MPVLib.command(command)
|
|
}
|
|
}
|
|
}
|
|
MPVLib.MPV_EVENT_SEEK -> {
|
|
setPlayerStateAndNotifyIfChanged(playbackState = Player.STATE_BUFFERING)
|
|
listeners.sendEvent(Player.EVENT_POSITION_DISCONTINUITY) { listener ->
|
|
@Suppress("DEPRECATION")
|
|
listener.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK)
|
|
}
|
|
}
|
|
MPVLib.MPV_EVENT_PLAYBACK_RESTART -> {
|
|
if (!isPlayerReady) {
|
|
isPlayerReady = true
|
|
listeners.sendEvent(Player.EVENT_TRACKS_CHANGED) { listener ->
|
|
//listener.onTracksChanged(currentTrackGroups, currentTrackSelections)
|
|
listener.onTracksInfoChanged(currentTracksInfo)
|
|
}
|
|
seekTo(C.TIME_UNSET)
|
|
if (playWhenReady) {
|
|
Log.d("mpv", "Starting playback...")
|
|
MPVLib.setPropertyBoolean("pause", false)
|
|
}
|
|
for (videoListener in videoListeners) {
|
|
videoListener.onRenderedFirstFrame()
|
|
}
|
|
} else {
|
|
if (playbackState == Player.STATE_BUFFERING && bufferedPosition > currentPosition) {
|
|
setPlayerStateAndNotifyIfChanged(playbackState = Player.STATE_READY)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun eventEndFile(@MPVLib.Reason reason: Int, @MPVLib.Error error: Int) {
|
|
// Nothing to do...
|
|
}
|
|
|
|
private fun setPlayerStateAndNotifyIfChanged(
|
|
playWhenReady: Boolean = getPlayWhenReady(),
|
|
@Player.PlayWhenReadyChangeReason playWhenReadyChangeReason: Int = Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
|
|
@Player.State playbackState: Int = getPlaybackState()
|
|
) {
|
|
var playerStateChanged = false
|
|
val wasPlaying = isPlaying
|
|
if (playbackState != getPlaybackState()) {
|
|
this.playbackState = playbackState
|
|
listeners.queueEvent(Player.EVENT_PLAYBACK_STATE_CHANGED) { listener ->
|
|
listener.onPlaybackStateChanged(playbackState)
|
|
}
|
|
playerStateChanged = true
|
|
}
|
|
if (playWhenReady != getPlayWhenReady()) {
|
|
this.currentPlayWhenReady = playWhenReady
|
|
listeners.queueEvent(Player.EVENT_PLAY_WHEN_READY_CHANGED) { listener ->
|
|
listener.onPlayWhenReadyChanged(playWhenReady, playWhenReadyChangeReason)
|
|
}
|
|
playerStateChanged = true
|
|
}
|
|
if (playerStateChanged) {
|
|
listeners.queueEvent( /* eventFlag= */ C.INDEX_UNSET) { listener ->
|
|
@Suppress("DEPRECATION")
|
|
listener.onPlayerStateChanged(playWhenReady, playbackState)
|
|
}
|
|
}
|
|
if (wasPlaying != isPlaying) {
|
|
listeners.queueEvent(Player.EVENT_IS_PLAYING_CHANGED) { listener ->
|
|
listener.onIsPlayingChanged(isPlaying)
|
|
}
|
|
}
|
|
listeners.flushEvents()
|
|
}
|
|
|
|
/**
|
|
* Select a [Track] or disable a [TrackType] in the current player.
|
|
*
|
|
* @param trackType The [TrackType]
|
|
* @param isExternal If track is external or embed in media
|
|
* @param index Index to select or [C.INDEX_UNSET] to disable [TrackType]
|
|
* @return true if the track is or was already selected
|
|
*/
|
|
fun selectTrack(
|
|
@TrackType trackType: String,
|
|
isExternal: Boolean = false,
|
|
index: Int
|
|
): Boolean {
|
|
if (index != C.INDEX_UNSET) {
|
|
Log.i("mpv", "${currentTracks.size}")
|
|
currentTracks.firstOrNull {
|
|
it.type == trackType && (if (isExternal) it.title else "${it.ffIndex}") == "$index"
|
|
}.let { track ->
|
|
if (track != null) {
|
|
Log.i("mpv", "selected track ${track.ffIndex} ${track.type}")
|
|
if (!track.selected) {
|
|
MPVLib.setPropertyInt(trackType, track.id)
|
|
}
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
} else {
|
|
if (currentTracks.indexOfFirst { it.type == trackType && it.selected } != C.INDEX_UNSET) {
|
|
MPVLib.setPropertyString(trackType, "no")
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Timeline wrapper
|
|
private val timeline: Timeline = object : Timeline() {
|
|
/**
|
|
* Returns the number of windows in the timeline.
|
|
*/
|
|
override fun getWindowCount(): Int {
|
|
return internalMediaItems?.size ?: 0
|
|
}
|
|
|
|
/**
|
|
* Populates a [com.google.android.exoplayer2.Timeline.Window] with data for the window at the specified index.
|
|
*
|
|
* @param windowIndex The index of the window.
|
|
* @param window The [com.google.android.exoplayer2.Timeline.Window] to populate. Must not be null.
|
|
* @param defaultPositionProjectionUs A duration into the future that the populated window's
|
|
* default start position should be projected.
|
|
* @return The populated [com.google.android.exoplayer2.Timeline.Window], for convenience.
|
|
*/
|
|
override fun getWindow(
|
|
windowIndex: Int,
|
|
window: Window,
|
|
defaultPositionProjectionUs: Long
|
|
): Window {
|
|
val currentMediaItem =
|
|
internalMediaItems?.get(windowIndex) ?: MediaItem.Builder().build()
|
|
return window.set(
|
|
/* uid= */ windowIndex,
|
|
/* mediaItem= */ currentMediaItem,
|
|
/* manifest= */ null,
|
|
/* presentationStartTimeMs= */ C.TIME_UNSET,
|
|
/* windowStartTimeMs= */ C.TIME_UNSET,
|
|
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
|
|
/* isSeekable= */ isSeekable,
|
|
/* isDynamic= */ !isSeekable,
|
|
/* liveConfiguration= */ currentMediaItem.liveConfiguration,
|
|
/* defaultPositionUs= */ C.TIME_UNSET,
|
|
/* durationUs= */ Util.msToUs(currentDurationMs ?: C.TIME_UNSET),
|
|
/* firstPeriodIndex= */ windowIndex,
|
|
/* lastPeriodIndex= */ windowIndex,
|
|
/* positionInFirstPeriodUs= */ C.TIME_UNSET
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Returns the number of periods in the timeline.
|
|
*/
|
|
override fun getPeriodCount(): Int {
|
|
return internalMediaItems?.size ?: 0
|
|
}
|
|
|
|
/**
|
|
* Populates a [com.google.android.exoplayer2.Timeline.Period] with data for the period at the specified index.
|
|
*
|
|
* @param periodIndex The index of the period.
|
|
* @param period The [com.google.android.exoplayer2.Timeline.Period] to populate. Must not be null.
|
|
* @param setIds Whether [com.google.android.exoplayer2.Timeline.Period.id] and [com.google.android.exoplayer2.Timeline.Period.uid] should be populated. If false,
|
|
* the fields will be set to null. The caller should pass false for efficiency reasons unless
|
|
* the fields are required.
|
|
* @return The populated [com.google.android.exoplayer2.Timeline.Period], for convenience.
|
|
*/
|
|
override fun getPeriod(periodIndex: Int, period: Period, setIds: Boolean): Period {
|
|
return period.set(
|
|
/* id= */ periodIndex,
|
|
/* uid= */ periodIndex,
|
|
/* windowIndex= */ periodIndex,
|
|
/* durationUs= */ Util.msToUs(currentDurationMs ?: C.TIME_UNSET),
|
|
/* positionInWindowUs= */ 0
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Returns the index of the period identified by its unique [com.google.android.exoplayer2.Timeline.Period.uid], or [ ][C.INDEX_UNSET] if the period is not in the timeline.
|
|
*
|
|
* @param uid A unique identifier for a period.
|
|
* @return The index of the period, or [C.INDEX_UNSET] if the period was not found.
|
|
*/
|
|
override fun getIndexOfPeriod(uid: Any): Int {
|
|
return uid as Int
|
|
}
|
|
|
|
/**
|
|
* Returns the unique id of the period identified by its index in the timeline.
|
|
*
|
|
* @param periodIndex The index of the period.
|
|
* @return The unique id of the period.
|
|
*/
|
|
override fun getUidOfPeriod(periodIndex: Int): Any {
|
|
return periodIndex
|
|
}
|
|
}
|
|
|
|
// OnAudioFocusChangeListener implementation.
|
|
|
|
/**
|
|
* Called on the listener to notify it the audio focus for this listener has been changed.
|
|
* The focusChange value indicates whether the focus was gained,
|
|
* whether the focus was lost, and whether that loss is transient, or whether the new focus
|
|
* holder will hold it for an unknown amount of time.
|
|
* When losing focus, listeners can use the focus change information to decide what
|
|
* behavior to adopt when losing focus. A music player could for instance elect to lower
|
|
* the volume of its music stream (duck) for transient focus losses, and pause otherwise.
|
|
* @param focusChange the type of focus change, one of [AudioManager.AUDIOFOCUS_GAIN],
|
|
* [AudioManager.AUDIOFOCUS_LOSS], [AudioManager.AUDIOFOCUS_LOSS_TRANSIENT]
|
|
* and [AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK].
|
|
*/
|
|
override fun onAudioFocusChange(focusChange: Int) {
|
|
when (focusChange) {
|
|
AudioManager.AUDIOFOCUS_LOSS,
|
|
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
|
val oldAudioFocusCallback = audioFocusCallback
|
|
val wasPlaying = isPlaying
|
|
MPVLib.setPropertyBoolean("pause", true)
|
|
setPlayerStateAndNotifyIfChanged(
|
|
playWhenReady = false,
|
|
playWhenReadyChangeReason = Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS
|
|
)
|
|
audioFocusCallback = {
|
|
oldAudioFocusCallback()
|
|
if (wasPlaying) MPVLib.setPropertyBoolean("pause", false)
|
|
}
|
|
}
|
|
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
|
MPVLib.command(arrayOf("multiply", "volume", "$AUDIO_FOCUS_DUCKING"))
|
|
audioFocusCallback = {
|
|
MPVLib.command(arrayOf("multiply", "volume", "${1f / AUDIO_FOCUS_DUCKING}"))
|
|
}
|
|
}
|
|
AudioManager.AUDIOFOCUS_GAIN -> {
|
|
audioFocusCallback()
|
|
audioFocusCallback = {}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Player implementation.
|
|
|
|
/**
|
|
* Returns the [Looper] associated with the application thread that's used to access the
|
|
* player and on which player events are received.
|
|
*/
|
|
override fun getApplicationLooper(): Looper {
|
|
return handler.looper
|
|
}
|
|
|
|
/**
|
|
* Registers a listener to receive all events from the player.
|
|
*
|
|
* @param listener The listener to register.
|
|
*/
|
|
override fun addListener(listener: Player.Listener) {
|
|
listeners.add(listener)
|
|
videoListeners.add(listener)
|
|
}
|
|
|
|
/**
|
|
* Unregister a listener registered through [.addListener]. The listener will no
|
|
* longer receive events.
|
|
*
|
|
* @param listener The listener to unregister.
|
|
*/
|
|
override fun removeListener(listener: Player.Listener) {
|
|
listeners.remove(listener)
|
|
videoListeners.remove(listener)
|
|
}
|
|
|
|
/**
|
|
* Clears the playlist and adds the specified [MediaItems][MediaItem].
|
|
*
|
|
* @param mediaItems The new [MediaItems][MediaItem].
|
|
* @param resetPosition Whether the playback position should be reset to the default position in
|
|
* the first [Timeline.Window]. If false, playback will start from the position defined
|
|
* by [.getCurrentWindowIndex] and [.getCurrentPosition].
|
|
*/
|
|
override fun setMediaItems(mediaItems: MutableList<MediaItem>, resetPosition: Boolean) {
|
|
internalMediaItems = mediaItems
|
|
}
|
|
|
|
/**
|
|
* Clears the playlist and adds the specified [MediaItems][MediaItem].
|
|
*
|
|
* @param mediaItems The new [MediaItems][MediaItem].
|
|
* @param startWindowIndex The window index to start playback from. If [com.google.android.exoplayer2.C.INDEX_UNSET] is
|
|
* passed, the current position is not reset.
|
|
* @param startPositionMs The position in milliseconds to start playback from. If [ ][com.google.android.exoplayer2.C.TIME_UNSET] is passed, the default position of the given window is used. In any case, if
|
|
* `startWindowIndex` is set to [com.google.android.exoplayer2.C.INDEX_UNSET], this parameter is ignored and the
|
|
* position is not reset at all.
|
|
* @throws com.google.android.exoplayer2.IllegalSeekPositionException If the provided `startWindowIndex` is not within the
|
|
* bounds of the list of media items.
|
|
*/
|
|
override fun setMediaItems(
|
|
mediaItems: MutableList<MediaItem>,
|
|
startWindowIndex: Int,
|
|
startPositionMs: Long
|
|
) {
|
|
internalMediaItems = mediaItems
|
|
currentIndex = startWindowIndex
|
|
initialSeekTo = startPositionMs / 1000
|
|
}
|
|
|
|
/**
|
|
* Adds a list of media items at the given index of the playlist.
|
|
*
|
|
* @param index The index at which to add the media items. If the index is larger than the size of
|
|
* the playlist, the media items are added to the end of the playlist.
|
|
* @param mediaItems The [MediaItems][MediaItem] to add.
|
|
*/
|
|
override fun addMediaItems(index: Int, mediaItems: MutableList<MediaItem>) {
|
|
TODO("Not yet implemented")
|
|
}
|
|
|
|
/**
|
|
* Moves the media item range to the new index.
|
|
*
|
|
* @param fromIndex The start of the range to move.
|
|
* @param toIndex The first item not to be included in the range (exclusive).
|
|
* @param newIndex The new index of the first media item of the range. If the new index is larger
|
|
* than the size of the remaining playlist after removing the range, the range is moved to the
|
|
* end of the playlist.
|
|
*/
|
|
override fun moveMediaItems(fromIndex: Int, toIndex: Int, newIndex: Int) {
|
|
TODO("Not yet implemented")
|
|
}
|
|
|
|
/**
|
|
* Removes a range of media items from the playlist.
|
|
*
|
|
* @param fromIndex The index at which to start removing media items.
|
|
* @param toIndex The index of the first item to be kept (exclusive). If the index is larger than
|
|
* the size of the playlist, media items to the end of the playlist are removed.
|
|
*/
|
|
override fun removeMediaItems(fromIndex: Int, toIndex: Int) {
|
|
TODO("Not yet implemented")
|
|
}
|
|
|
|
/**
|
|
* Returns the player's currently available [com.google.android.exoplayer2.Player.Commands].
|
|
*
|
|
*
|
|
* The returned [com.google.android.exoplayer2.Player.Commands] are not updated when available commands change. Use [ ][com.google.android.exoplayer2.Player.Listener.onAvailableCommandsChanged] to get an update when the available commands
|
|
* change.
|
|
*
|
|
*
|
|
* Executing a command that is not available (for example, calling [.next] if [ ][.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM] is unavailable) will neither throw an exception nor generate
|
|
* a [.getPlayerError] player error}.
|
|
*
|
|
*
|
|
* [.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM] and [.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM]
|
|
* are unavailable if there is no such [MediaItem].
|
|
*
|
|
* @return The currently available [com.google.android.exoplayer2.Player.Commands].
|
|
* @see com.google.android.exoplayer2.Player.Listener.onAvailableCommandsChanged
|
|
*/
|
|
override fun getAvailableCommands(): Commands {
|
|
return Commands.Builder()
|
|
.addAll(permanentAvailableCommands)
|
|
.addIf(COMMAND_SEEK_TO_DEFAULT_POSITION, !isPlayingAd)
|
|
.addIf(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, isCurrentMediaItemSeekable && !isPlayingAd)
|
|
.addIf(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, hasPreviousMediaItem() && !isPlayingAd)
|
|
.addIf(
|
|
COMMAND_SEEK_TO_PREVIOUS,
|
|
!currentTimeline.isEmpty
|
|
&& (hasPreviousMediaItem() || !isCurrentMediaItemLive || isCurrentMediaItemSeekable)
|
|
&& !isPlayingAd
|
|
)
|
|
.addIf(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, hasNextMediaItem() && !isPlayingAd)
|
|
.addIf(
|
|
COMMAND_SEEK_TO_NEXT,
|
|
!currentTimeline.isEmpty
|
|
&& (hasNextMediaItem() || (isCurrentMediaItemLive && isCurrentMediaItemDynamic))
|
|
&& !isPlayingAd
|
|
)
|
|
.addIf(COMMAND_SEEK_TO_MEDIA_ITEM, !isPlayingAd)
|
|
.addIf(COMMAND_SEEK_BACK, isCurrentMediaItemSeekable && !isPlayingAd)
|
|
.addIf(COMMAND_SEEK_FORWARD, isCurrentMediaItemSeekable && !isPlayingAd)
|
|
.build()
|
|
}
|
|
|
|
private fun resetInternalState() {
|
|
isPlayerReady = false
|
|
isSeekable = false
|
|
playbackState = Player.STATE_IDLE
|
|
currentPlayWhenReady = false
|
|
currentPositionMs = null
|
|
currentDurationMs = null
|
|
currentCacheDurationMs = null
|
|
tracksInfo = TracksInfo.EMPTY
|
|
playbackParameters = PlaybackParameters.DEFAULT
|
|
initialCommands.clear()
|
|
//initialSeekTo = 0L
|
|
}
|
|
|
|
/** Prepares the player. */
|
|
override fun prepare() {
|
|
internalMediaItems?.forEach { mediaItem ->
|
|
MPVLib.command(
|
|
arrayOf(
|
|
"loadfile",
|
|
"${mediaItem.localConfiguration?.uri}",
|
|
"append"
|
|
)
|
|
)
|
|
}
|
|
prepareMediaItem(currentIndex)
|
|
}
|
|
|
|
/**
|
|
* Returns the current [playback state][com.google.android.exoplayer2.Player.State] of the player.
|
|
*
|
|
* @return The current [playback state][com.google.android.exoplayer2.Player.State].
|
|
* @see com.google.android.exoplayer2.Player.Listener.onPlaybackStateChanged
|
|
*/
|
|
override fun getPlaybackState(): Int {
|
|
return playbackState
|
|
}
|
|
|
|
/**
|
|
* Returns the reason why playback is suppressed even though [.getPlayWhenReady] is `true`, or [.PLAYBACK_SUPPRESSION_REASON_NONE] if playback is not suppressed.
|
|
*
|
|
* @return The current [playback suppression reason][com.google.android.exoplayer2.Player.PlaybackSuppressionReason].
|
|
* @see com.google.android.exoplayer2.Player.Listener.onPlaybackSuppressionReasonChanged
|
|
*/
|
|
override fun getPlaybackSuppressionReason(): Int {
|
|
return PLAYBACK_SUPPRESSION_REASON_NONE
|
|
}
|
|
|
|
/**
|
|
* Returns the error that caused playback to fail. This is the same error that will have been
|
|
* reported via [com.google.android.exoplayer2.Player.Listener.onPlayerError] at the time of failure. It
|
|
* can be queried using this method until the player is re-prepared.
|
|
*
|
|
*
|
|
* Note that this method will always return `null` if [.getPlaybackState] is not
|
|
* [.STATE_IDLE].
|
|
*
|
|
* @return The error, or `null`.
|
|
* @see com.google.android.exoplayer2.Player.Listener.onPlayerError
|
|
*/
|
|
override fun getPlayerError(): ExoPlaybackException? {
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Sets whether playback should proceed when [.getPlaybackState] == [.STATE_READY].
|
|
*
|
|
*
|
|
* If the player is already in the ready state then this method pauses and resumes playback.
|
|
*
|
|
* @param playWhenReady Whether playback should proceed when ready.
|
|
*/
|
|
override fun setPlayWhenReady(playWhenReady: Boolean) {
|
|
if (currentPlayWhenReady != playWhenReady) {
|
|
setPlayerStateAndNotifyIfChanged(
|
|
playWhenReady = playWhenReady,
|
|
playWhenReadyChangeReason = Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST
|
|
)
|
|
if (isPlayerReady) {
|
|
MPVLib.setPropertyBoolean("pause", !playWhenReady)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Whether playback will proceed when [.getPlaybackState] == [.STATE_READY].
|
|
*
|
|
* @return Whether playback will proceed when ready.
|
|
* @see com.google.android.exoplayer2.Player.Listener.onPlayWhenReadyChanged
|
|
*/
|
|
override fun getPlayWhenReady(): Boolean {
|
|
return currentPlayWhenReady
|
|
}
|
|
|
|
/**
|
|
* Sets the [com.google.android.exoplayer2.Player.RepeatMode] to be used for playback.
|
|
*
|
|
* @param repeatMode The repeat mode.
|
|
*/
|
|
override fun setRepeatMode(repeatMode: Int) {
|
|
TODO("Not yet implemented")
|
|
}
|
|
|
|
/**
|
|
* Returns the current [com.google.android.exoplayer2.Player.RepeatMode] used for playback.
|
|
*
|
|
* @return The current repeat mode.
|
|
* @see com.google.android.exoplayer2.Player.Listener.onRepeatModeChanged
|
|
*/
|
|
override fun getRepeatMode(): Int {
|
|
return repeatMode
|
|
}
|
|
|
|
/**
|
|
* Sets whether shuffling of windows is enabled.
|
|
*
|
|
* @param shuffleModeEnabled Whether shuffling is enabled.
|
|
*/
|
|
override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) {
|
|
TODO("Not yet implemented")
|
|
}
|
|
|
|
/**
|
|
* Returns whether shuffling of windows is enabled.
|
|
*
|
|
* @see com.google.android.exoplayer2.Player.Listener.onShuffleModeEnabledChanged
|
|
*/
|
|
override fun getShuffleModeEnabled(): Boolean {
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Whether the player is currently loading the source.
|
|
*
|
|
* @return Whether the player is currently loading the source.
|
|
* @see com.google.android.exoplayer2.Player.Listener.onIsLoadingChanged
|
|
*/
|
|
override fun isLoading(): Boolean {
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Seeks to a position specified in milliseconds in the specified window.
|
|
*
|
|
* @param windowIndex The index of the window.
|
|
* @param positionMs The seek position in the specified window, or [com.google.android.exoplayer2.C.TIME_UNSET] to seek to
|
|
* the window's default position.
|
|
* @throws com.google.android.exoplayer2.IllegalSeekPositionException If the player has a non-empty timeline and the provided
|
|
* `windowIndex` is not within the bounds of the current timeline.
|
|
*/
|
|
override fun seekTo(windowIndex: Int, positionMs: Long) {
|
|
if (windowIndex == currentMediaItemIndex) {
|
|
val seekTo =
|
|
if (positionMs != C.TIME_UNSET) positionMs / C.MILLIS_PER_SECOND else initialSeekTo
|
|
initialSeekTo = if (isPlayerReady) {
|
|
MPVLib.command(arrayOf("seek", "$seekTo", "absolute"))
|
|
0L
|
|
} else {
|
|
seekTo
|
|
}
|
|
} else {
|
|
prepareMediaItem(windowIndex)
|
|
play()
|
|
}
|
|
}
|
|
|
|
private fun prepareMediaItem(index: Int) {
|
|
internalMediaItems?.get(index)?.let { mediaItem ->
|
|
internalMediaItem = mediaItem
|
|
resetInternalState()
|
|
mediaItem.localConfiguration?.subtitleConfigurations?.forEach { subtitle ->
|
|
initialCommands.add(
|
|
arrayOf(
|
|
/* command= */ "sub-add",
|
|
/* url= */ "${subtitle.uri}",
|
|
/* flags= */ "auto",
|
|
/* title= */ "${subtitle.label}",
|
|
/* lang= */ "${subtitle.language}"
|
|
)
|
|
)
|
|
}
|
|
currentIndex = index
|
|
MPVLib.command(arrayOf("playlist-play-index", "$index"))
|
|
MPVLib.setPropertyBoolean("pause", true)
|
|
listeners.sendEvent(Player.EVENT_TIMELINE_CHANGED) { listener ->
|
|
listener.onTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)
|
|
}
|
|
listeners.sendEvent(Player.EVENT_MEDIA_ITEM_TRANSITION) { listener ->
|
|
listener.onMediaItemTransition(
|
|
mediaItem,
|
|
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED
|
|
)
|
|
}
|
|
setPlayerStateAndNotifyIfChanged(playbackState = Player.STATE_BUFFERING)
|
|
}
|
|
}
|
|
|
|
override fun getSeekBackIncrement(): Long {
|
|
return C.DEFAULT_SEEK_BACK_INCREMENT_MS
|
|
}
|
|
|
|
override fun getSeekForwardIncrement(): Long {
|
|
return C.DEFAULT_SEEK_FORWARD_INCREMENT_MS
|
|
}
|
|
|
|
override fun getMaxSeekToPreviousPosition(): Long {
|
|
return C.DEFAULT_MAX_SEEK_TO_PREVIOUS_POSITION_MS
|
|
}
|
|
|
|
/**
|
|
* Attempts to set the playback parameters. Passing [PlaybackParameters.DEFAULT] resets the
|
|
* player to the default, which means there is no speed or pitch adjustment.
|
|
*
|
|
*
|
|
* Playback parameters changes may cause the player to buffer. [ ][com.google.android.exoplayer2.Player.Listener.onPlaybackParametersChanged] will be called whenever the currently
|
|
* active playback parameters change.
|
|
*
|
|
* @param playbackParameters The playback parameters.
|
|
*/
|
|
override fun setPlaybackParameters(playbackParameters: PlaybackParameters) {
|
|
if (getPlaybackParameters().speed != playbackParameters.speed) {
|
|
MPVLib.setPropertyDouble("speed", playbackParameters.speed.toDouble())
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the currently active playback parameters.
|
|
*
|
|
* @see com.google.android.exoplayer2.Player.Listener.onPlaybackParametersChanged
|
|
*/
|
|
override fun getPlaybackParameters(): PlaybackParameters {
|
|
return playbackParameters
|
|
}
|
|
|
|
override fun stop() {
|
|
MPVLib.command(arrayOf("stop", "keep-playlist"))
|
|
}
|
|
|
|
@Deprecated("Deprecated in Java")
|
|
override fun stop(reset: Boolean) {
|
|
MPVLib.command(arrayOf("stop", "keep-playlist"))
|
|
}
|
|
|
|
/**
|
|
* Releases the player. This method must be called when the player is no longer required. The
|
|
* player must not be used after calling this method.
|
|
*/
|
|
override fun release() {
|
|
if (audioFocusRequest == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
|
@Suppress("DEPRECATION")
|
|
audioManager.abandonAudioFocus(this)
|
|
}
|
|
resetInternalState()
|
|
MPVLib.destroy()
|
|
currentIndex = 0
|
|
}
|
|
|
|
/**
|
|
* Returns the available track groups.
|
|
*
|
|
* @see com.google.android.exoplayer2.Player.Listener.onTracksChanged
|
|
*/
|
|
@Deprecated("Deprecated in Java")
|
|
override fun getCurrentTrackGroups(): TrackGroupArray {
|
|
return TrackGroupArray.EMPTY
|
|
}
|
|
|
|
/**
|
|
* Returns the current track selections.
|
|
*
|
|
*
|
|
* A concrete implementation may include null elements if it has a fixed number of renderer
|
|
* components, wishes to report a TrackSelection for each of them, and has one or more renderer
|
|
* components that is not assigned any selected tracks.
|
|
*
|
|
* @see com.google.android.exoplayer2.Player.Listener.onTracksChanged
|
|
*/
|
|
@Deprecated("Deprecated in Java")
|
|
override fun getCurrentTrackSelections(): TrackSelectionArray {
|
|
return TrackSelectionArray()
|
|
}
|
|
|
|
override fun getCurrentTracksInfo(): TracksInfo {
|
|
return tracksInfo
|
|
}
|
|
|
|
override fun getTrackSelectionParameters(): TrackSelectionParameters {
|
|
TODO("Not yet implemented")
|
|
}
|
|
|
|
override fun setTrackSelectionParameters(parameters: TrackSelectionParameters) {
|
|
TODO("Not yet implemented")
|
|
}
|
|
|
|
/**
|
|
* Returns the current combined [MediaMetadata], or [MediaMetadata.EMPTY] if not
|
|
* supported.
|
|
*
|
|
*
|
|
* This [MediaMetadata] is a combination of the [MediaItem.mediaMetadata] and the
|
|
* static and dynamic metadata sourced from [com.google.android.exoplayer2.Player.Listener.onMediaMetadataChanged] and
|
|
* [com.google.android.exoplayer2.metadata.MetadataOutput.onMetadata].
|
|
*/
|
|
override fun getMediaMetadata(): MediaMetadata {
|
|
return MediaMetadata.EMPTY
|
|
}
|
|
|
|
override fun getPlaylistMetadata(): MediaMetadata {
|
|
TODO("Not yet implemented")
|
|
}
|
|
|
|
override fun setPlaylistMetadata(mediaMetadata: MediaMetadata) {
|
|
TODO("Not yet implemented")
|
|
}
|
|
|
|
/**
|
|
* Returns the current [Timeline]. Never null, but may be empty.
|
|
*
|
|
* @see com.google.android.exoplayer2.Player.Listener.onTimelineChanged
|
|
*/
|
|
override fun getCurrentTimeline(): Timeline {
|
|
return timeline
|
|
}
|
|
|
|
/** Returns the index of the period currently being played. */
|
|
override fun getCurrentPeriodIndex(): Int {
|
|
return currentMediaItemIndex
|
|
}
|
|
|
|
override fun getCurrentMediaItemIndex(): Int {
|
|
return currentIndex
|
|
}
|
|
|
|
/**
|
|
* Returns the duration of the current content window or ad in milliseconds, or [ ][com.google.android.exoplayer2.C.TIME_UNSET] if the duration is not known.
|
|
*/
|
|
override fun getDuration(): Long {
|
|
return timeline.getWindow(currentMediaItemIndex, window).durationMs
|
|
}
|
|
|
|
/**
|
|
* Returns the playback position in the current content window or ad, in milliseconds, or the
|
|
* prospective position in milliseconds if the [current timeline][.getCurrentTimeline] is
|
|
* empty.
|
|
*/
|
|
override fun getCurrentPosition(): Long {
|
|
return currentPositionMs ?: C.TIME_UNSET
|
|
}
|
|
|
|
/**
|
|
* Returns an estimate of the position in the current content window or ad up to which data is
|
|
* buffered, in milliseconds.
|
|
*/
|
|
override fun getBufferedPosition(): Long {
|
|
return currentCacheDurationMs ?: contentPosition
|
|
}
|
|
|
|
/**
|
|
* Returns an estimate of the total buffered duration from the current position, in milliseconds.
|
|
* This includes pre-buffered data for subsequent ads and windows.
|
|
*/
|
|
override fun getTotalBufferedDuration(): Long {
|
|
return bufferedPosition
|
|
}
|
|
|
|
/** Returns whether the player is currently playing an ad. */
|
|
override fun isPlayingAd(): Boolean {
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* If [.isPlayingAd] returns true, returns the index of the ad group in the period
|
|
* currently being played. Returns [com.google.android.exoplayer2.C.INDEX_UNSET] otherwise.
|
|
*/
|
|
override fun getCurrentAdGroupIndex(): Int {
|
|
return C.INDEX_UNSET
|
|
}
|
|
|
|
/**
|
|
* If [.isPlayingAd] returns true, returns the index of the ad in its ad group. Returns
|
|
* [com.google.android.exoplayer2.C.INDEX_UNSET] otherwise.
|
|
*/
|
|
override fun getCurrentAdIndexInAdGroup(): Int {
|
|
return C.INDEX_UNSET
|
|
}
|
|
|
|
/**
|
|
* If [.isPlayingAd] returns `true`, returns the content position that will be
|
|
* played once all ads in the ad group have finished playing, in milliseconds. If there is no ad
|
|
* playing, the returned position is the same as that returned by [.getCurrentPosition].
|
|
*/
|
|
override fun getContentPosition(): Long {
|
|
return currentPosition
|
|
}
|
|
|
|
/**
|
|
* If [.isPlayingAd] returns `true`, returns an estimate of the content position in
|
|
* the current content window up to which data is buffered, in milliseconds. If there is no ad
|
|
* playing, the returned position is the same as that returned by [.getBufferedPosition].
|
|
*/
|
|
override fun getContentBufferedPosition(): Long {
|
|
return bufferedPosition
|
|
}
|
|
|
|
/** Returns the attributes for audio playback. */
|
|
override fun getAudioAttributes(): AudioAttributes {
|
|
return AudioAttributes.DEFAULT
|
|
}
|
|
|
|
/**
|
|
* Sets the audio volume, with 0 being silence and 1 being unity gain (signal unchanged).
|
|
*
|
|
* @param audioVolume Linear output gain to apply to all audio channels.
|
|
*/
|
|
override fun setVolume(audioVolume: Float) {
|
|
TODO("Not yet implemented")
|
|
}
|
|
|
|
/**
|
|
* Returns the audio volume, with 0 being silence and 1 being unity gain (signal unchanged).
|
|
*
|
|
* @return The linear gain applied to all audio channels.
|
|
*/
|
|
override fun getVolume(): Float {
|
|
TODO("Not yet implemented")
|
|
}
|
|
|
|
/**
|
|
* Clears any [Surface], [SurfaceHolder], [SurfaceView] or [TextureView]
|
|
* currently set on the player.
|
|
*/
|
|
override fun clearVideoSurface() {
|
|
TODO("Not yet implemented")
|
|
}
|
|
|
|
/**
|
|
* Clears the [Surface] onto which video is being rendered if it matches the one passed.
|
|
* Else does nothing.
|
|
*
|
|
* @param surface The surface to clear.
|
|
*/
|
|
override fun clearVideoSurface(surface: Surface?) {
|
|
TODO("Not yet implemented")
|
|
}
|
|
|
|
/**
|
|
* Sets the [Surface] onto which video will be rendered. The caller is responsible for
|
|
* tracking the lifecycle of the surface, and must clear the surface by calling `setVideoSurface(null)` if the surface is destroyed.
|
|
*
|
|
*
|
|
* If the surface is held by a [SurfaceView], [TextureView] or [ ] then it's recommended to use [.setVideoSurfaceView], [ ][.setVideoTextureView] or [.setVideoSurfaceHolder] rather than
|
|
* this method, since passing the holder allows the player to track the lifecycle of the surface
|
|
* automatically.
|
|
*
|
|
* @param surface The [Surface].
|
|
*/
|
|
override fun setVideoSurface(surface: Surface?) {
|
|
TODO("Not yet implemented")
|
|
}
|
|
|
|
/**
|
|
* Sets the [SurfaceHolder] that holds the [Surface] onto which video will be
|
|
* rendered. The player will track the lifecycle of the surface automatically.
|
|
*
|
|
* @param surfaceHolder The surface holder.
|
|
*/
|
|
override fun setVideoSurfaceHolder(surfaceHolder: SurfaceHolder?) {
|
|
TODO("Not yet implemented")
|
|
}
|
|
|
|
/**
|
|
* Clears the [SurfaceHolder] that holds the [Surface] onto which video is being
|
|
* rendered if it matches the one passed. Else does nothing.
|
|
*
|
|
* @param surfaceHolder The surface holder to clear.
|
|
*/
|
|
override fun clearVideoSurfaceHolder(surfaceHolder: SurfaceHolder?) {
|
|
TODO("Not yet implemented")
|
|
}
|
|
|
|
/**
|
|
* Sets the [SurfaceView] onto which video will be rendered. The player will track the
|
|
* lifecycle of the surface automatically.
|
|
*
|
|
* @param surfaceView The surface view.
|
|
*/
|
|
override fun setVideoSurfaceView(surfaceView: SurfaceView?) {
|
|
surfaceView?.holder?.addCallback(surfaceHolder)
|
|
}
|
|
|
|
/**
|
|
* Clears the [SurfaceView] onto which video is being rendered if it matches the one passed.
|
|
* Else does nothing.
|
|
*
|
|
* @param surfaceView The texture view to clear.
|
|
*/
|
|
override fun clearVideoSurfaceView(surfaceView: SurfaceView?) {
|
|
surfaceView?.holder?.removeCallback(surfaceHolder)
|
|
}
|
|
|
|
/**
|
|
* Sets the [TextureView] onto which video will be rendered. The player will track the
|
|
* lifecycle of the surface automatically.
|
|
*
|
|
* @param textureView The texture view.
|
|
*/
|
|
override fun setVideoTextureView(textureView: TextureView?) {
|
|
TODO("Not yet implemented")
|
|
}
|
|
|
|
/**
|
|
* Clears the [TextureView] onto which video is being rendered if it matches the one passed.
|
|
* Else does nothing.
|
|
*
|
|
* @param textureView The texture view to clear.
|
|
*/
|
|
override fun clearVideoTextureView(textureView: TextureView?) {
|
|
TODO("Not yet implemented")
|
|
}
|
|
|
|
/**
|
|
* Gets the size of the video.
|
|
*
|
|
*
|
|
* The video's width and height are `0` if there is no video or its size has not been
|
|
* determined yet.
|
|
*
|
|
* @see com.google.android.exoplayer2.Player.Listener.onVideoSizeChanged
|
|
*/
|
|
override fun getVideoSize(): VideoSize {
|
|
return VideoSize.UNKNOWN
|
|
}
|
|
|
|
/** Returns the current [Cues][Cue]. This list may be empty. */
|
|
override fun getCurrentCues(): MutableList<Cue> {
|
|
TODO("Not yet implemented")
|
|
}
|
|
|
|
/** Gets the device information. */
|
|
override fun getDeviceInfo(): DeviceInfo {
|
|
TODO("Not yet implemented")
|
|
}
|
|
|
|
/**
|
|
* Gets the current volume of the device.
|
|
*
|
|
*
|
|
* For devices with [local playback][DeviceInfo.PLAYBACK_TYPE_LOCAL], the volume returned
|
|
* by this method varies according to the current [stream type][com.google.android.exoplayer2.C.StreamType]. The stream
|
|
* type is determined by [AudioAttributes.usage] which can be converted to stream type with
|
|
* [Util.getStreamTypeForAudioUsage].
|
|
*
|
|
*
|
|
* For devices with [remote playback][DeviceInfo.PLAYBACK_TYPE_REMOTE], the volume of the
|
|
* remote device is returned.
|
|
*/
|
|
override fun getDeviceVolume(): Int {
|
|
TODO("Not yet implemented")
|
|
}
|
|
|
|
/** Gets whether the device is muted or not. */
|
|
override fun isDeviceMuted(): Boolean {
|
|
TODO("Not yet implemented")
|
|
}
|
|
|
|
/**
|
|
* Sets the volume of the device.
|
|
*
|
|
* @param volume The volume to set.
|
|
*/
|
|
override fun setDeviceVolume(volume: Int) {
|
|
throw IllegalArgumentException("You should use global volume controls. Check out AUDIO_SERVICE.")
|
|
}
|
|
|
|
/** Increases the volume of the device. */
|
|
override fun increaseDeviceVolume() {
|
|
throw IllegalArgumentException("You should use global volume controls. Check out AUDIO_SERVICE.")
|
|
}
|
|
|
|
/** Decreases the volume of the device. */
|
|
override fun decreaseDeviceVolume() {
|
|
throw IllegalArgumentException("You should use global volume controls. Check out AUDIO_SERVICE.")
|
|
}
|
|
|
|
/** Sets the mute state of the device. */
|
|
override fun setDeviceMuted(muted: Boolean) {
|
|
throw IllegalArgumentException("You should use global volume controls. Check out AUDIO_SERVICE.")
|
|
}
|
|
|
|
/*private class CurrentTrackSelection(
|
|
private val currentTrackGroup: TrackGroup,
|
|
private val index: Int
|
|
) : TrackSelection {
|
|
/**
|
|
* Returns an integer specifying the type of the selection, or [.TYPE_UNSET] if not
|
|
* specified.
|
|
*
|
|
*
|
|
* Track selection types are specific to individual applications, but should be defined
|
|
* starting from [.TYPE_CUSTOM_BASE] to ensure they don't conflict with any types that may
|
|
* be added to the library in the future.
|
|
*/
|
|
override fun getType(): Int {
|
|
return TrackSelection.TYPE_UNSET
|
|
}
|
|
|
|
/** Returns the [TrackGroup] to which the selected tracks belong. */
|
|
override fun getTrackGroup(): TrackGroup {
|
|
return currentTrackGroup
|
|
}
|
|
|
|
/** Returns the number of tracks in the selection. */
|
|
override fun length(): Int {
|
|
return if (index != C.INDEX_UNSET) 1 else 0
|
|
}
|
|
|
|
/**
|
|
* Returns the format of the track at a given index in the selection.
|
|
*
|
|
* @param index The index in the selection.
|
|
* @return The format of the selected track.
|
|
*/
|
|
override fun getFormat(index: Int): Format {
|
|
return currentTrackGroup.getFormat(index)
|
|
}
|
|
|
|
/**
|
|
* Returns the index in the track group of the track at a given index in the selection.
|
|
*
|
|
* @param index The index in the selection.
|
|
* @return The index of the selected track.
|
|
*/
|
|
override fun getIndexInTrackGroup(index: Int): Int {
|
|
return index
|
|
}
|
|
|
|
/**
|
|
* Returns the index in the selection of the track with the specified format. The format is
|
|
* located by identity so, for example, `selection.indexOf(selection.getFormat(index)) ==
|
|
* index` even if multiple selected tracks have formats that contain the same values.
|
|
*
|
|
* @param format The format.
|
|
* @return The index in the selection, or [C.INDEX_UNSET] if the track with the specified
|
|
* format is not part of the selection.
|
|
*/
|
|
override fun indexOf(format: Format): Int {
|
|
return currentTrackGroup.indexOf(format)
|
|
}
|
|
|
|
/**
|
|
* Returns the index in the selection of the track with the specified index in the track group.
|
|
*
|
|
* @param indexInTrackGroup The index in the track group.
|
|
* @return The index in the selection, or [C.INDEX_UNSET] if the track with the specified
|
|
* index is not part of the selection.
|
|
*/
|
|
override fun indexOf(indexInTrackGroup: Int): Int {
|
|
return indexInTrackGroup
|
|
}
|
|
}*/
|
|
|
|
companion object {
|
|
/**
|
|
* Fraction to which audio volume is ducked on loss of audio focus
|
|
*/
|
|
private const val AUDIO_FOCUS_DUCKING = 0.5f
|
|
|
|
private val permanentAvailableCommands: Commands = Commands.Builder()
|
|
.addAll(
|
|
COMMAND_PLAY_PAUSE,
|
|
COMMAND_SET_SPEED_AND_PITCH,
|
|
COMMAND_GET_CURRENT_MEDIA_ITEM,
|
|
COMMAND_GET_MEDIA_ITEMS_METADATA,
|
|
COMMAND_CHANGE_MEDIA_ITEMS,
|
|
COMMAND_SET_VIDEO_SURFACE
|
|
)
|
|
.build()
|
|
|
|
private val surfaceHolder: SurfaceHolder.Callback = object : SurfaceHolder.Callback {
|
|
/**
|
|
* This is called immediately after the surface is first created.
|
|
* Implementations of this should start up whatever rendering code
|
|
* they desire. Note that only one thread can ever draw into
|
|
* a [Surface], so you should not draw into the Surface here
|
|
* if your normal rendering will be in another thread.
|
|
*
|
|
* @param holder The SurfaceHolder whose surface is being created.
|
|
*/
|
|
override fun surfaceCreated(holder: SurfaceHolder) {
|
|
MPVLib.attachSurface(holder.surface)
|
|
MPVLib.setOptionString("force-window", "yes")
|
|
MPVLib.setOptionString("vo", "gpu")
|
|
}
|
|
|
|
/**
|
|
* This is called immediately after any structural changes (format or
|
|
* size) have been made to the surface. You should at this point update
|
|
* the imagery in the surface. This method is always called at least
|
|
* once, after [.surfaceCreated].
|
|
*
|
|
* @param holder The SurfaceHolder whose surface has changed.
|
|
* @param format The new [android.graphics.PixelFormat] of the surface.
|
|
* @param width The new width of the surface.
|
|
* @param height The new height of the surface.
|
|
*/
|
|
override fun surfaceChanged(
|
|
holder: SurfaceHolder,
|
|
format: Int,
|
|
width: Int,
|
|
height: Int
|
|
) {
|
|
MPVLib.setPropertyString("android-surface-size", "${width}x$height")
|
|
}
|
|
|
|
/**
|
|
* This is called immediately before a surface is being destroyed. After
|
|
* returning from this call, you should no longer try to access this
|
|
* surface. If you have a rendering thread that directly accesses
|
|
* the surface, you must ensure that thread is no longer touching the
|
|
* Surface before returning from this function.
|
|
*
|
|
* @param holder The SurfaceHolder whose surface is being destroyed.
|
|
*/
|
|
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
|
MPVLib.setOptionString("vo", "null")
|
|
MPVLib.setOptionString("force-window", "no")
|
|
MPVLib.detachSurface()
|
|
}
|
|
}
|
|
|
|
@Parcelize
|
|
data class Track(
|
|
val id: Int,
|
|
@TrackType val type: String,
|
|
val mimeType: String = when (type) {
|
|
TrackType.VIDEO -> MimeTypes.BASE_TYPE_VIDEO
|
|
TrackType.AUDIO -> MimeTypes.BASE_TYPE_AUDIO
|
|
TrackType.SUBTITLE -> MimeTypes.BASE_TYPE_TEXT
|
|
else -> ""
|
|
},
|
|
val title: String,
|
|
val lang: String,
|
|
val external: Boolean,
|
|
val selected: Boolean,
|
|
val externalFilename: String?,
|
|
val ffIndex: Int,
|
|
val codec: String,
|
|
val width: Int?,
|
|
val height: Int?
|
|
) : Parcelable {
|
|
fun toFormat(): Format {
|
|
return Format.Builder()
|
|
.setId(id)
|
|
.setContainerMimeType("$mimeType/$codec")
|
|
.setSampleMimeType("$mimeType/$codec")
|
|
.setCodecs(codec)
|
|
.setWidth(width ?: Format.NO_VALUE)
|
|
.setHeight(height ?: Format.NO_VALUE)
|
|
.build()
|
|
}
|
|
|
|
companion object {
|
|
fun fromJSON(json: JSONObject): Track {
|
|
return Track(
|
|
id = json.optInt("id"),
|
|
type = json.optString("type"),
|
|
title = json.optString("title"),
|
|
lang = json.optString("lang"),
|
|
external = json.getBoolean("external"),
|
|
selected = json.getBoolean("selected"),
|
|
externalFilename = json.optString("external-filename"),
|
|
ffIndex = json.optInt("ff-index"),
|
|
codec = json.optString("codec"),
|
|
width = json.optInt("demux-w").takeIf { it > 0 },
|
|
height = json.optInt("demux-h").takeIf { it > 0 }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun getMPVTracks(trackList: String): Pair<List<Track>, TracksInfo> {
|
|
val tracks = mutableListOf<Track>()
|
|
var tracksInfo = TracksInfo.EMPTY
|
|
val trackGroupInfos = mutableListOf<TracksInfo.TrackGroupInfo>()
|
|
|
|
val trackListVideo = mutableListOf<Format>()
|
|
val trackListAudio = mutableListOf<Format>()
|
|
val trackListText = mutableListOf<Format>()
|
|
var indexCurrentVideo: Int = C.INDEX_UNSET
|
|
var indexCurrentAudio: Int = C.INDEX_UNSET
|
|
var indexCurrentText: Int = C.INDEX_UNSET
|
|
try {
|
|
val emptyTrack = Track(
|
|
id = -1,
|
|
type = TrackType.SUBTITLE,
|
|
mimeType = MimeTypes.BASE_TYPE_TEXT,
|
|
title = "None",
|
|
lang = "",
|
|
external = false,
|
|
selected = false,
|
|
externalFilename = null,
|
|
ffIndex = -1,
|
|
codec = "",
|
|
width = null,
|
|
height = null
|
|
)
|
|
tracks.add(emptyTrack)
|
|
trackListText.add(emptyTrack.toFormat())
|
|
val currentTrackList = JSONArray(trackList)
|
|
for (index in 0 until currentTrackList.length()) {
|
|
val currentTrack = Track.fromJSON(currentTrackList.getJSONObject(index))
|
|
val currentFormat = currentTrack.toFormat()
|
|
when (currentTrack.type) {
|
|
TrackType.VIDEO -> {
|
|
tracks.add(currentTrack)
|
|
trackListVideo.add(currentFormat)
|
|
if (currentTrack.selected) {
|
|
indexCurrentVideo = trackListVideo.indexOf(currentFormat)
|
|
}
|
|
}
|
|
TrackType.AUDIO -> {
|
|
tracks.add(currentTrack)
|
|
trackListAudio.add(currentFormat)
|
|
if (currentTrack.selected) {
|
|
indexCurrentAudio = trackListAudio.indexOf(currentFormat)
|
|
}
|
|
}
|
|
TrackType.SUBTITLE -> {
|
|
tracks.add(currentTrack)
|
|
trackListText.add(currentFormat)
|
|
if (currentTrack.selected) {
|
|
indexCurrentText = trackListText.indexOf(currentFormat)
|
|
}
|
|
}
|
|
else -> continue
|
|
}
|
|
}
|
|
if (trackListText.size == 1 && trackListText[0].id == emptyTrack.id.toString()) {
|
|
tracks.remove(emptyTrack)
|
|
trackListText.removeFirst()
|
|
}
|
|
if (trackListVideo.isNotEmpty()) {
|
|
with(TrackGroup(*trackListVideo.toTypedArray())) {
|
|
TracksInfo.TrackGroupInfo(
|
|
this,
|
|
intArrayOf(C.FORMAT_HANDLED),
|
|
C.TRACK_TYPE_VIDEO,
|
|
BooleanArray(this.length) { it == indexCurrentVideo }
|
|
)
|
|
}
|
|
}
|
|
if (trackListAudio.isNotEmpty()) {
|
|
with(TrackGroup(*trackListAudio.toTypedArray())) {
|
|
TracksInfo.TrackGroupInfo(
|
|
this,
|
|
IntArray(this.length) { C.FORMAT_HANDLED },
|
|
C.TRACK_TYPE_AUDIO,
|
|
BooleanArray(this.length) { it == indexCurrentAudio }
|
|
)
|
|
}
|
|
}
|
|
if (trackListText.isNotEmpty()) {
|
|
with(TrackGroup(*trackListText.toTypedArray())) {
|
|
TracksInfo.TrackGroupInfo(
|
|
this,
|
|
IntArray(this.length) { C.FORMAT_HANDLED },
|
|
C.TRACK_TYPE_TEXT,
|
|
BooleanArray(this.length) { it == indexCurrentText }
|
|
)
|
|
}
|
|
}
|
|
if (trackGroupInfos.isNotEmpty()) {
|
|
tracksInfo = TracksInfo(trackGroupInfos)
|
|
}
|
|
} catch (e: JSONException) {
|
|
}
|
|
return Pair(tracks, tracksInfo)
|
|
}
|
|
|
|
/**
|
|
* Merges multiple [subtitleSources] into a single [videoSource]
|
|
*/
|
|
fun mergeMediaSources(
|
|
videoSource: MediaSource,
|
|
subtitleSources: Array<MediaSource>,
|
|
dataSource: DataSource.Factory
|
|
): MediaSource {
|
|
return when {
|
|
subtitleSources.isEmpty() -> videoSource
|
|
else -> {
|
|
val subtitleConfigurations = mutableListOf<MediaItem.SubtitleConfiguration>()
|
|
subtitleSources.forEach { subtitleSource ->
|
|
subtitleSource.mediaItem.localConfiguration?.subtitleConfigurations?.forEach { subtitle ->
|
|
subtitleConfigurations.add(subtitle)
|
|
}
|
|
}
|
|
ProgressiveMediaSource.Factory(dataSource)
|
|
.createMediaSource(
|
|
videoSource.mediaItem.buildUpon()
|
|
.setSubtitleConfigurations(subtitleConfigurations).build()
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|