Playlist support for MPV player (#77)

This commit is contained in:
maulik9898 2022-02-12 21:28:52 +05:30 committed by GitHub
parent 1afeb54802
commit f2b090d09b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 101 additions and 52 deletions

View file

@ -34,8 +34,6 @@ Home | Library | Movie | Season | Episode
- Audio codecs: Opus, FLAC, MP3, AAC, AC-3, E-AC-3, TrueHD, DTS, DTS-HD - Audio codecs: Opus, FLAC, MP3, AAC, AC-3, E-AC-3, TrueHD, DTS, DTS-HD
- Subtitle codecs: SRT, VTT, SSA/ASS, DVDSUB - Subtitle codecs: SRT, VTT, SSA/ASS, DVDSUB
- Optionally force software decoding when hardware decoding has issues. - Optionally force software decoding when hardware decoding has issues.
- Issues:
- Can only play one item at a time, doesn't transistion to the next episode
## Planned features ## Planned features

View file

@ -15,6 +15,7 @@ import android.view.SurfaceView
import android.view.TextureView import android.view.TextureView
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import com.google.android.exoplayer2.* import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.Player.Commands
import com.google.android.exoplayer2.audio.AudioAttributes import com.google.android.exoplayer2.audio.AudioAttributes
import com.google.android.exoplayer2.device.DeviceInfo import com.google.android.exoplayer2.device.DeviceInfo
import com.google.android.exoplayer2.metadata.Metadata import com.google.android.exoplayer2.metadata.Metadata
@ -34,7 +35,6 @@ import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.lang.IllegalArgumentException
import java.util.concurrent.CopyOnWriteArraySet import java.util.concurrent.CopyOnWriteArraySet
@Suppress("SpellCheckingInspection") @Suppress("SpellCheckingInspection")
@ -47,6 +47,7 @@ class MPVPlayer(
private val audioManager: AudioManager by lazy { context.getSystemService()!! } private val audioManager: AudioManager by lazy { context.getSystemService()!! }
private var audioFocusCallback: () -> Unit = {} private var audioFocusCallback: () -> Unit = {}
private var currentIndex = 0
private var audioFocusRequest = AudioManager.AUDIOFOCUS_REQUEST_FAILED private var audioFocusRequest = AudioManager.AUDIOFOCUS_REQUEST_FAILED
private val handler = Handler(context.mainLooper) private val handler = Handler(context.mainLooper)
@ -209,12 +210,18 @@ class MPVPlayer(
when (property) { when (property) {
"eof-reached" -> { "eof-reached" -> {
if (value && isPlayerReady) { if (value && isPlayerReady) {
setPlayerStateAndNotifyIfChanged( if (currentIndex < internalMediaItems?.size ?: 0) {
playWhenReady = false, currentIndex += 1
playWhenReadyChangeReason = Player.PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM, prepareMediaItem(currentIndex)
playbackState = Player.STATE_ENDED play()
) } else {
resetInternalState() setPlayerStateAndNotifyIfChanged(
playWhenReady = false,
playWhenReadyChangeReason = Player.PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM,
playbackState = Player.STATE_ENDED
)
resetInternalState()
}
} }
} }
"paused-for-cache" -> { "paused-for-cache" -> {
@ -386,7 +393,7 @@ class MPVPlayer(
* Returns the number of windows in the timeline. * Returns the number of windows in the timeline.
*/ */
override fun getWindowCount(): Int { override fun getWindowCount(): Int {
return 1 return internalMediaItems?.size ?: 0
} }
/** /**
@ -399,9 +406,10 @@ class MPVPlayer(
* @return The populated [com.google.android.exoplayer2.Timeline.Window], for convenience. * @return The populated [com.google.android.exoplayer2.Timeline.Window], for convenience.
*/ */
override fun getWindow(windowIndex: Int, window: Window, defaultPositionProjectionUs: Long): Window { override fun getWindow(windowIndex: Int, window: Window, defaultPositionProjectionUs: Long): Window {
val currentMediaItem = internalMediaItem ?: MediaItem.Builder().build() val currentMediaItem =
return if (windowIndex == 0) window.set( internalMediaItems?.get(windowIndex) ?: MediaItem.Builder().build()
/* uid= */ 0, return window.set(
/* uid= */ windowIndex,
/* mediaItem= */ currentMediaItem, /* mediaItem= */ currentMediaItem,
/* manifest= */ null, /* manifest= */ null,
/* presentationStartTimeMs= */ C.TIME_UNSET, /* presentationStartTimeMs= */ C.TIME_UNSET,
@ -415,14 +423,14 @@ class MPVPlayer(
/* firstPeriodIndex= */ windowIndex, /* firstPeriodIndex= */ windowIndex,
/* lastPeriodIndex= */ windowIndex, /* lastPeriodIndex= */ windowIndex,
/* positionInFirstPeriodUs= */ C.TIME_UNSET /* positionInFirstPeriodUs= */ C.TIME_UNSET
) else window )
} }
/** /**
* Returns the number of periods in the timeline. * Returns the number of periods in the timeline.
*/ */
override fun getPeriodCount(): Int { override fun getPeriodCount(): Int {
return 1 return internalMediaItems?.size ?: 0
} }
/** /**
@ -436,13 +444,13 @@ class MPVPlayer(
* @return The populated [com.google.android.exoplayer2.Timeline.Period], for convenience. * @return The populated [com.google.android.exoplayer2.Timeline.Period], for convenience.
*/ */
override fun getPeriod(periodIndex: Int, period: Period, setIds: Boolean): Period { override fun getPeriod(periodIndex: Int, period: Period, setIds: Boolean): Period {
return if (periodIndex == 0) period.set( return period.set(
/* id= */ 0, /* id= */ periodIndex,
/* uid= */ 0, /* uid= */ periodIndex,
/* windowIndex= */ periodIndex, /* windowIndex= */ periodIndex,
/* durationUs= */ C.msToUs(currentDurationMs ?: C.TIME_UNSET), /* durationUs= */ C.msToUs(currentDurationMs ?: C.TIME_UNSET),
/* positionInWindowUs= */ 0 /* positionInWindowUs= */ 0
) else period )
} }
/** /**
@ -452,7 +460,7 @@ class MPVPlayer(
* @return The index of the period, or [C.INDEX_UNSET] if the period was not found. * @return The index of the period, or [C.INDEX_UNSET] if the period was not found.
*/ */
override fun getIndexOfPeriod(uid: Any): Int { override fun getIndexOfPeriod(uid: Any): Int {
return if (uid == 0) 0 else C.INDEX_UNSET return uid as Int
} }
/** /**
@ -462,7 +470,7 @@ class MPVPlayer(
* @return The unique id of the period. * @return The unique id of the period.
*/ */
override fun getUidOfPeriod(periodIndex: Int): Any { override fun getUidOfPeriod(periodIndex: Int): Any {
return if (periodIndex == 0) 0 else C.INDEX_UNSET return periodIndex
} }
} }
@ -589,6 +597,7 @@ class MPVPlayer(
*/ */
override fun setMediaItems(mediaItems: MutableList<MediaItem>, startWindowIndex: Int, startPositionMs: Long) { override fun setMediaItems(mediaItems: MutableList<MediaItem>, startWindowIndex: Int, startPositionMs: Long) {
internalMediaItems = mediaItems internalMediaItems = mediaItems
currentIndex = startWindowIndex
initialSeekTo = startPositionMs / 1000 initialSeekTo = startPositionMs / 1000
} }
@ -646,9 +655,29 @@ class MPVPlayer(
* @see com.google.android.exoplayer2.Player.Listener.onAvailableCommandsChanged * @see com.google.android.exoplayer2.Player.Listener.onAvailableCommandsChanged
*/ */
override fun getAvailableCommands(): Player.Commands { override fun getAvailableCommands(): Player.Commands {
return permanentAvailableCommands return Commands.Builder()
.addAll(permanentAvailableCommands)
.addIf(COMMAND_SEEK_TO_DEFAULT_POSITION, !isPlayingAd)
.addIf(COMMAND_SEEK_IN_CURRENT_WINDOW, isCurrentWindowSeekable && !isPlayingAd)
.addIf(COMMAND_SEEK_TO_PREVIOUS_WINDOW, hasPreviousWindow() && !isPlayingAd)
.addIf(
COMMAND_SEEK_TO_PREVIOUS,
!currentTimeline.isEmpty
&& (hasPreviousWindow() || !isCurrentWindowLive || isCurrentWindowSeekable)
&& !isPlayingAd
)
.addIf(COMMAND_SEEK_TO_NEXT_WINDOW, hasNextWindow() && !isPlayingAd)
.addIf(
COMMAND_SEEK_TO_NEXT,
!currentTimeline.isEmpty()
&& (hasNextWindow() || (isCurrentWindowLive && isCurrentWindowDynamic()))
&& !isPlayingAd
)
.addIf(COMMAND_SEEK_TO_WINDOW, !isPlayingAd)
.addIf(COMMAND_SEEK_BACK, isCurrentWindowSeekable && !isPlayingAd)
.addIf(COMMAND_SEEK_FORWARD, isCurrentWindowSeekable && !isPlayingAd)
.build()
} }
private fun resetInternalState() { private fun resetInternalState() {
isPlayerReady = false isPlayerReady = false
isSeekable = false isSeekable = false
@ -666,28 +695,16 @@ class MPVPlayer(
/** Prepares the player. */ /** Prepares the player. */
override fun prepare() { override fun prepare() {
internalMediaItems?.firstOrNull { it.playbackProperties?.uri != null }?.let { mediaItem -> internalMediaItems?.forEach { mediaItem ->
internalMediaItem = mediaItem MPVLib.command(
resetInternalState() arrayOf(
mediaItem.playbackProperties?.subtitles?.forEach { subtitle -> "loadfile",
initialCommands.add(arrayOf( "${mediaItem.playbackProperties?.uri}",
/* command= */ "sub-add", "append"
/* url= */ "${subtitle.uri}", )
/* flags= */ "auto", )
/* title= */ "${subtitle.label}",
/* lang= */ "${subtitle.language}"
))
}
MPVLib.command(arrayOf("loadfile", "${mediaItem.playbackProperties?.uri}"))
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)
} }
prepareMediaItem(currentIndex)
} }
/** /**
@ -813,14 +830,49 @@ class MPVPlayer(
* `windowIndex` is not within the bounds of the current timeline. * `windowIndex` is not within the bounds of the current timeline.
*/ */
override fun seekTo(windowIndex: Int, positionMs: Long) { override fun seekTo(windowIndex: Int, positionMs: Long) {
if (windowIndex == 0) { if (windowIndex == currentWindowIndex) {
val seekTo = if (positionMs != C.TIME_UNSET) positionMs / C.MILLIS_PER_SECOND else initialSeekTo val seekTo =
if (positionMs != C.TIME_UNSET) positionMs / C.MILLIS_PER_SECOND else initialSeekTo
initialSeekTo = if (isPlayerReady) { initialSeekTo = if (isPlayerReady) {
MPVLib.command(arrayOf("seek", "$seekTo", "absolute")) MPVLib.command(arrayOf("seek", "$seekTo", "absolute"))
0L 0L
} else { } else {
seekTo seekTo
} }
} else {
prepareMediaItem(windowIndex)
play()
}
}
private fun prepareMediaItem(index: Int) {
internalMediaItems?.get(index)?.let { mediaItem ->
internalMediaItem = mediaItem
resetInternalState()
mediaItem.playbackProperties?.subtitles?.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)
} }
} }
@ -833,7 +885,7 @@ class MPVPlayer(
} }
override fun getMaxSeekToPreviousPosition(): Int { override fun getMaxSeekToPreviousPosition(): Int {
TODO("Not yet implemented") return C.DEFAULT_MAX_SEEK_TO_PREVIOUS_POSITION_MS
} }
/** /**
@ -876,6 +928,7 @@ class MPVPlayer(
} }
resetInternalState() resetInternalState()
MPVLib.destroy() MPVLib.destroy()
currentIndex = 0
} }
/** /**
@ -959,7 +1012,8 @@ class MPVPlayer(
* Returns the index of the current [window][Timeline.Window] in the [ ][.getCurrentTimeline], or the prospective window index if the [ ][.getCurrentTimeline] is empty. * Returns the index of the current [window][Timeline.Window] in the [ ][.getCurrentTimeline], or the prospective window index if the [ ][.getCurrentTimeline] is empty.
*/ */
override fun getCurrentWindowIndex(): Int { override fun getCurrentWindowIndex(): Int {
return timeline.getFirstWindowIndex(shuffleModeEnabled)
return currentIndex
} }
/** /**
@ -1299,15 +1353,12 @@ class MPVPlayer(
private val permanentAvailableCommands: Player.Commands = Player.Commands.Builder() private val permanentAvailableCommands: Player.Commands = Player.Commands.Builder()
.addAll( .addAll(
COMMAND_PLAY_PAUSE, COMMAND_PLAY_PAUSE,
COMMAND_SEEK_IN_CURRENT_WINDOW,
COMMAND_PREPARE_STOP, COMMAND_PREPARE_STOP,
COMMAND_SET_SPEED_AND_PITCH, COMMAND_SET_SPEED_AND_PITCH,
COMMAND_GET_CURRENT_MEDIA_ITEM, COMMAND_GET_CURRENT_MEDIA_ITEM,
COMMAND_GET_MEDIA_ITEMS_METADATA, COMMAND_GET_MEDIA_ITEMS_METADATA,
COMMAND_CHANGE_MEDIA_ITEMS, COMMAND_CHANGE_MEDIA_ITEMS,
COMMAND_SET_VIDEO_SURFACE, COMMAND_SET_VIDEO_SURFACE
COMMAND_SEEK_FORWARD,
COMMAND_SEEK_BACK
) )
.build() .build()