remove playlist item and replace it with MediaItem and MediaMetadata
This commit is contained in:
parent
5710596972
commit
a297a4c08f
|
@ -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" />
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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(),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue