Upgrade ExoPlayer to 2.16.1 (#115)
* Upgrade ExoPlayer to 2.26.1 and fix mpv player * Re-enable ffmpeg extension
This commit is contained in:
parent
0b0bdab9d3
commit
a785d6d3f8
5 changed files with 133 additions and 133 deletions
|
@ -113,7 +113,7 @@ dependencies {
|
||||||
kapt("com.google.dagger:hilt-compiler:$hiltVersion")
|
kapt("com.google.dagger:hilt-compiler:$hiltVersion")
|
||||||
|
|
||||||
// ExoPlayer
|
// ExoPlayer
|
||||||
val exoplayerVersion = "2.15.1"
|
val exoplayerVersion = "2.16.1"
|
||||||
implementation("com.google.android.exoplayer:exoplayer-core:$exoplayerVersion")
|
implementation("com.google.android.exoplayer:exoplayer-core:$exoplayerVersion")
|
||||||
implementation("com.google.android.exoplayer:exoplayer-ui:$exoplayerVersion")
|
implementation("com.google.android.exoplayer:exoplayer-ui:$exoplayerVersion")
|
||||||
implementation(files("libs/extension-ffmpeg-release.aar"))
|
implementation(files("libs/extension-ffmpeg-release.aar"))
|
||||||
|
|
Binary file not shown.
|
@ -17,15 +17,13 @@ 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.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.metadata.Metadata
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource
|
import com.google.android.exoplayer2.source.MediaSource
|
||||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
||||||
import com.google.android.exoplayer2.source.TrackGroup
|
import com.google.android.exoplayer2.source.TrackGroup
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray
|
import com.google.android.exoplayer2.source.TrackGroupArray
|
||||||
import com.google.android.exoplayer2.text.Cue
|
import com.google.android.exoplayer2.text.Cue
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelection
|
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray
|
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.upstream.DataSource
|
||||||
import com.google.android.exoplayer2.util.*
|
import com.google.android.exoplayer2.util.*
|
||||||
import com.google.android.exoplayer2.video.VideoSize
|
import com.google.android.exoplayer2.video.VideoSize
|
||||||
|
@ -61,7 +59,8 @@ class MPVPlayer(
|
||||||
val file = File(mpvDir, fileName)
|
val file = File(mpvDir, fileName)
|
||||||
Log.i("mpv", "File ${file.absolutePath}")
|
Log.i("mpv", "File ${file.absolutePath}")
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
context.assets.open(fileName, AssetManager.ACCESS_STREAMING).copyTo(FileOutputStream(file))
|
context.assets.open(fileName, AssetManager.ACCESS_STREAMING)
|
||||||
|
.copyTo(FileOutputStream(file))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MPVLib.create(context)
|
MPVLib.create(context)
|
||||||
|
@ -144,26 +143,26 @@ class MPVPlayer(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listeners and notification.
|
// Listeners and notification.
|
||||||
@Suppress("DEPRECATION")
|
private val listeners: ListenerSet<Player.Listener> = ListenerSet(
|
||||||
private val listeners: ListenerSet<Player.EventListener> = ListenerSet(
|
|
||||||
context.mainLooper,
|
context.mainLooper,
|
||||||
Clock.DEFAULT
|
Clock.DEFAULT
|
||||||
) { listener: Player.EventListener, flags: FlagSet ->
|
) { listener: Player.Listener, flags: FlagSet ->
|
||||||
listener.onEvents( /* player= */this, Player.Events(flags))
|
listener.onEvents( /* player= */this, Player.Events(flags))
|
||||||
}
|
}
|
||||||
@Suppress("DEPRECATION")
|
private val videoListeners =
|
||||||
private val videoListeners = CopyOnWriteArraySet<com.google.android.exoplayer2.video.VideoListener>()
|
CopyOnWriteArraySet<Player.Listener>()
|
||||||
|
|
||||||
// Internal state.
|
// Internal state.
|
||||||
private var internalMediaItems: List<MediaItem>? = null
|
private var internalMediaItems: List<MediaItem>? = null
|
||||||
private var internalMediaItem: MediaItem? = null
|
private var internalMediaItem: MediaItem? = null
|
||||||
|
|
||||||
@Player.State
|
@Player.State
|
||||||
private var playbackState: Int = Player.STATE_IDLE
|
private var playbackState: Int = Player.STATE_IDLE
|
||||||
private var currentPlayWhenReady: Boolean = false
|
private var currentPlayWhenReady: Boolean = false
|
||||||
|
|
||||||
@Player.RepeatMode
|
@Player.RepeatMode
|
||||||
private val repeatMode: Int = REPEAT_MODE_OFF
|
private val repeatMode: Int = REPEAT_MODE_OFF
|
||||||
private var trackGroupArray: TrackGroupArray = TrackGroupArray.EMPTY
|
private var tracksInfo: TracksInfo = TracksInfo.EMPTY
|
||||||
private var trackSelectionArray: TrackSelectionArray = TrackSelectionArray()
|
|
||||||
private var playbackParameters: PlaybackParameters = PlaybackParameters.DEFAULT
|
private var playbackParameters: PlaybackParameters = PlaybackParameters.DEFAULT
|
||||||
|
|
||||||
// MPV Custom
|
// MPV Custom
|
||||||
|
@ -185,20 +184,18 @@ class MPVPlayer(
|
||||||
handler.post {
|
handler.post {
|
||||||
when (property) {
|
when (property) {
|
||||||
"track-list" -> {
|
"track-list" -> {
|
||||||
val (tracks, newTrackGroupArray, newTrackSelectionArray) = getMPVTracks(value)
|
val (tracks, newTracksInfo) = getMPVTracks(value)
|
||||||
tracks.forEach { Log.i("mpv", "${it.ffIndex} ${it.type} ${it.codec}") }
|
tracks.forEach { Log.i("mpv", "${it.ffIndex} ${it.type} ${it.codec}") }
|
||||||
currentTracks = tracks
|
currentTracks = tracks
|
||||||
if (isPlayerReady) {
|
if (isPlayerReady) {
|
||||||
if (newTrackGroupArray != trackGroupArray || newTrackSelectionArray != trackSelectionArray) {
|
if (newTracksInfo != tracksInfo) {
|
||||||
trackGroupArray = newTrackGroupArray
|
tracksInfo = newTracksInfo
|
||||||
trackSelectionArray = newTrackSelectionArray
|
|
||||||
listeners.sendEvent(Player.EVENT_TRACKS_CHANGED) { listener ->
|
listeners.sendEvent(Player.EVENT_TRACKS_CHANGED) { listener ->
|
||||||
listener.onTracksChanged(currentTrackGroups, currentTrackSelections)
|
listener.onTracksInfoChanged(currentTracksInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
trackGroupArray = newTrackGroupArray
|
tracksInfo = newTracksInfo
|
||||||
trackSelectionArray = newTrackSelectionArray
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -237,7 +234,10 @@ class MPVPlayer(
|
||||||
if (isSeekable != value) {
|
if (isSeekable != value) {
|
||||||
isSeekable = value
|
isSeekable = value
|
||||||
listeners.sendEvent(Player.EVENT_TIMELINE_CHANGED) { listener ->
|
listeners.sendEvent(Player.EVENT_TIMELINE_CHANGED) { listener ->
|
||||||
listener.onTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
|
listener.onTimelineChanged(
|
||||||
|
timeline,
|
||||||
|
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -253,7 +253,10 @@ class MPVPlayer(
|
||||||
if (currentDurationMs != value * C.MILLIS_PER_SECOND) {
|
if (currentDurationMs != value * C.MILLIS_PER_SECOND) {
|
||||||
currentDurationMs = value * C.MILLIS_PER_SECOND
|
currentDurationMs = value * C.MILLIS_PER_SECOND
|
||||||
listeners.sendEvent(Player.EVENT_TIMELINE_CHANGED) { listener ->
|
listeners.sendEvent(Player.EVENT_TIMELINE_CHANGED) { listener ->
|
||||||
listener.onTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
|
listener.onTimelineChanged(
|
||||||
|
timeline,
|
||||||
|
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -267,7 +270,7 @@ class MPVPlayer(
|
||||||
when (property) {
|
when (property) {
|
||||||
"speed" -> {
|
"speed" -> {
|
||||||
playbackParameters = getPlaybackParameters().withSpeed(value.toFloat())
|
playbackParameters = getPlaybackParameters().withSpeed(value.toFloat())
|
||||||
listeners.sendEvent(Player.EVENT_PLAYBACK_PARAMETERS_CHANGED) {listener ->
|
listeners.sendEvent(Player.EVENT_PLAYBACK_PARAMETERS_CHANGED) { listener ->
|
||||||
listener.onPlaybackParametersChanged(getPlaybackParameters())
|
listener.onPlaybackParametersChanged(getPlaybackParameters())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -297,7 +300,8 @@ class MPVPlayer(
|
||||||
if (!isPlayerReady) {
|
if (!isPlayerReady) {
|
||||||
isPlayerReady = true
|
isPlayerReady = true
|
||||||
listeners.sendEvent(Player.EVENT_TRACKS_CHANGED) { listener ->
|
listeners.sendEvent(Player.EVENT_TRACKS_CHANGED) { listener ->
|
||||||
listener.onTracksChanged(currentTrackGroups, currentTrackSelections)
|
//listener.onTracksChanged(currentTrackGroups, currentTrackSelections)
|
||||||
|
listener.onTracksInfoChanged(currentTracksInfo)
|
||||||
}
|
}
|
||||||
seekTo(C.TIME_UNSET)
|
seekTo(C.TIME_UNSET)
|
||||||
if (playWhenReady) {
|
if (playWhenReady) {
|
||||||
|
@ -364,7 +368,11 @@ class MPVPlayer(
|
||||||
* @param index Index to select or [C.INDEX_UNSET] to disable [TrackType]
|
* @param index Index to select or [C.INDEX_UNSET] to disable [TrackType]
|
||||||
* @return true if the track is or was already selected
|
* @return true if the track is or was already selected
|
||||||
*/
|
*/
|
||||||
fun selectTrack(@TrackType trackType: String, isExternal: Boolean = false, index: Int): Boolean {
|
fun selectTrack(
|
||||||
|
@TrackType trackType: String,
|
||||||
|
isExternal: Boolean = false,
|
||||||
|
index: Int
|
||||||
|
): Boolean {
|
||||||
if (index != C.INDEX_UNSET) {
|
if (index != C.INDEX_UNSET) {
|
||||||
Log.i("mpv", "${currentTracks.size}")
|
Log.i("mpv", "${currentTracks.size}")
|
||||||
currentTracks.firstOrNull {
|
currentTracks.firstOrNull {
|
||||||
|
@ -405,7 +413,11 @@ class MPVPlayer(
|
||||||
* default start position should be projected.
|
* default start position should be projected.
|
||||||
* @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 =
|
val currentMediaItem =
|
||||||
internalMediaItems?.get(windowIndex) ?: MediaItem.Builder().build()
|
internalMediaItems?.get(windowIndex) ?: MediaItem.Builder().build()
|
||||||
return window.set(
|
return window.set(
|
||||||
|
@ -419,7 +431,7 @@ class MPVPlayer(
|
||||||
/* isDynamic= */ !isSeekable,
|
/* isDynamic= */ !isSeekable,
|
||||||
/* liveConfiguration= */ currentMediaItem.liveConfiguration,
|
/* liveConfiguration= */ currentMediaItem.liveConfiguration,
|
||||||
/* defaultPositionUs= */ C.TIME_UNSET,
|
/* defaultPositionUs= */ C.TIME_UNSET,
|
||||||
/* durationUs= */ C.msToUs(currentDurationMs ?: C.TIME_UNSET),
|
/* durationUs= */ Util.msToUs(currentDurationMs ?: C.TIME_UNSET),
|
||||||
/* firstPeriodIndex= */ windowIndex,
|
/* firstPeriodIndex= */ windowIndex,
|
||||||
/* lastPeriodIndex= */ windowIndex,
|
/* lastPeriodIndex= */ windowIndex,
|
||||||
/* positionInFirstPeriodUs= */ C.TIME_UNSET
|
/* positionInFirstPeriodUs= */ C.TIME_UNSET
|
||||||
|
@ -448,7 +460,7 @@ class MPVPlayer(
|
||||||
/* id= */ periodIndex,
|
/* id= */ periodIndex,
|
||||||
/* uid= */ periodIndex,
|
/* uid= */ periodIndex,
|
||||||
/* windowIndex= */ periodIndex,
|
/* windowIndex= */ periodIndex,
|
||||||
/* durationUs= */ C.msToUs(currentDurationMs ?: C.TIME_UNSET),
|
/* durationUs= */ Util.msToUs(currentDurationMs ?: C.TIME_UNSET),
|
||||||
/* positionInWindowUs= */ 0
|
/* positionInWindowUs= */ 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -507,7 +519,7 @@ class MPVPlayer(
|
||||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
||||||
MPVLib.command(arrayOf("multiply", "volume", "$AUDIO_FOCUS_DUCKING"))
|
MPVLib.command(arrayOf("multiply", "volume", "$AUDIO_FOCUS_DUCKING"))
|
||||||
audioFocusCallback = {
|
audioFocusCallback = {
|
||||||
MPVLib.command(arrayOf("multiply", "volume", "${1f/AUDIO_FOCUS_DUCKING}"))
|
MPVLib.command(arrayOf("multiply", "volume", "${1f / AUDIO_FOCUS_DUCKING}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AudioManager.AUDIOFOCUS_GAIN -> {
|
AudioManager.AUDIOFOCUS_GAIN -> {
|
||||||
|
@ -527,18 +539,6 @@ class MPVPlayer(
|
||||||
return handler.looper
|
return handler.looper
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a listener to receive events from the player. The listener's methods will be called
|
|
||||||
* on the thread that was used to construct the player. However, if the thread used to construct
|
|
||||||
* the player does not have a [Looper], then the listener will be called on the main thread.
|
|
||||||
*
|
|
||||||
* @param listener The listener to register.
|
|
||||||
*/
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
override fun addListener(listener: Player.EventListener) {
|
|
||||||
listeners.add(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers a listener to receive all events from the player.
|
* Registers a listener to receive all events from the player.
|
||||||
*
|
*
|
||||||
|
@ -549,17 +549,6 @@ class MPVPlayer(
|
||||||
videoListeners.add(listener)
|
videoListeners.add(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Unregister a listener registered through [.addListener]. The listener will
|
|
||||||
* no longer receive events from the player.
|
|
||||||
*
|
|
||||||
* @param listener The listener to unregister.
|
|
||||||
*/
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
override fun removeListener(listener: Player.EventListener) {
|
|
||||||
listeners.remove(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unregister a listener registered through [.addListener]. The listener will no
|
* Unregister a listener registered through [.addListener]. The listener will no
|
||||||
* longer receive events.
|
* longer receive events.
|
||||||
|
@ -595,7 +584,11 @@ class MPVPlayer(
|
||||||
* @throws com.google.android.exoplayer2.IllegalSeekPositionException If the provided `startWindowIndex` is not within the
|
* @throws com.google.android.exoplayer2.IllegalSeekPositionException If the provided `startWindowIndex` is not within the
|
||||||
* bounds of the list of media items.
|
* bounds of the list of media items.
|
||||||
*/
|
*/
|
||||||
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
|
currentIndex = startWindowIndex
|
||||||
initialSeekTo = startPositionMs / 1000
|
initialSeekTo = startPositionMs / 1000
|
||||||
|
@ -654,30 +647,31 @@ class MPVPlayer(
|
||||||
* @return The currently available [com.google.android.exoplayer2.Player.Commands].
|
* @return The currently available [com.google.android.exoplayer2.Player.Commands].
|
||||||
* @see com.google.android.exoplayer2.Player.Listener.onAvailableCommandsChanged
|
* @see com.google.android.exoplayer2.Player.Listener.onAvailableCommandsChanged
|
||||||
*/
|
*/
|
||||||
override fun getAvailableCommands(): Player.Commands {
|
override fun getAvailableCommands(): Commands {
|
||||||
return Commands.Builder()
|
return Commands.Builder()
|
||||||
.addAll(permanentAvailableCommands)
|
.addAll(permanentAvailableCommands)
|
||||||
.addIf(COMMAND_SEEK_TO_DEFAULT_POSITION, !isPlayingAd)
|
.addIf(COMMAND_SEEK_TO_DEFAULT_POSITION, !isPlayingAd)
|
||||||
.addIf(COMMAND_SEEK_IN_CURRENT_WINDOW, isCurrentWindowSeekable && !isPlayingAd)
|
.addIf(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, isCurrentMediaItemSeekable && !isPlayingAd)
|
||||||
.addIf(COMMAND_SEEK_TO_PREVIOUS_WINDOW, hasPreviousWindow() && !isPlayingAd)
|
.addIf(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, hasPreviousMediaItem() && !isPlayingAd)
|
||||||
.addIf(
|
.addIf(
|
||||||
COMMAND_SEEK_TO_PREVIOUS,
|
COMMAND_SEEK_TO_PREVIOUS,
|
||||||
!currentTimeline.isEmpty
|
!currentTimeline.isEmpty
|
||||||
&& (hasPreviousWindow() || !isCurrentWindowLive || isCurrentWindowSeekable)
|
&& (hasPreviousMediaItem() || !isCurrentMediaItemLive || isCurrentMediaItemSeekable)
|
||||||
&& !isPlayingAd
|
&& !isPlayingAd
|
||||||
)
|
)
|
||||||
.addIf(COMMAND_SEEK_TO_NEXT_WINDOW, hasNextWindow() && !isPlayingAd)
|
.addIf(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, hasNextMediaItem() && !isPlayingAd)
|
||||||
.addIf(
|
.addIf(
|
||||||
COMMAND_SEEK_TO_NEXT,
|
COMMAND_SEEK_TO_NEXT,
|
||||||
!currentTimeline.isEmpty()
|
!currentTimeline.isEmpty
|
||||||
&& (hasNextWindow() || (isCurrentWindowLive && isCurrentWindowDynamic()))
|
&& (hasNextMediaItem() || (isCurrentMediaItemLive && isCurrentMediaItemDynamic))
|
||||||
&& !isPlayingAd
|
&& !isPlayingAd
|
||||||
)
|
)
|
||||||
.addIf(COMMAND_SEEK_TO_WINDOW, !isPlayingAd)
|
.addIf(COMMAND_SEEK_TO_MEDIA_ITEM, !isPlayingAd)
|
||||||
.addIf(COMMAND_SEEK_BACK, isCurrentWindowSeekable && !isPlayingAd)
|
.addIf(COMMAND_SEEK_BACK, isCurrentMediaItemSeekable && !isPlayingAd)
|
||||||
.addIf(COMMAND_SEEK_FORWARD, isCurrentWindowSeekable && !isPlayingAd)
|
.addIf(COMMAND_SEEK_FORWARD, isCurrentMediaItemSeekable && !isPlayingAd)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resetInternalState() {
|
private fun resetInternalState() {
|
||||||
isPlayerReady = false
|
isPlayerReady = false
|
||||||
isSeekable = false
|
isSeekable = false
|
||||||
|
@ -686,8 +680,7 @@ class MPVPlayer(
|
||||||
currentPositionMs = null
|
currentPositionMs = null
|
||||||
currentDurationMs = null
|
currentDurationMs = null
|
||||||
currentCacheDurationMs = null
|
currentCacheDurationMs = null
|
||||||
trackGroupArray = TrackGroupArray.EMPTY
|
tracksInfo = TracksInfo.EMPTY
|
||||||
trackSelectionArray = TrackSelectionArray()
|
|
||||||
playbackParameters = PlaybackParameters.DEFAULT
|
playbackParameters = PlaybackParameters.DEFAULT
|
||||||
initialCommands.clear()
|
initialCommands.clear()
|
||||||
//initialSeekTo = 0L
|
//initialSeekTo = 0L
|
||||||
|
@ -699,7 +692,7 @@ class MPVPlayer(
|
||||||
MPVLib.command(
|
MPVLib.command(
|
||||||
arrayOf(
|
arrayOf(
|
||||||
"loadfile",
|
"loadfile",
|
||||||
"${mediaItem.playbackProperties?.uri}",
|
"${mediaItem.localConfiguration?.uri}",
|
||||||
"append"
|
"append"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -830,7 +823,7 @@ 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 == currentWindowIndex) {
|
if (windowIndex == currentMediaItemIndex) {
|
||||||
val seekTo =
|
val seekTo =
|
||||||
if (positionMs != C.TIME_UNSET) positionMs / C.MILLIS_PER_SECOND else initialSeekTo
|
if (positionMs != C.TIME_UNSET) positionMs / C.MILLIS_PER_SECOND else initialSeekTo
|
||||||
initialSeekTo = if (isPlayerReady) {
|
initialSeekTo = if (isPlayerReady) {
|
||||||
|
@ -849,7 +842,7 @@ class MPVPlayer(
|
||||||
internalMediaItems?.get(index)?.let { mediaItem ->
|
internalMediaItems?.get(index)?.let { mediaItem ->
|
||||||
internalMediaItem = mediaItem
|
internalMediaItem = mediaItem
|
||||||
resetInternalState()
|
resetInternalState()
|
||||||
mediaItem.playbackProperties?.subtitles?.forEach { subtitle ->
|
mediaItem.localConfiguration?.subtitleConfigurations?.forEach { subtitle ->
|
||||||
initialCommands.add(
|
initialCommands.add(
|
||||||
arrayOf(
|
arrayOf(
|
||||||
/* command= */ "sub-add",
|
/* command= */ "sub-add",
|
||||||
|
@ -877,14 +870,14 @@ class MPVPlayer(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getSeekBackIncrement(): Long {
|
override fun getSeekBackIncrement(): Long {
|
||||||
return 5000
|
return C.DEFAULT_SEEK_BACK_INCREMENT_MS
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getSeekForwardIncrement(): Long {
|
override fun getSeekForwardIncrement(): Long {
|
||||||
return 15000
|
return C.DEFAULT_SEEK_FORWARD_INCREMENT_MS
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMaxSeekToPreviousPosition(): Int {
|
override fun getMaxSeekToPreviousPosition(): Long {
|
||||||
return C.DEFAULT_MAX_SEEK_TO_PREVIOUS_POSITION_MS
|
return C.DEFAULT_MAX_SEEK_TO_PREVIOUS_POSITION_MS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -913,6 +906,11 @@ class MPVPlayer(
|
||||||
return playbackParameters
|
return playbackParameters
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
MPVLib.command(arrayOf("stop", "keep-playlist"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
override fun stop(reset: Boolean) {
|
override fun stop(reset: Boolean) {
|
||||||
MPVLib.command(arrayOf("stop", "keep-playlist"))
|
MPVLib.command(arrayOf("stop", "keep-playlist"))
|
||||||
}
|
}
|
||||||
|
@ -936,8 +934,9 @@ class MPVPlayer(
|
||||||
*
|
*
|
||||||
* @see com.google.android.exoplayer2.Player.Listener.onTracksChanged
|
* @see com.google.android.exoplayer2.Player.Listener.onTracksChanged
|
||||||
*/
|
*/
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
override fun getCurrentTrackGroups(): TrackGroupArray {
|
override fun getCurrentTrackGroups(): TrackGroupArray {
|
||||||
return trackGroupArray
|
return TrackGroupArray.EMPTY
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -950,27 +949,21 @@ class MPVPlayer(
|
||||||
*
|
*
|
||||||
* @see com.google.android.exoplayer2.Player.Listener.onTracksChanged
|
* @see com.google.android.exoplayer2.Player.Listener.onTracksChanged
|
||||||
*/
|
*/
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
override fun getCurrentTrackSelections(): TrackSelectionArray {
|
override fun getCurrentTrackSelections(): TrackSelectionArray {
|
||||||
return trackSelectionArray
|
return TrackSelectionArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
override fun getCurrentTracksInfo(): TracksInfo {
|
||||||
* Returns the current static metadata for the track selections.
|
return tracksInfo
|
||||||
*
|
}
|
||||||
*
|
|
||||||
* The returned `metadataList` is an immutable list of [Metadata] instances, where
|
override fun getTrackSelectionParameters(): TrackSelectionParameters {
|
||||||
* the elements correspond to the [current track selections][.getCurrentTrackSelections],
|
TODO("Not yet implemented")
|
||||||
* or an empty list if there are no track selections or the selected tracks contain no static
|
}
|
||||||
* metadata.
|
|
||||||
*
|
override fun setTrackSelectionParameters(parameters: TrackSelectionParameters) {
|
||||||
*
|
TODO("Not yet implemented")
|
||||||
* This metadata is considered static in that it comes from the tracks' declared Formats,
|
|
||||||
* rather than being timed (or dynamic) metadata, which is represented within a metadata track.
|
|
||||||
*
|
|
||||||
* @see com.google.android.exoplayer2.Player.Listener.onStaticMetadataChanged
|
|
||||||
*/
|
|
||||||
override fun getCurrentStaticMetadata(): List<Metadata> {
|
|
||||||
return emptyList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -979,7 +972,7 @@ class MPVPlayer(
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* This [MediaMetadata] is a combination of the [MediaItem.mediaMetadata] and the
|
* This [MediaMetadata] is a combination of the [MediaItem.mediaMetadata] and the
|
||||||
* static and dynamic metadata sourced from [com.google.android.exoplayer2.Player.Listener.onStaticMetadataChanged] and
|
* static and dynamic metadata sourced from [com.google.android.exoplayer2.Player.Listener.onMediaMetadataChanged] and
|
||||||
* [com.google.android.exoplayer2.metadata.MetadataOutput.onMetadata].
|
* [com.google.android.exoplayer2.metadata.MetadataOutput.onMetadata].
|
||||||
*/
|
*/
|
||||||
override fun getMediaMetadata(): MediaMetadata {
|
override fun getMediaMetadata(): MediaMetadata {
|
||||||
|
@ -1005,14 +998,10 @@ class MPVPlayer(
|
||||||
|
|
||||||
/** Returns the index of the period currently being played. */
|
/** Returns the index of the period currently being played. */
|
||||||
override fun getCurrentPeriodIndex(): Int {
|
override fun getCurrentPeriodIndex(): Int {
|
||||||
return currentWindowIndex
|
return currentMediaItemIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
override fun getCurrentMediaItemIndex(): Int {
|
||||||
* 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 {
|
|
||||||
|
|
||||||
return currentIndex
|
return currentIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1020,7 +1009,7 @@ class MPVPlayer(
|
||||||
* 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.
|
* 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 {
|
override fun getDuration(): Long {
|
||||||
return timeline.getWindow(currentWindowIndex, window).durationMs
|
return timeline.getWindow(currentMediaItemIndex, window).durationMs
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1272,7 +1261,7 @@ class MPVPlayer(
|
||||||
throw IllegalArgumentException("You should use global volume controls. Check out AUDIO_SERVICE.")
|
throw IllegalArgumentException("You should use global volume controls. Check out AUDIO_SERVICE.")
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CurrentTrackSelection(
|
/*private class CurrentTrackSelection(
|
||||||
private val currentTrackGroup: TrackGroup,
|
private val currentTrackGroup: TrackGroup,
|
||||||
private val index: Int
|
private val index: Int
|
||||||
) : TrackSelection {
|
) : TrackSelection {
|
||||||
|
@ -1342,7 +1331,7 @@ class MPVPlayer(
|
||||||
override fun indexOf(indexInTrackGroup: Int): Int {
|
override fun indexOf(indexInTrackGroup: Int): Int {
|
||||||
return indexInTrackGroup
|
return indexInTrackGroup
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
|
@ -1350,10 +1339,9 @@ class MPVPlayer(
|
||||||
*/
|
*/
|
||||||
private const val AUDIO_FOCUS_DUCKING = 0.5f
|
private const val AUDIO_FOCUS_DUCKING = 0.5f
|
||||||
|
|
||||||
private val permanentAvailableCommands: Player.Commands = Player.Commands.Builder()
|
private val permanentAvailableCommands: Commands = Commands.Builder()
|
||||||
.addAll(
|
.addAll(
|
||||||
COMMAND_PLAY_PAUSE,
|
COMMAND_PLAY_PAUSE,
|
||||||
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,
|
||||||
|
@ -1389,7 +1377,12 @@ class MPVPlayer(
|
||||||
* @param width The new width of the surface.
|
* @param width The new width of the surface.
|
||||||
* @param height The new height of the surface.
|
* @param height The new height of the surface.
|
||||||
*/
|
*/
|
||||||
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
override fun surfaceChanged(
|
||||||
|
holder: SurfaceHolder,
|
||||||
|
format: Int,
|
||||||
|
width: Int,
|
||||||
|
height: Int
|
||||||
|
) {
|
||||||
MPVLib.setPropertyString("android-surface-size", "${width}x$height")
|
MPVLib.setPropertyString("android-surface-size", "${width}x$height")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1429,7 +1422,7 @@ class MPVPlayer(
|
||||||
val width: Int?,
|
val width: Int?,
|
||||||
val height: Int?
|
val height: Int?
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
fun toFormat() : Format {
|
fun toFormat(): Format {
|
||||||
return Format.Builder()
|
return Format.Builder()
|
||||||
.setId(id)
|
.setId(id)
|
||||||
.setContainerMimeType("$mimeType/$codec")
|
.setContainerMimeType("$mimeType/$codec")
|
||||||
|
@ -1439,6 +1432,7 @@ class MPVPlayer(
|
||||||
.setHeight(height ?: Format.NO_VALUE)
|
.setHeight(height ?: Format.NO_VALUE)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromJSON(json: JSONObject): Track {
|
fun fromJSON(json: JSONObject): Track {
|
||||||
return Track(
|
return Track(
|
||||||
|
@ -1458,10 +1452,10 @@ class MPVPlayer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMPVTracks(trackList: String) : Triple<List<Track> ,TrackGroupArray, TrackSelectionArray> {
|
private fun getMPVTracks(trackList: String): Pair<List<Track>, TracksInfo> {
|
||||||
val tracks = mutableListOf<Track>()
|
val tracks = mutableListOf<Track>()
|
||||||
var trackGroupArray = TrackGroupArray.EMPTY
|
var tracksInfo = TracksInfo.EMPTY
|
||||||
var trackSelectionArray = TrackSelectionArray()
|
val trackGroupInfos = mutableListOf<TracksInfo.TrackGroupInfo>()
|
||||||
|
|
||||||
val trackListVideo = mutableListOf<Format>()
|
val trackListVideo = mutableListOf<Format>()
|
||||||
val trackListAudio = mutableListOf<Format>()
|
val trackListAudio = mutableListOf<Format>()
|
||||||
|
@ -1519,32 +1513,42 @@ class MPVPlayer(
|
||||||
tracks.remove(emptyTrack)
|
tracks.remove(emptyTrack)
|
||||||
trackListText.removeFirst()
|
trackListText.removeFirst()
|
||||||
}
|
}
|
||||||
val trackGroups = mutableListOf<TrackGroup>()
|
|
||||||
val trackSelections = mutableListOf<TrackSelection>()
|
|
||||||
if (trackListVideo.isNotEmpty()) {
|
if (trackListVideo.isNotEmpty()) {
|
||||||
with(TrackGroup(*trackListVideo.toTypedArray())) {
|
with(TrackGroup(*trackListVideo.toTypedArray())) {
|
||||||
trackGroups.add(this)
|
TracksInfo.TrackGroupInfo(
|
||||||
trackSelections.add(CurrentTrackSelection(this, indexCurrentVideo))
|
this,
|
||||||
|
intArrayOf(C.FORMAT_HANDLED),
|
||||||
|
C.TRACK_TYPE_VIDEO,
|
||||||
|
BooleanArray(this.length) { it == indexCurrentVideo }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (trackListAudio.isNotEmpty()) {
|
if (trackListAudio.isNotEmpty()) {
|
||||||
with(TrackGroup(*trackListAudio.toTypedArray())) {
|
with(TrackGroup(*trackListAudio.toTypedArray())) {
|
||||||
trackGroups.add(this)
|
TracksInfo.TrackGroupInfo(
|
||||||
trackSelections.add(CurrentTrackSelection(this, indexCurrentAudio))
|
this,
|
||||||
|
IntArray(this.length) { C.FORMAT_HANDLED },
|
||||||
|
C.TRACK_TYPE_AUDIO,
|
||||||
|
BooleanArray(this.length) { it == indexCurrentAudio }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (trackListText.isNotEmpty()) {
|
if (trackListText.isNotEmpty()) {
|
||||||
with(TrackGroup(*trackListText.toTypedArray())) {
|
with(TrackGroup(*trackListText.toTypedArray())) {
|
||||||
trackGroups.add(this)
|
TracksInfo.TrackGroupInfo(
|
||||||
trackSelections.add(CurrentTrackSelection(this, indexCurrentText))
|
this,
|
||||||
|
IntArray(this.length) { C.FORMAT_HANDLED },
|
||||||
|
C.TRACK_TYPE_TEXT,
|
||||||
|
BooleanArray(this.length) { it == indexCurrentText }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (trackGroups.isNotEmpty()) {
|
if (trackGroupInfos.isNotEmpty()) {
|
||||||
trackGroupArray = TrackGroupArray(*trackGroups.toTypedArray())
|
tracksInfo = TracksInfo(trackGroupInfos)
|
||||||
trackSelectionArray = TrackSelectionArray(*trackSelections.toTypedArray())
|
|
||||||
}
|
}
|
||||||
} catch (e: JSONException) {}
|
} catch (e: JSONException) {
|
||||||
return Triple(tracks, trackGroupArray, trackSelectionArray)
|
}
|
||||||
|
return Pair(tracks, tracksInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1558,16 +1562,16 @@ class MPVPlayer(
|
||||||
return when {
|
return when {
|
||||||
subtitleSources.isEmpty() -> videoSource
|
subtitleSources.isEmpty() -> videoSource
|
||||||
else -> {
|
else -> {
|
||||||
val subtitles = mutableListOf<MediaItem.Subtitle>()
|
val subtitleConfigurations = mutableListOf<MediaItem.SubtitleConfiguration>()
|
||||||
subtitleSources.forEach { subtitleSource ->
|
subtitleSources.forEach { subtitleSource ->
|
||||||
subtitleSource.mediaItem.playbackProperties?.subtitles?.forEach { subtitle ->
|
subtitleSource.mediaItem.localConfiguration?.subtitleConfigurations?.forEach { subtitle ->
|
||||||
subtitles.add(subtitle)
|
subtitleConfigurations.add(subtitle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ProgressiveMediaSource.Factory(dataSource)
|
ProgressiveMediaSource.Factory(dataSource)
|
||||||
.createMediaSource(
|
.createMediaSource(
|
||||||
videoSource.mediaItem.buildUpon()
|
videoSource.mediaItem.buildUpon()
|
||||||
.setSubtitles(subtitles).build()
|
.setSubtitleConfigurations(subtitleConfigurations).build()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,12 +8,10 @@ import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.google.android.exoplayer2.BasePlayer
|
|
||||||
import com.google.android.exoplayer2.DefaultRenderersFactory
|
import com.google.android.exoplayer2.DefaultRenderersFactory
|
||||||
import com.google.android.exoplayer2.ExoPlayer
|
import com.google.android.exoplayer2.ExoPlayer
|
||||||
import com.google.android.exoplayer2.MediaItem
|
import com.google.android.exoplayer2.MediaItem
|
||||||
import com.google.android.exoplayer2.Player
|
import com.google.android.exoplayer2.Player
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer
|
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dev.jdtech.jellyfin.database.DownloadDatabaseDao
|
import dev.jdtech.jellyfin.database.DownloadDatabaseDao
|
||||||
|
@ -36,7 +34,7 @@ constructor(
|
||||||
private val jellyfinRepository: JellyfinRepository,
|
private val jellyfinRepository: JellyfinRepository,
|
||||||
private val downloadDatabase: DownloadDatabaseDao
|
private val downloadDatabase: DownloadDatabaseDao
|
||||||
) : ViewModel(), Player.Listener {
|
) : ViewModel(), Player.Listener {
|
||||||
val player: BasePlayer
|
val player: Player
|
||||||
|
|
||||||
private val _navigateBack = MutableLiveData<Boolean>()
|
private val _navigateBack = MutableLiveData<Boolean>()
|
||||||
val navigateBack: LiveData<Boolean> = _navigateBack
|
val navigateBack: LiveData<Boolean> = _navigateBack
|
||||||
|
@ -55,7 +53,7 @@ constructor(
|
||||||
val trackSelector = DefaultTrackSelector(application)
|
val trackSelector = DefaultTrackSelector(application)
|
||||||
var playWhenReady = true
|
var playWhenReady = true
|
||||||
private var playFromDownloads = false
|
private var playFromDownloads = false
|
||||||
private var currentWindow = 0
|
private var currentMediaItemIndex = 0
|
||||||
private var playbackPosition: Long = 0
|
private var playbackPosition: Long = 0
|
||||||
|
|
||||||
var playbackSpeed: Float = 1f
|
var playbackSpeed: Float = 1f
|
||||||
|
@ -90,7 +88,7 @@ constructor(
|
||||||
.setPreferredAudioLanguage(preferredAudioLanguage)
|
.setPreferredAudioLanguage(preferredAudioLanguage)
|
||||||
.setPreferredTextLanguage(preferredSubtitleLanguage)
|
.setPreferredTextLanguage(preferredSubtitleLanguage)
|
||||||
)
|
)
|
||||||
player = SimpleExoPlayer.Builder(application, renderersFactory)
|
player = ExoPlayer.Builder(application, renderersFactory)
|
||||||
.setTrackSelector(trackSelector)
|
.setTrackSelector(trackSelector)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
@ -124,7 +122,7 @@ constructor(
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
player.setMediaItems(mediaItems, currentWindow, items[0].playbackPosition)
|
player.setMediaItems(mediaItems, currentMediaItemIndex, items[0].playbackPosition)
|
||||||
val useMpv = sp.getBoolean("mpv_player", false)
|
val useMpv = sp.getBoolean("mpv_player", false)
|
||||||
if(!useMpv || !playFromDownloads)
|
if(!useMpv || !playFromDownloads)
|
||||||
player.prepare() //TODO: This line causes a crash when playing from downloads with MPV
|
player.prepare() //TODO: This line causes a crash when playing from downloads with MPV
|
||||||
|
@ -149,12 +147,12 @@ constructor(
|
||||||
|
|
||||||
playWhenReady = player.playWhenReady
|
playWhenReady = player.playWhenReady
|
||||||
playbackPosition = player.currentPosition
|
playbackPosition = player.currentPosition
|
||||||
currentWindow = player.currentWindowIndex
|
currentMediaItemIndex = player.currentMediaItemIndex
|
||||||
player.removeListener(this)
|
player.removeListener(this)
|
||||||
player.release()
|
player.release()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pollPosition(player: BasePlayer) {
|
private fun pollPosition(player: Player) {
|
||||||
val handler = Handler(Looper.getMainLooper())
|
val handler = Handler(Looper.getMainLooper())
|
||||||
val runnable = object : Runnable {
|
val runnable = object : Runnable {
|
||||||
override fun run() {
|
override fun run() {
|
||||||
|
|
|
@ -9,10 +9,8 @@ import dev.jdtech.jellyfin.models.PlayerItem
|
||||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||||
import dev.jdtech.jellyfin.utils.getDownloadPlayerItem
|
import dev.jdtech.jellyfin.utils.getDownloadPlayerItem
|
||||||
import dev.jdtech.jellyfin.utils.isItemAvailable
|
import dev.jdtech.jellyfin.utils.isItemAvailable
|
||||||
import dev.jdtech.jellyfin.utils.isItemDownloaded
|
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import org.jellyfin.sdk.model.api.ItemFields
|
import org.jellyfin.sdk.model.api.ItemFields
|
||||||
|
|
Loading…
Reference in a new issue