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

View File

@ -20,7 +20,6 @@
package net.newpipe.newplayer.model
import androidx.media3.common.Player
import net.newpipe.newplayer.Chapter
import net.newpipe.newplayer.RepeatMode
import net.newpipe.newplayer.playerInternals.PlaylistItem
@ -45,14 +44,12 @@ data class VideoPlayerUIState(
val chapters: List<Chapter>,
val shuffleEnabled: Boolean,
val repeatMode: RepeatMode,
val playListDurationInS: Int
val playListDurationInS: Int,
val currentlyPlaying: PlaylistItem
) {
companion object {
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,
playing = false,
contentRatio = 16 / 9f,
embeddedUiRatio = 16f / 9f,
@ -70,7 +67,24 @@ data class VideoPlayerUIState(
chapters = emptyList(),
shuffleEnabled = false,
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 removePlaylistItem(index: Int)
fun onStreamItemDragFinished()
fun dialogVisible(visible: Boolean)
}

View File

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

View File

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

View File

@ -21,6 +21,7 @@
package net.newpipe.newplayer.playerInternals
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
@ -37,12 +38,55 @@ data class PlaylistItem(
val uniqueId: Long,
val thumbnail: Thumbnail?,
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)) {
(0..player.mediaItemCount - 1).map { index ->
println("gurken index: $index")
val mediaItem = player.getMediaItemAt(index)
val uniqueId = mediaItem.mediaId.toLong()
val id = idLookupTable.get(uniqueId)

View File

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

View File

@ -122,7 +122,7 @@ fun VideoPlayerControllerUICenterUIPreview() {
Surface(color = Color.Black) {
CenterUI(
viewModel = VideoPlayerViewModelDummy(),
uiState = VideoPlayerUIState.DEFAULT.copy(
uiState = VideoPlayerUIState.DUMMY.copy(
isLoading = false,
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.dp
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
@Composable
fun DropDownMenu() {
fun DropDownMenu(viewModel: VideoPlayerViewModel, uiState: VideoPlayerUIState) {
var showMainMenu: Boolean by remember { mutableStateOf(false) }
var pixel_density = LocalDensity.current
val pixel_density = LocalDensity.current
var offsetY by remember {
mutableStateOf(0.dp)
@ -141,7 +144,7 @@ fun DropDownMenu() {
fun VideoPlayerControllerDropDownPreview() {
VideoPlayerTheme {
Box(Modifier.fillMaxSize()) {
DropDownMenu()
DropDownMenu(VideoPlayerViewModelDummy(), VideoPlayerUIState.DUMMY)
}
}
}

View File

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

View File

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