handle currently plaing and show title and creator

This commit is contained in:
Christian Schabesberger 2024-09-04 13:19:36 +02:00
parent d97ecc7519
commit a47ea8e078
11 changed files with 171 additions and 44 deletions

View File

@ -40,8 +40,8 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.newpipe.newplayer.playerInternals.PlaylistItem import net.newpipe.newplayer.playerInternals.PlaylistItem
import net.newpipe.newplayer.playerInternals.fetchPlaylistItem
import net.newpipe.newplayer.playerInternals.getPlaylistItemsFromExoplayer import net.newpipe.newplayer.playerInternals.getPlaylistItemsFromExoplayer
import net.newpipe.newplayer.utils.Thumbnail
import kotlin.Exception import kotlin.Exception
import kotlin.random.Random import kotlin.random.Random
@ -72,12 +72,12 @@ interface NewPlayer {
val sharingLinkWithOffsetPossible: Boolean val sharingLinkWithOffsetPossible: Boolean
var currentPosition: Long var currentPosition: Long
var fastSeekAmountSec: Int var fastSeekAmountSec: Int
var playBackMode: PlayMode val playBackMode: MutableStateFlow<PlayMode?>
var playMode: StateFlow<PlayMode?>
var shuffle: Boolean var shuffle: Boolean
var repeatMode: RepeatMode var repeatMode: RepeatMode
val playlist: StateFlow<List<PlaylistItem>> val playlist: StateFlow<List<PlaylistItem>>
val currentlyPlaying: StateFlow<PlaylistItem?>
// callbacks // callbacks
@ -93,7 +93,6 @@ interface NewPlayer {
fun removePlaylistItem(index: Int) fun removePlaylistItem(index: Int)
fun playStream(item: String, playMode: PlayMode) fun playStream(item: String, playMode: PlayMode)
fun playStream(item: String, streamVariant: String, playMode: PlayMode) fun playStream(item: String, streamVariant: String, playMode: PlayMode)
fun setPlayMode(playMode: PlayMode)
data class Builder(val app: Application, val repository: MediaRepository) { data class Builder(val app: Application, val repository: MediaRepository) {
private var mediaSourceFactory: MediaSource.Factory? = null private var mediaSourceFactory: MediaSource.Factory? = null
@ -155,12 +154,10 @@ class NewPlayerImpl(
} }
override var fastSeekAmountSec: Int = 10 override var fastSeekAmountSec: Int = 10
override var playBackMode: PlayMode = PlayMode.EMBEDDED_VIDEO
private var playerScope = CoroutineScope(Dispatchers.Main + Job()) private var playerScope = CoroutineScope(Dispatchers.Main + Job())
var mutablePlayMode = MutableStateFlow<PlayMode?>(null) override var playBackMode = MutableStateFlow<PlayMode?>(null)
override var playMode = mutablePlayMode.asStateFlow()
override var shuffle: Boolean override var shuffle: Boolean
get() = internalPlayer.shuffleModeEnabled get() = internalPlayer.shuffleModeEnabled
@ -183,7 +180,7 @@ class NewPlayerImpl(
} }
} }
var mutableOnEvent = MutableSharedFlow<Pair<Player, Player.Events>>() private var mutableOnEvent = MutableSharedFlow<Pair<Player, Player.Events>>()
override val onExoPlayerEvent: SharedFlow<Pair<Player, Player.Events>> = override val onExoPlayerEvent: SharedFlow<Pair<Player, Player.Events>> =
mutableOnEvent.asSharedFlow() mutableOnEvent.asSharedFlow()
@ -197,10 +194,13 @@ class NewPlayerImpl(
override val duration: Long override val duration: Long
get() = internalPlayer.duration get() = internalPlayer.duration
val mutablePlaylist = MutableStateFlow<List<PlaylistItem>>(emptyList()) private val mutablePlaylist = MutableStateFlow<List<PlaylistItem>>(emptyList())
override val playlist: StateFlow<List<PlaylistItem>> = override val playlist: StateFlow<List<PlaylistItem>> =
mutablePlaylist.asStateFlow() mutablePlaylist.asStateFlow()
private val mutableCurrentlyPlaying = MutableStateFlow<PlaylistItem?>(null)
override val currentlyPlaying: StateFlow<PlaylistItem?> = mutableCurrentlyPlaying.asStateFlow()
init { init {
println("gurken init") println("gurken init")
internalPlayer.addListener(object : Player.Listener { internalPlayer.addListener(object : Player.Listener {
@ -227,12 +227,37 @@ class NewPlayerImpl(
super.onTimelineChanged(timeline, reason) super.onTimelineChanged(timeline, reason)
updatePlaylistItems() updatePlaylistItems()
} }
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
super.onMediaItemTransition(mediaItem, reason)
mediaItem?.let {
val playlistItem = getPlaylistItem(mediaItem.mediaId.toLong())
if (playlistItem != null) {
mutableCurrentlyPlaying.update {
playlistItem
}
} else {
launchJobAndCollectError {
val item = fetchPlaylistItem(
uniqueId = mediaItem.mediaId.toLong(),
mediaRepo = repository,
idLookupTable = uniqueIdToIdLookup
)
mutableCurrentlyPlaying.update { item }
}
}
}
}
}) })
} }
private fun updatePlaylistItems() { private fun updatePlaylistItems() {
playerScope.launch { playerScope.launch {
val playlist = getPlaylistItemsFromExoplayer(internalPlayer, repository, uniqueIdToIdLookup) val playlist =
getPlaylistItemsFromExoplayer(internalPlayer, repository, uniqueIdToIdLookup)
var playlistDuration = 0 var playlistDuration = 0
for (item in playlist) { for (item in playlist) {
playlistDuration += item.lengthInS playlistDuration += item.lengthInS
@ -244,6 +269,15 @@ class NewPlayerImpl(
} }
} }
private fun getPlaylistItem(uniqueId: Long): PlaylistItem? {
for (item in playlist.value) {
if (item.uniqueId == uniqueId) {
return item
}
}
return null
}
override fun prepare() { override fun prepare() {
internalPlayer.prepare() internalPlayer.prepare()
} }
@ -289,15 +323,12 @@ class NewPlayerImpl(
} }
} }
override fun setPlayMode(playMode: PlayMode) {
this.mutablePlayMode.update { playMode }
}
private fun internalPlayStream(mediaItem: MediaItem, playMode: PlayMode) { private fun internalPlayStream(mediaItem: MediaItem, playMode: PlayMode) {
if (internalPlayer.playbackState == Player.STATE_IDLE) { if (internalPlayer.playbackState == Player.STATE_IDLE) {
internalPlayer.prepare() internalPlayer.prepare()
} }
this.mutablePlayMode.update { playMode } this.playBackMode.update { playMode }
this.internalPlayer.setMediaItem(mediaItem) this.internalPlayer.setMediaItem(mediaItem)
this.internalPlayer.play() this.internalPlayer.play()
} }
@ -305,7 +336,7 @@ class NewPlayerImpl(
private suspend fun toMediaItem(item: String, streamVariant: String): MediaItem { private suspend fun toMediaItem(item: String, streamVariant: String): MediaItem {
val dataStream = repository.getStream(item, streamVariant) val dataStream = repository.getStream(item, streamVariant)
val uniqueId = Random.nextLong() val uniqueId = Random.nextLong()
uniqueIdToIdLookup.set(uniqueId, item) uniqueIdToIdLookup[uniqueId] = item
val mediaItem = MediaItem.Builder().setMediaId(uniqueId.toString()).setUri(dataStream) val mediaItem = MediaItem.Builder().setMediaId(uniqueId.toString()).setUri(dataStream)
return mediaItem.build() return mediaItem.build()
} }

View File

@ -20,7 +20,6 @@
package net.newpipe.newplayer.model package net.newpipe.newplayer.model
import androidx.media3.common.Player
import net.newpipe.newplayer.Chapter import net.newpipe.newplayer.Chapter
import net.newpipe.newplayer.RepeatMode import net.newpipe.newplayer.RepeatMode
import net.newpipe.newplayer.playerInternals.PlaylistItem import net.newpipe.newplayer.playerInternals.PlaylistItem
@ -45,14 +44,12 @@ data class VideoPlayerUIState(
val chapters: List<Chapter>, val chapters: List<Chapter>,
val shuffleEnabled: Boolean, val shuffleEnabled: Boolean,
val repeatMode: RepeatMode, val repeatMode: RepeatMode,
val playListDurationInS: Int val playListDurationInS: Int,
val currentlyPlaying: PlaylistItem
) { ) {
companion object { companion object {
val DEFAULT = VideoPlayerUIState( val DEFAULT = VideoPlayerUIState(
// TODO: replace this with the placeholder state.
// The actual initial state upon starting to play is dictated by the NewPlayer instance
uiMode = UIModeState.PLACEHOLDER, uiMode = UIModeState.PLACEHOLDER,
//uiMode = UIModeState.PLACEHOLDER,
playing = false, playing = false,
contentRatio = 16 / 9f, contentRatio = 16 / 9f,
embeddedUiRatio = 16f / 9f, embeddedUiRatio = 16f / 9f,
@ -70,7 +67,24 @@ data class VideoPlayerUIState(
chapters = emptyList(), chapters = emptyList(),
shuffleEnabled = false, shuffleEnabled = false,
repeatMode = RepeatMode.DONT_REPEAT, repeatMode = RepeatMode.DONT_REPEAT,
playListDurationInS = 0 playListDurationInS = 0,
currentlyPlaying = PlaylistItem.DEFAULT
)
val DUMMY = DEFAULT.copy(
uiMode = UIModeState.EMBEDDED_VIDEO,
playing = true,
seekerPosition = 0.3f,
bufferedPercentage = 0.5f,
isLoading = false,
durationInMs = 420,
playbackPositionInMs = 69,
fastSeekSeconds = 10,
soundVolume = 0.5f,
brightness = 0.2f,
shuffleEnabled = true,
playListDurationInS = 5493,
currentlyPlaying = PlaylistItem.DUMMY
) )
} }
} }

View File

@ -67,4 +67,5 @@ interface VideoPlayerViewModel {
fun movePlaylistItem(from: Int, to: Int) fun movePlaylistItem(from: Int, to: Int)
fun removePlaylistItem(index: Int) fun removePlaylistItem(index: Int)
fun onStreamItemDragFinished() fun onStreamItemDragFinished()
fun dialogVisible(visible: Boolean)
} }

View File

@ -40,12 +40,14 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.newpipe.newplayer.Chapter import net.newpipe.newplayer.Chapter
import net.newpipe.newplayer.utils.VideoSize import net.newpipe.newplayer.utils.VideoSize
import net.newpipe.newplayer.NewPlayer import net.newpipe.newplayer.NewPlayer
import net.newpipe.newplayer.RepeatMode import net.newpipe.newplayer.RepeatMode
import net.newpipe.newplayer.playerInternals.PlaylistItem
import net.newpipe.newplayer.ui.ContentScale import net.newpipe.newplayer.ui.ContentScale
import java.util.LinkedList import java.util.LinkedList
@ -179,7 +181,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
} }
newPlayer?.let { newPlayer -> newPlayer?.let { newPlayer ->
viewModelScope.launch { viewModelScope.launch {
newPlayer.playMode.collect { newMode -> newPlayer.playBackMode.collect { newMode ->
val currentMode = mutableUiState.value.uiMode.toPlayMode() val currentMode = mutableUiState.value.uiMode.toPlayMode()
if (currentMode != newMode) { if (currentMode != newMode) {
mutableUiState.update { mutableUiState.update {
@ -196,6 +198,13 @@ class VideoPlayerViewModelImpl @Inject constructor(
mutableUiState.update { it.copy(playList = playlist) } mutableUiState.update { it.copy(playList = playlist) }
} }
} }
viewModelScope.launch {
newPlayer.currentlyPlaying.collect { playlistItem ->
mutableUiState.update {
it.copy(currentlyPlaying = playlistItem ?: PlaylistItem.DEFAULT)
}
}
}
} }
} }
@ -467,6 +476,14 @@ class VideoPlayerViewModelImpl @Inject constructor(
playlistItemToBeMoved = null playlistItemToBeMoved = null
} }
override fun dialogVisible(visible: Boolean) {
if(visible) {
uiVisibilityJob?.cancel()
} else {
resetHideUiDelayedJob()
}
}
override fun removePlaylistItem(index: Int) { override fun removePlaylistItem(index: Int) {
newPlayer?.removePlaylistItem(index) newPlayer?.removePlaylistItem(index)
} }
@ -476,7 +493,9 @@ class VideoPlayerViewModelImpl @Inject constructor(
val newPlayMode = newState.toPlayMode() val newPlayMode = newState.toPlayMode()
val currentPlayMode = mutableUiState.value.uiMode.toPlayMode() val currentPlayMode = mutableUiState.value.uiMode.toPlayMode()
if (newPlayMode != currentPlayMode) { if (newPlayMode != currentPlayMode) {
newPlayer?.setPlayMode(newPlayMode!!) newPlayer?.playBackMode?.update {
newPlayMode!!
}
} else { } else {
mutableUiState.update { mutableUiState.update {
it.copy(uiMode = newState) it.copy(uiMode = newState)

View File

@ -116,6 +116,10 @@ open class VideoPlayerViewModelDummy : VideoPlayerViewModel {
println("dummy impl") println("dummy impl")
} }
override fun dialogVisible(visible: Boolean) {
println("dummy impl dialog visible: $visible")
}
override fun pause() { override fun pause() {
println("dummy pause") println("dummy pause")
} }

View File

@ -21,6 +21,7 @@
package net.newpipe.newplayer.playerInternals package net.newpipe.newplayer.playerInternals
import androidx.media3.common.MediaItem
import androidx.media3.common.Player import androidx.media3.common.Player
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
@ -37,12 +38,55 @@ data class PlaylistItem(
val uniqueId: Long, val uniqueId: Long,
val thumbnail: Thumbnail?, val thumbnail: Thumbnail?,
val lengthInS: Int val lengthInS: Int
) {
companion object {
val DEFAULT = PlaylistItem(
title = "",
creator = "",
id = "",
uniqueId = -1L,
thumbnail = null,
lengthInS = 0
) )
suspend fun getPlaylistItemsFromExoplayer(player: Player, mediaRepo: MediaRepository, idLookupTable: HashMap<Long, String>) = val DUMMY = PlaylistItem(
title = "Superawesome Video",
creator = "Yours truely",
id = "some_id",
uniqueId = 12345L,
thumbnail = null,
lengthInS = 420
)
}
}
suspend fun fetchPlaylistItem(
uniqueId: Long,
mediaRepo: MediaRepository,
idLookupTable: HashMap<Long, String>
) : PlaylistItem {
val id = idLookupTable[uniqueId]
?: throw NewPlayerException("Unknown uniqueId: $uniqueId, uniqueId Id mapping error. Something went wrong during datafetching.")
val metaInfo = mediaRepo.getMetaInfo(id)
return PlaylistItem(
title = metaInfo.title,
creator = metaInfo.channelName,
id = id,
thumbnail = metaInfo.thumbnail,
lengthInS = metaInfo.lengthInS,
uniqueId = uniqueId
)
}
suspend fun getPlaylistItemsFromExoplayer(
player: Player,
mediaRepo: MediaRepository,
idLookupTable: HashMap<Long, String>
) =
with(CoroutineScope(coroutineContext)) { with(CoroutineScope(coroutineContext)) {
(0..player.mediaItemCount - 1).map { index -> (0..player.mediaItemCount - 1).map { index ->
println("gurken index: $index")
val mediaItem = player.getMediaItemAt(index) val mediaItem = player.getMediaItemAt(index)
val uniqueId = mediaItem.mediaId.toLong() val uniqueId = mediaItem.mediaId.toLong()
val id = idLookupTable.get(uniqueId) val id = idLookupTable.get(uniqueId)

View File

@ -126,7 +126,7 @@ fun VideoPlayerControllerBottomUIPreview() {
BottomUI( BottomUI(
modifier = Modifier, modifier = Modifier,
viewModel = VideoPlayerViewModelDummy(), viewModel = VideoPlayerViewModelDummy(),
uiState = VideoPlayerUIState.DEFAULT.copy( uiState = VideoPlayerUIState.DUMMY.copy(
uiMode = UIModeState.FULLSCREEN_VIDEO_CONTROLLER_UI, uiMode = UIModeState.FULLSCREEN_VIDEO_CONTROLLER_UI,
seekerPosition = 0.4f, seekerPosition = 0.4f,
durationInMs = 90 * 60 * 1000, durationInMs = 90 * 60 * 1000,

View File

@ -122,7 +122,7 @@ fun VideoPlayerControllerUICenterUIPreview() {
Surface(color = Color.Black) { Surface(color = Color.Black) {
CenterUI( CenterUI(
viewModel = VideoPlayerViewModelDummy(), viewModel = VideoPlayerViewModelDummy(),
uiState = VideoPlayerUIState.DEFAULT.copy( uiState = VideoPlayerUIState.DUMMY.copy(
isLoading = false, isLoading = false,
playing = true playing = true
) )

View File

@ -52,13 +52,16 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import net.newpipe.newplayer.R import net.newpipe.newplayer.R
import net.newpipe.newplayer.model.VideoPlayerUIState
import net.newpipe.newplayer.model.VideoPlayerViewModel
import net.newpipe.newplayer.model.VideoPlayerViewModelDummy
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
@Composable @Composable
fun DropDownMenu() { fun DropDownMenu(viewModel: VideoPlayerViewModel, uiState: VideoPlayerUIState) {
var showMainMenu: Boolean by remember { mutableStateOf(false) } var showMainMenu: Boolean by remember { mutableStateOf(false) }
var pixel_density = LocalDensity.current val pixel_density = LocalDensity.current
var offsetY by remember { var offsetY by remember {
mutableStateOf(0.dp) mutableStateOf(0.dp)
@ -141,7 +144,7 @@ fun DropDownMenu() {
fun VideoPlayerControllerDropDownPreview() { fun VideoPlayerControllerDropDownPreview() {
VideoPlayerTheme { VideoPlayerTheme {
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {
DropDownMenu() DropDownMenu(VideoPlayerViewModelDummy(), VideoPlayerUIState.DUMMY)
} }
} }
} }

View File

@ -201,7 +201,7 @@ fun VideoPlayerStreamSelectUIPreview() {
StreamSelectUI( StreamSelectUI(
isChapterSelect = false, isChapterSelect = false,
viewModel = VideoPlayerViewModelDummy(), viewModel = VideoPlayerViewModelDummy(),
uiState = VideoPlayerUIState.DEFAULT.copy( uiState = VideoPlayerUIState.DUMMY.copy(
playList = arrayListOf( playList = arrayListOf(
PlaylistItem( PlaylistItem(
id = "6502", id = "6502",

View File

@ -51,6 +51,7 @@ import net.newpipe.newplayer.R
import net.newpipe.newplayer.model.VideoPlayerUIState import net.newpipe.newplayer.model.VideoPlayerUIState
import net.newpipe.newplayer.model.VideoPlayerViewModel import net.newpipe.newplayer.model.VideoPlayerViewModel
import net.newpipe.newplayer.model.VideoPlayerViewModelDummy import net.newpipe.newplayer.model.VideoPlayerViewModelDummy
import net.newpipe.newplayer.playerInternals.PlaylistItem
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
import net.newpipe.newplayer.ui.theme.video_player_onSurface import net.newpipe.newplayer.ui.theme.video_player_onSurface
import net.newpipe.newplayer.utils.getEmbeddedUiConfig import net.newpipe.newplayer.utils.getEmbeddedUiConfig
@ -67,14 +68,17 @@ fun TopUI(
) { ) {
Column(horizontalAlignment = Alignment.Start, modifier = Modifier.weight(1F)) { Column(horizontalAlignment = Alignment.Start, modifier = Modifier.weight(1F)) {
Text( Text(
"The Title", uiState.currentlyPlaying.title,
fontSize = 15.sp, fontSize = 15.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
Text( Text(
"The Channel", fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis uiState.currentlyPlaying.creator,
fontSize = 12.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
) )
} }
Button( Button(
@ -105,9 +109,14 @@ fun TopUI(
) )
} }
} }
androidx.compose.animation.AnimatedVisibility(visible = 1 < uiState.playList.size) { AnimatedVisibility(visible = 1 < uiState.playList.size) {
IconButton( IconButton(
onClick = { viewModel.openStreamSelection(selectChapter = false, embeddedUiConfig) }, onClick = {
viewModel.openStreamSelection(
selectChapter = false,
embeddedUiConfig
)
},
) { ) {
Icon( Icon(
imageVector = Icons.AutoMirrored.Filled.List, imageVector = Icons.AutoMirrored.Filled.List,
@ -115,7 +124,7 @@ fun TopUI(
) )
} }
} }
DropDownMenu() DropDownMenu(viewModel, uiState)
} }
} }
@ -128,7 +137,9 @@ fun TopUI(
fun VideoPlayerControllerTopUIPreview() { fun VideoPlayerControllerTopUIPreview() {
VideoPlayerTheme { VideoPlayerTheme {
Surface(color = Color.Black) { Surface(color = Color.Black) {
TopUI(modifier = Modifier, VideoPlayerViewModelDummy(), VideoPlayerUIState.DEFAULT) TopUI(
modifier = Modifier, VideoPlayerViewModelDummy(), VideoPlayerUIState.DUMMY
)
} }
} }
} }