remove playlist item and replace it with MediaItem and MediaMetadata

This commit is contained in:
Christian Schabesberger 2024-09-09 21:59:09 +02:00
parent 5710596972
commit a297a4c08f
13 changed files with 193 additions and 249 deletions

View File

@ -4,7 +4,7 @@
<selectionStates>
<SelectionState runConfigName="TestApp">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2024-09-02T13:31:45.216735120Z">
<DropdownSelection timestamp="2024-09-09T16:54:32.015849656Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=981f7af2" />

View File

@ -21,22 +21,15 @@
package net.newpipe.newplayer
import android.net.Uri
import androidx.media3.common.MediaMetadata
import androidx.media3.common.PlaybackException
import net.newpipe.newplayer.utils.Thumbnail
data class Chapter(val chapterStartInMs: Long, val chapterTitle: String?, val thumbnail: Uri?)
data class MetaInfo(
val title: String,
val channelName: String,
val thumbnail: Uri?,
val lengthInS: Int
)
interface MediaRepository {
suspend fun getMetaInfo(item: String): MetaInfo
suspend fun getMetaInfo(item: String): MediaMetadata
suspend fun getAvailableStreamVariants(item: String): List<String>
suspend fun getStream(item: String, streamSelector: String): Uri

View File

@ -21,29 +21,14 @@
package net.newpipe.newplayer
import android.app.Application
import android.util.Log
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.Timeline
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.MediaSource
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import net.newpipe.newplayer.model.PlaylistItem
import net.newpipe.newplayer.model.fetchPlaylistItem
import net.newpipe.newplayer.model.getPlaylistItemsFromExoplayer
import kotlin.Exception
import kotlin.random.Random
enum class PlayMode {
IDLE,
@ -74,8 +59,8 @@ interface NewPlayer {
var shuffle: Boolean
var repeatMode: RepeatMode
val playlist: StateFlow<List<PlaylistItem>>
val currentlyPlaying: StateFlow<PlaylistItem?>
val playlist: StateFlow<List<MediaItem>>
val currentlyPlaying: StateFlow<MediaItem?>
var currentlyPlayingPlaylistItem: Int
val currentChapters: StateFlow<List<Chapter>>

View File

@ -43,9 +43,6 @@ import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import net.newpipe.newplayer.model.PlaylistItem
import net.newpipe.newplayer.model.fetchPlaylistItem
import net.newpipe.newplayer.model.getPlaylistItemsFromExoplayer
import net.newpipe.newplayer.service.NewPlayerService
import kotlin.random.Random
@ -116,12 +113,12 @@ class NewPlayerImpl(
override val duration: Long
get() = internalPlayer.duration
private val mutablePlaylist = MutableStateFlow<List<PlaylistItem>>(emptyList())
override val playlist: StateFlow<List<PlaylistItem>> =
private val mutablePlaylist = MutableStateFlow<List<MediaItem>>(emptyList())
override val playlist: StateFlow<List<MediaItem>> =
mutablePlaylist.asStateFlow()
private val mutableCurrentlyPlaying = MutableStateFlow<PlaylistItem?>(null)
override val currentlyPlaying: StateFlow<PlaylistItem?> = mutableCurrentlyPlaying.asStateFlow()
private val mutableCurrentlyPlaying = MutableStateFlow<MediaItem?>(null)
override val currentlyPlaying: StateFlow<MediaItem?> = mutableCurrentlyPlaying.asStateFlow()
private val mutableCurrentChapter = MutableStateFlow<List<Chapter>>(emptyList())
override val currentChapters: StateFlow<List<Chapter>> = mutableCurrentChapter.asStateFlow()
@ -157,29 +154,16 @@ class NewPlayerImpl(
}
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
super.onTimelineChanged(timeline, reason)
updatePlaylistItems()
mutablePlaylist.update {
(0..<internalPlayer.mediaItemCount).map {
internalPlayer.getMediaItemAt(it)
}
}
}
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 }
}
}
}
mutableCurrentlyPlaying.update { mediaItem }
}
})
@ -187,7 +171,8 @@ class NewPlayerImpl(
currentlyPlaying.collect { playing ->
playing?.let {
try {
val chapters = repository.getChapters(playing.id)
val chapters =
repository.getChapters(uniqueIdToIdLookup[playing.mediaId.toLong()]!!)
mutableCurrentChapter.update { chapters }
} catch (e: Exception) {
mutableErrorFlow.emit(e)
@ -197,35 +182,6 @@ class NewPlayerImpl(
}
}
private fun updatePlaylistItems() {
if (internalPlayer.mediaItemCount == 0) {
playBackMode.update {
PlayMode.IDLE
}
}
playerScope.launch {
val playlist =
getPlaylistItemsFromExoplayer(internalPlayer, repository, uniqueIdToIdLookup)
var playlistDuration = 0
for (item in playlist) {
playlistDuration += item.lengthInS
}
mutablePlaylist.update {
playlist
}
}
}
private fun getPlaylistItem(uniqueId: Long): PlaylistItem? {
for (item in playlist.value) {
if (item.uniqueId == uniqueId) {
return item
}
}
return null
}
override fun prepare() {
internalPlayer.prepare()
if (mediaController == null) {
@ -309,10 +265,21 @@ class NewPlayerImpl(
private suspend fun toMediaItem(item: String, streamVariant: String): MediaItem {
val dataStream = repository.getStream(item, streamVariant)
val uniqueId = Random.nextLong()
uniqueIdToIdLookup[uniqueId] = item
val mediaItem = MediaItem.Builder().setMediaId(uniqueId.toString()).setUri(dataStream)
return mediaItem.build()
val mediaItemBuilder = MediaItem.Builder()
.setMediaId(uniqueId.toString())
.setUri(dataStream)
try {
val metadata = repository.getMetaInfo(item)
mediaItemBuilder.setMediaMetadata(metadata)
} catch (e: Exception) {
mutableErrorFlow.emit(e)
}
return mediaItemBuilder.build()
}
private suspend fun toMediaItem(item: String): MediaItem {

View File

@ -22,6 +22,7 @@
package net.newpipe.newplayer.model
import android.net.Uri
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
@ -30,90 +31,3 @@ import net.newpipe.newplayer.NewPlayerException
import net.newpipe.newplayer.utils.Thumbnail
import kotlin.coroutines.coroutineContext
data class PlaylistItem(
val title: String,
val creator: String,
val id: String,
val uniqueId: Long,
val thumbnail: Uri?,
val lengthInS: Int
) {
companion object {
val DEFAULT = PlaylistItem(
title = "",
creator = "",
id = "",
uniqueId = -1L,
thumbnail = null,
lengthInS = 0
)
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).map { index ->
val mediaItem = player.getMediaItemAt(index)
val uniqueId = mediaItem.mediaId.toLong()
val id = idLookupTable.get(uniqueId)
?: throw NewPlayerException("Unknown uniqueId: $uniqueId, uniqueId Id mapping error. Something went wrong during datafetching.")
Pair(uniqueId, id)
}.map { item ->
Pair(item, async {
mediaRepo.getMetaInfo(item.second)
})
}.map {
val uniqueId = it.first.first
val id = it.first.second
val metaInfo = it.second.await()
PlaylistItem(
title = metaInfo.title,
creator = metaInfo.channelName,
id = id,
thumbnail = metaInfo.thumbnail,
lengthInS = metaInfo.lengthInS,
uniqueId = uniqueId
)
}
}
fun getPlaylistDurationInS(items: List<PlaylistItem>): Int {
var duration = 0
for (item in items) {
duration += item.lengthInS
}
return duration
}

View File

@ -20,10 +20,38 @@
package net.newpipe.newplayer.model
import android.net.Uri
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.util.UnstableApi
import net.newpipe.newplayer.Chapter
import net.newpipe.newplayer.RepeatMode
import net.newpipe.newplayer.ui.ContentScale
data class GurkenItem(
val title: String,
val creator: String,
val id: String,
val uniqueId: Long,
val thumbnail: Uri?,
val lengthInS: Int
) {
companion object {
val DEFAULT = GurkenItem(
title = "",
creator = "",
id = "",
uniqueId = -1L,
thumbnail = null,
lengthInS = 0
)
}
}
@UnstableApi
data class VideoPlayerUIState(
val uiMode: UIModeState,
val playing: Boolean,
@ -40,12 +68,12 @@ data class VideoPlayerUIState(
val soundVolume: Float,
val brightness: Float?, // when null use system value
val embeddedUiConfig: EmbeddedUiConfig?,
val playList: List<PlaylistItem>,
val playList: List<MediaItem>,
val chapters: List<Chapter>,
val shuffleEnabled: Boolean,
val repeatMode: RepeatMode,
val playListDurationInS: Int,
val currentlyPlaying: PlaylistItem,
val currentlyPlaying: MediaItem?,
val currentPlaylistItemIndex: Int
) {
companion object {
@ -70,7 +98,7 @@ data class VideoPlayerUIState(
shuffleEnabled = false,
repeatMode = RepeatMode.DONT_REPEAT,
playListDurationInS = 0,
currentlyPlaying = PlaylistItem.DEFAULT,
currentlyPlaying = null,
currentPlaylistItemIndex = 0
)
@ -88,7 +116,16 @@ data class VideoPlayerUIState(
brightness = 0.2f,
shuffleEnabled = true,
playListDurationInS = 5493,
currentlyPlaying = PlaylistItem.DUMMY,
currentlyPlaying = MediaItem.Builder()
.setUri("https://ftp.fau.de/cdn.media.ccc.de/congress/2010/mp4-h264-HQ/27c3-4159-en-reverse_engineering_mos_6502.mp4")
.setMediaId("0")
.setMediaMetadata(MediaMetadata.Builder()
.setTitle("Superawesome Video")
.setArtist("Yours truely")
.setArtworkUri(null)
.setDurationMs(4201000L)
.build())
.build(),
currentPlaylistItemIndex = 1,
chapters = arrayListOf(
Chapter(
@ -108,30 +145,36 @@ data class VideoPlayerUIState(
),
),
playList = arrayListOf(
PlaylistItem(
id = "6502",
title = "Stream 1",
creator = "The Creator",
lengthInS = 6 * 60 + 5,
thumbnail = null,
uniqueId = 0
),
PlaylistItem(
id = "6502",
title = "Stream 2",
creator = "The Creator 2",
lengthInS = 2 * 60 + 5,
thumbnail = null,
uniqueId = 1
),
PlaylistItem(
id = "6502",
title = "Stream 3",
creator = "The Creator 3",
lengthInS = 29 * 60 + 5,
thumbnail = null,
uniqueId = 2
)
MediaItem.Builder()
.setUri("https://ftp.fau.de/cdn.media.ccc.de/congress/2010/mp4-h264-HQ/27c3-4159-en-reverse_engineering_mos_6502.mp4")
.setMediaId("0")
.setMediaMetadata(MediaMetadata.Builder()
.setTitle("Stream 1")
.setArtist("Yours truely")
.setArtworkUri(null)
.setDurationMs(4201000L)
.build())
.build(),
MediaItem.Builder()
.setUri("https://ftp.fau.de/cdn.media.ccc.de/congress/2010/mp4-h264-HQ/27c3-4159-en-reverse_engineering_mos_6502.mp4")
.setMediaId("1")
.setMediaMetadata(MediaMetadata.Builder()
.setTitle("Stream 2")
.setArtist("Yours truely")
.setArtworkUri(null)
.setDurationMs(3201000L)
.build())
.build(),
MediaItem.Builder()
.setUri("https://ftp.fau.de/cdn.media.ccc.de/congress/2010/mp4-h264-HQ/27c3-4159-en-reverse_engineering_mos_6502.mp4")
.setMediaId("2")
.setMediaMetadata(MediaMetadata.Builder()
.setTitle("Stream 3")
.setArtist("Yours truely")
.setArtworkUri(null)
.setDurationMs(2201000L)
.build())
.build(),
)
)
}

View File

@ -25,12 +25,14 @@ import android.media.AudioManager
import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.annotation.OptIn
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat.getSystemService
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@ -44,6 +46,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import net.newpipe.newplayer.utils.VideoSize
import net.newpipe.newplayer.NewPlayer
import net.newpipe.newplayer.NewPlayerException
import net.newpipe.newplayer.RepeatMode
import net.newpipe.newplayer.ui.ContentScale
import java.util.LinkedList
@ -144,7 +147,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
mutableUiState.update {
it.copy(playing = isPlaying, isLoading = false)
}
if(isPlaying && uiState.value.uiMode.controllerUiVisible) {
if (isPlaying && uiState.value.uiMode.controllerUiVisible) {
resetHideUiDelayedJob()
} else {
uiVisibilityJob?.cancel()
@ -209,7 +212,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
newPlayer.currentlyPlaying.collect { playlistItem ->
mutableUiState.update {
it.copy(
currentlyPlaying = playlistItem ?: PlaylistItem.DEFAULT,
currentlyPlaying = playlistItem,
currentPlaylistItemIndex = newPlayer.currentlyPlayingPlaylistItem
)
}
@ -243,6 +246,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
Log.d(TAG, "viewmodel cleared")
}
@OptIn(UnstableApi::class)
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun initUIState(instanceState: Bundle) {
@ -341,18 +345,20 @@ class VideoPlayerViewModelImpl @Inject constructor(
}
}
@OptIn(UnstableApi::class)
private fun updateProgressInPlaylistOnce() {
var progress = 0
val currentlyPlaying = uiState.value.currentlyPlaying.uniqueId
var progress = 0L
val currentlyPlaying = uiState.value.currentlyPlaying?.mediaId?.toLong() ?: 0L
for (item in uiState.value.playList) {
if (item.uniqueId == currentlyPlaying)
if (item.mediaId.toLong() == currentlyPlaying)
break;
progress += item.lengthInS
progress += item.mediaMetadata.durationMs
?: throw NewPlayerException("Media Item not containing duration. Media Item in question: ${item.mediaMetadata.title}")
}
progress += ((newPlayer?.currentPosition ?: 0) / 1000).toInt()
progress += ((newPlayer?.currentPosition ?: 0) / 1000)
mutableUiState.update {
it.copy(
playbackPositionInPlaylistS = progress
playbackPositionInPlaylistS = progress.toInt()
)
}
}

View File

@ -20,6 +20,7 @@
package net.newpipe.newplayer.ui.videoplayer
import androidx.annotation.OptIn
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
@ -37,11 +38,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.media3.common.util.UnstableApi
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.model.PlaylistItem
import net.newpipe.newplayer.ui.STREAMSELECT_UI_BACKGROUND_COLOR
import net.newpipe.newplayer.ui.videoplayer.streamselect.ChapterItem
import net.newpipe.newplayer.ui.videoplayer.streamselect.ChapterSelectTopBar
@ -56,6 +57,7 @@ import sh.calvin.reorderable.rememberReorderableLazyListState
val ITEM_CORNER_SHAPE = RoundedCornerShape(10.dp)
@OptIn(UnstableApi::class)
@Composable
fun StreamSelectUI(
isChapterSelect: Boolean = false,
@ -122,6 +124,7 @@ fun StreamSelectUI(
}
}
@OptIn(UnstableApi::class)
@Composable
fun ReorderableStreamItemsList(
padding: PaddingValues,
@ -144,10 +147,10 @@ fun ReorderableStreamItemsList(
verticalArrangement = Arrangement.spacedBy(5.dp),
state = lazyListState
) {
itemsIndexed(uiState.playList, key = { _, item -> item.uniqueId }) { index, playlistItem ->
itemsIndexed(uiState.playList, key = { _, item -> item.mediaId.toLong() }) { index, playlistItem ->
ReorderableItem(
state = reorderableLazyListState,
key = playlistItem.uniqueId
key = playlistItem.mediaId.toLong()
) { isDragging ->
StreamItem(
playlistItem = playlistItem,
@ -156,9 +159,9 @@ fun ReorderableStreamItemsList(
haptic = haptic,
onDragFinished = viewModel::onStreamItemDragFinished,
isDragging = isDragging,
isCurrentlyPlaying = playlistItem.uniqueId == uiState.currentlyPlaying.uniqueId,
isCurrentlyPlaying = playlistItem.mediaId.toLong() == uiState.currentlyPlaying?.mediaId?.toLong(),
onDelete = {
viewModel.removePlaylistItem(playlistItem.uniqueId)
viewModel.removePlaylistItem(playlistItem.mediaId.toLong())
}
)
}
@ -180,6 +183,7 @@ fun VideoPlayerChannelSelectUIPreview() {
}
}
@OptIn(UnstableApi::class)
@Preview(device = "id:pixel_5")
@Composable
fun VideoPlayerStreamSelectUIPreview() {
@ -188,9 +192,7 @@ fun VideoPlayerStreamSelectUIPreview() {
StreamSelectUI(
isChapterSelect = false,
viewModel = VideoPlayerViewModelDummy(),
uiState = VideoPlayerUIState.DUMMY.copy(
currentlyPlaying = PlaylistItem.DUMMY.copy(uniqueId = 1)
)
uiState = VideoPlayerUIState.DUMMY
)
}
}

View File

@ -21,6 +21,7 @@
package net.newpipe.newplayer.ui.videoplayer
import android.app.Activity
import androidx.annotation.OptIn
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -47,6 +48,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.media3.common.util.UnstableApi
import net.newpipe.newplayer.R
import net.newpipe.newplayer.model.VideoPlayerUIState
import net.newpipe.newplayer.model.VideoPlayerViewModel
@ -67,14 +69,14 @@ fun TopUI(
) {
Column(horizontalAlignment = Alignment.Start, modifier = Modifier.weight(1F)) {
Text(
uiState.currentlyPlaying.title,
uiState.currentlyPlaying?.mediaMetadata?.title.toString() ?: "",
fontSize = 15.sp,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
uiState.currentlyPlaying.creator,
uiState.currentlyPlaying?.mediaMetadata?.artist.toString() ?: "",
fontSize = 12.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
@ -131,6 +133,7 @@ fun TopUI(
// Preview
///////////////////////////////////////////////////////////////////
@OptIn(UnstableApi::class)
@Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape")
@Composable
fun VideoPlayerControllerTopUIPreview() {

View File

@ -59,8 +59,10 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.media3.common.MediaItem
import androidx.media3.common.util.UnstableApi
import net.newpipe.newplayer.R
import net.newpipe.newplayer.model.PlaylistItem
import net.newpipe.newplayer.model.VideoPlayerUIState
import net.newpipe.newplayer.ui.CONTROLLER_UI_BACKGROUND_COLOR
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
import net.newpipe.newplayer.ui.videoplayer.ITEM_CORNER_SHAPE
@ -71,11 +73,12 @@ import net.newpipe.newplayer.utils.getLocale
import net.newpipe.newplayer.utils.getTimeStringFromMs
import sh.calvin.reorderable.ReorderableCollectionItemScope
@androidx.annotation.OptIn(UnstableApi::class)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun StreamItem(
modifier: Modifier = Modifier,
playlistItem: PlaylistItem,
playlistItem: MediaItem,
onClicked: (Long) -> Unit,
onDragFinished: () -> Unit,
onDelete: () -> Unit,
@ -113,7 +116,7 @@ fun StreamItem(
.fillMaxSize()
.clip(ITEM_CORNER_SHAPE)
.clickable {
onClicked(playlistItem.uniqueId)
onClicked(playlistItem.mediaId.toLong())
}) {
androidx.compose.animation.AnimatedVisibility(
@ -152,7 +155,7 @@ fun StreamItem(
val contentDescription = stringResource(R.string.stream_item_thumbnail)
Thumbnail(
modifier = Modifier.fillMaxHeight(),
thumbnail = playlistItem.thumbnail,
thumbnail = playlistItem.mediaMetadata.artworkUri,
contentDescription = contentDescription,
shape = ITEM_CORNER_SHAPE
)
@ -172,7 +175,7 @@ fun StreamItem(
bottom = 0.5.dp
),
text = getTimeStringFromMs(
playlistItem.lengthInS * 1000L,
playlistItem.mediaMetadata.durationMs ?: 0L,
locale,
leadingZerosForMinutes = false
),
@ -189,14 +192,14 @@ fun StreamItem(
.fillMaxWidth()
) {
Text(
text = playlistItem.title,
text = playlistItem.mediaMetadata.title.toString(),
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = playlistItem.creator,
text = playlistItem.mediaMetadata.artist.toString(),
fontSize = 13.sp,
fontWeight = FontWeight.Light,
maxLines = 1,
@ -238,6 +241,7 @@ fun StreamItem(
}
@androidx.annotation.OptIn(UnstableApi::class)
@Preview(device = "spec:width=1080px,height=400px,dpi=440,orientation=landscape")
@Composable
fun StreamItemPreview() {
@ -245,7 +249,7 @@ fun StreamItemPreview() {
Surface(modifier = Modifier.fillMaxSize(), color = Color.DarkGray) {
Box(modifier = Modifier.fillMaxSize()) {
StreamItem(
playlistItem = PlaylistItem.DUMMY,
playlistItem = VideoPlayerUIState.DUMMY.currentlyPlaying!!,
onClicked = {},
reorderableScope = null,
haptic = null,

View File

@ -42,16 +42,18 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.media3.common.util.UnstableApi
import net.newpipe.newplayer.R
import net.newpipe.newplayer.RepeatMode
import net.newpipe.newplayer.model.VideoPlayerUIState
import net.newpipe.newplayer.model.VideoPlayerViewModel
import net.newpipe.newplayer.model.VideoPlayerViewModelDummy
import net.newpipe.newplayer.model.getPlaylistDurationInS
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
import net.newpipe.newplayer.utils.getLocale
import net.newpipe.newplayer.utils.getPlaylistDurationInMS
import net.newpipe.newplayer.utils.getTimeStringFromMs
@androidx.annotation.OptIn(UnstableApi::class)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun StreamSelectTopBar(
@ -64,7 +66,7 @@ fun StreamSelectTopBar(
colors = topAppBarColors(containerColor = Color.Transparent),
title = {
val locale = getLocale()!!
val duration = getPlaylistDurationInS(uiState.playList).toLong() * 1000
val duration = getPlaylistDurationInMS(uiState.playList)
val durationString = getTimeStringFromMs(timeSpanInMs = duration, locale)
val playbackPositionString = getTimeStringFromMs(
timeSpanInMs = uiState.playbackPositionInPlaylistS.toLong() * 1000, locale = locale
@ -133,6 +135,7 @@ fun StreamSelectTopBar(
}
@androidx.annotation.OptIn(UnstableApi::class)
@Preview(device = "spec:width=1080px,height=150px,dpi=440,orientation=landscape")
@Composable
fun StreamSelectTopBarPreview() {

View File

@ -27,6 +27,8 @@ import android.content.ContextWrapper
import android.graphics.drawable.shapes.Shape
import android.net.Uri
import android.view.WindowManager
import androidx.annotation.OptIn
import androidx.compose.animation.core.withInfiniteAnimationFrameMillis
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.displayCutout
@ -47,7 +49,10 @@ import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.core.os.ConfigurationCompat
import androidx.core.view.WindowCompat
import androidx.media3.common.MediaItem
import androidx.media3.common.util.UnstableApi
import coil.compose.AsyncImage
import net.newpipe.newplayer.NewPlayerException
import net.newpipe.newplayer.R
import net.newpipe.newplayer.model.EmbeddedUiConfig
import java.util.Locale
@ -178,3 +183,14 @@ fun Thumbnail(
)
}
}
@OptIn(UnstableApi::class)
fun getPlaylistDurationInMS(playlist: List<MediaItem>) : Long {
var duration = 0L
for(item in playlist) {
val itemDuration = item.mediaMetadata.durationMs ?:
throw NewPlayerException("Can not calculate duration of a playlist if an item does not have a duration: MediItem in question: ${item.mediaMetadata.title}")
duration += itemDuration
}
return duration
}

View File

@ -2,11 +2,12 @@ package net.newpipe.newplayer.testapp
import android.content.Context
import android.net.Uri
import androidx.annotation.OptIn
import androidx.media3.common.MediaMetadata
import androidx.media3.common.PlaybackException
import androidx.media3.common.util.UnstableApi
import net.newpipe.newplayer.Chapter
import net.newpipe.newplayer.MediaRepository
import net.newpipe.newplayer.MetaInfo
import net.newpipe.newplayer.utils.Thumbnail
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
@ -21,28 +22,35 @@ class TestMediaRepository(val context: Context) : MediaRepository {
return client.newCall(request).execute()
}
override suspend fun getMetaInfo(item: String) =
@OptIn(UnstableApi::class)
override suspend fun getMetaInfo(item: String): MediaMetadata =
when (item) {
"6502" -> MetaInfo(
title = context.getString(R.string.ccc_6502_title),
channelName = context.getString(R.string.ccc_6502_channel),
thumbnail = Uri.parse(context.getString(R.string.ccc_6502_thumbnail)),
lengthInS = context.resources.getInteger(R.integer.ccc_6502_length)
)
"6502" -> MediaMetadata.Builder()
.setTitle(context.getString(R.string.ccc_6502_title))
.setArtist(context.getString(R.string.ccc_6502_channel))
.setArtworkUri(Uri.parse(context.getString(R.string.ccc_6502_thumbnail)))
.setDurationMs(
context.resources.getInteger(R.integer.ccc_6502_length).toLong() * 1000L
)
.build()
"imu" -> MetaInfo(
title = context.getString(R.string.ccc_imu_title),
channelName = context.getString(R.string.ccc_imu_channel),
thumbnail = Uri.parse(context.getString(R.string.ccc_imu_thumbnail)),
lengthInS = context.resources.getInteger(R.integer.ccc_imu_length)
)
"imu" -> MediaMetadata.Builder()
.setTitle(context.getString(R.string.ccc_imu_title))
.setArtist(context.getString(R.string.ccc_imu_channel))
.setArtworkUri(Uri.parse(context.getString(R.string.ccc_imu_thumbnail)))
.setDurationMs(
context.resources.getInteger(R.integer.ccc_imu_length).toLong() * 1000L
)
.build()
"portrait" -> MetaInfo(
title = context.getString(R.string.portrait_title),
channelName = context.getString(R.string.portrait_channel),
thumbnail = null,
lengthInS = context.resources.getInteger(R.integer.portrait_length)
)
"portrait" -> MediaMetadata.Builder()
.setTitle(context.getString(R.string.portrait_title))
.setArtist(context.getString(R.string.portrait_channel))
.setArtworkUri(null)
.setDurationMs(
context.resources.getInteger(R.integer.portrait_length).toLong() * 1000L
)
.build()
else -> throw Exception("Unknown stream: $item")
}