remove playlist item and replace it with MediaItem and MediaMetadata
This commit is contained in:
parent
5710596972
commit
a297a4c08f
|
@ -4,7 +4,7 @@
|
||||||
<selectionStates>
|
<selectionStates>
|
||||||
<SelectionState runConfigName="TestApp">
|
<SelectionState runConfigName="TestApp">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<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">
|
<Target type="DEFAULT_BOOT">
|
||||||
<handle>
|
<handle>
|
||||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=981f7af2" />
|
<DeviceId pluginId="PhysicalDevice" identifier="serial=981f7af2" />
|
||||||
|
|
|
@ -21,22 +21,15 @@
|
||||||
package net.newpipe.newplayer
|
package net.newpipe.newplayer
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.media3.common.MediaMetadata
|
||||||
import androidx.media3.common.PlaybackException
|
import androidx.media3.common.PlaybackException
|
||||||
import net.newpipe.newplayer.utils.Thumbnail
|
import net.newpipe.newplayer.utils.Thumbnail
|
||||||
|
|
||||||
data class Chapter(val chapterStartInMs: Long, val chapterTitle: String?, val thumbnail: Uri?)
|
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 {
|
interface MediaRepository {
|
||||||
|
|
||||||
suspend fun getMetaInfo(item: String): MetaInfo
|
suspend fun getMetaInfo(item: String): MediaMetadata
|
||||||
|
|
||||||
suspend fun getAvailableStreamVariants(item: String): List<String>
|
suspend fun getAvailableStreamVariants(item: String): List<String>
|
||||||
suspend fun getStream(item: String, streamSelector: String): Uri
|
suspend fun getStream(item: String, streamSelector: String): Uri
|
||||||
|
|
|
@ -21,29 +21,14 @@
|
||||||
package net.newpipe.newplayer
|
package net.newpipe.newplayer
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.util.Log
|
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.PlaybackException
|
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.Timeline
|
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
import androidx.media3.exoplayer.source.MediaSource
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
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.Exception
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
enum class PlayMode {
|
enum class PlayMode {
|
||||||
IDLE,
|
IDLE,
|
||||||
|
@ -74,8 +59,8 @@ interface NewPlayer {
|
||||||
var shuffle: Boolean
|
var shuffle: Boolean
|
||||||
var repeatMode: RepeatMode
|
var repeatMode: RepeatMode
|
||||||
|
|
||||||
val playlist: StateFlow<List<PlaylistItem>>
|
val playlist: StateFlow<List<MediaItem>>
|
||||||
val currentlyPlaying: StateFlow<PlaylistItem?>
|
val currentlyPlaying: StateFlow<MediaItem?>
|
||||||
var currentlyPlayingPlaylistItem: Int
|
var currentlyPlayingPlaylistItem: Int
|
||||||
|
|
||||||
val currentChapters: StateFlow<List<Chapter>>
|
val currentChapters: StateFlow<List<Chapter>>
|
||||||
|
|
|
@ -43,9 +43,6 @@ import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
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.model.PlaylistItem
|
|
||||||
import net.newpipe.newplayer.model.fetchPlaylistItem
|
|
||||||
import net.newpipe.newplayer.model.getPlaylistItemsFromExoplayer
|
|
||||||
import net.newpipe.newplayer.service.NewPlayerService
|
import net.newpipe.newplayer.service.NewPlayerService
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
@ -116,12 +113,12 @@ class NewPlayerImpl(
|
||||||
override val duration: Long
|
override val duration: Long
|
||||||
get() = internalPlayer.duration
|
get() = internalPlayer.duration
|
||||||
|
|
||||||
private val mutablePlaylist = MutableStateFlow<List<PlaylistItem>>(emptyList())
|
private val mutablePlaylist = MutableStateFlow<List<MediaItem>>(emptyList())
|
||||||
override val playlist: StateFlow<List<PlaylistItem>> =
|
override val playlist: StateFlow<List<MediaItem>> =
|
||||||
mutablePlaylist.asStateFlow()
|
mutablePlaylist.asStateFlow()
|
||||||
|
|
||||||
private val mutableCurrentlyPlaying = MutableStateFlow<PlaylistItem?>(null)
|
private val mutableCurrentlyPlaying = MutableStateFlow<MediaItem?>(null)
|
||||||
override val currentlyPlaying: StateFlow<PlaylistItem?> = mutableCurrentlyPlaying.asStateFlow()
|
override val currentlyPlaying: StateFlow<MediaItem?> = mutableCurrentlyPlaying.asStateFlow()
|
||||||
|
|
||||||
private val mutableCurrentChapter = MutableStateFlow<List<Chapter>>(emptyList())
|
private val mutableCurrentChapter = MutableStateFlow<List<Chapter>>(emptyList())
|
||||||
override val currentChapters: StateFlow<List<Chapter>> = mutableCurrentChapter.asStateFlow()
|
override val currentChapters: StateFlow<List<Chapter>> = mutableCurrentChapter.asStateFlow()
|
||||||
|
@ -157,29 +154,16 @@ class NewPlayerImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
|
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
|
||||||
super.onTimelineChanged(timeline, reason)
|
mutablePlaylist.update {
|
||||||
updatePlaylistItems()
|
(0..<internalPlayer.mediaItemCount).map {
|
||||||
|
internalPlayer.getMediaItemAt(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||||
super.onMediaItemTransition(mediaItem, reason)
|
super.onMediaItemTransition(mediaItem, reason)
|
||||||
mediaItem?.let {
|
mutableCurrentlyPlaying.update { mediaItem }
|
||||||
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 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -187,7 +171,8 @@ class NewPlayerImpl(
|
||||||
currentlyPlaying.collect { playing ->
|
currentlyPlaying.collect { playing ->
|
||||||
playing?.let {
|
playing?.let {
|
||||||
try {
|
try {
|
||||||
val chapters = repository.getChapters(playing.id)
|
val chapters =
|
||||||
|
repository.getChapters(uniqueIdToIdLookup[playing.mediaId.toLong()]!!)
|
||||||
mutableCurrentChapter.update { chapters }
|
mutableCurrentChapter.update { chapters }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
mutableErrorFlow.emit(e)
|
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() {
|
override fun prepare() {
|
||||||
internalPlayer.prepare()
|
internalPlayer.prepare()
|
||||||
if (mediaController == null) {
|
if (mediaController == null) {
|
||||||
|
@ -309,10 +265,21 @@ 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[uniqueId] = item
|
uniqueIdToIdLookup[uniqueId] = item
|
||||||
val mediaItem = MediaItem.Builder().setMediaId(uniqueId.toString()).setUri(dataStream)
|
val mediaItemBuilder = MediaItem.Builder()
|
||||||
return mediaItem.build()
|
.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 {
|
private suspend fun toMediaItem(item: String): MediaItem {
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
package net.newpipe.newplayer.model
|
package net.newpipe.newplayer.model
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
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
|
||||||
|
@ -30,90 +31,3 @@ import net.newpipe.newplayer.NewPlayerException
|
||||||
import net.newpipe.newplayer.utils.Thumbnail
|
import net.newpipe.newplayer.utils.Thumbnail
|
||||||
import kotlin.coroutines.coroutineContext
|
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
|
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.Chapter
|
||||||
import net.newpipe.newplayer.RepeatMode
|
import net.newpipe.newplayer.RepeatMode
|
||||||
import net.newpipe.newplayer.ui.ContentScale
|
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(
|
data class VideoPlayerUIState(
|
||||||
val uiMode: UIModeState,
|
val uiMode: UIModeState,
|
||||||
val playing: Boolean,
|
val playing: Boolean,
|
||||||
|
@ -40,12 +68,12 @@ data class VideoPlayerUIState(
|
||||||
val soundVolume: Float,
|
val soundVolume: Float,
|
||||||
val brightness: Float?, // when null use system value
|
val brightness: Float?, // when null use system value
|
||||||
val embeddedUiConfig: EmbeddedUiConfig?,
|
val embeddedUiConfig: EmbeddedUiConfig?,
|
||||||
val playList: List<PlaylistItem>,
|
val playList: List<MediaItem>,
|
||||||
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,
|
val currentlyPlaying: MediaItem?,
|
||||||
val currentPlaylistItemIndex: Int
|
val currentPlaylistItemIndex: Int
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -70,7 +98,7 @@ data class VideoPlayerUIState(
|
||||||
shuffleEnabled = false,
|
shuffleEnabled = false,
|
||||||
repeatMode = RepeatMode.DONT_REPEAT,
|
repeatMode = RepeatMode.DONT_REPEAT,
|
||||||
playListDurationInS = 0,
|
playListDurationInS = 0,
|
||||||
currentlyPlaying = PlaylistItem.DEFAULT,
|
currentlyPlaying = null,
|
||||||
currentPlaylistItemIndex = 0
|
currentPlaylistItemIndex = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -88,7 +116,16 @@ data class VideoPlayerUIState(
|
||||||
brightness = 0.2f,
|
brightness = 0.2f,
|
||||||
shuffleEnabled = true,
|
shuffleEnabled = true,
|
||||||
playListDurationInS = 5493,
|
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,
|
currentPlaylistItemIndex = 1,
|
||||||
chapters = arrayListOf(
|
chapters = arrayListOf(
|
||||||
Chapter(
|
Chapter(
|
||||||
|
@ -108,30 +145,36 @@ data class VideoPlayerUIState(
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
playList = arrayListOf(
|
playList = arrayListOf(
|
||||||
PlaylistItem(
|
MediaItem.Builder()
|
||||||
id = "6502",
|
.setUri("https://ftp.fau.de/cdn.media.ccc.de/congress/2010/mp4-h264-HQ/27c3-4159-en-reverse_engineering_mos_6502.mp4")
|
||||||
title = "Stream 1",
|
.setMediaId("0")
|
||||||
creator = "The Creator",
|
.setMediaMetadata(MediaMetadata.Builder()
|
||||||
lengthInS = 6 * 60 + 5,
|
.setTitle("Stream 1")
|
||||||
thumbnail = null,
|
.setArtist("Yours truely")
|
||||||
uniqueId = 0
|
.setArtworkUri(null)
|
||||||
),
|
.setDurationMs(4201000L)
|
||||||
PlaylistItem(
|
.build())
|
||||||
id = "6502",
|
.build(),
|
||||||
title = "Stream 2",
|
MediaItem.Builder()
|
||||||
creator = "The Creator 2",
|
.setUri("https://ftp.fau.de/cdn.media.ccc.de/congress/2010/mp4-h264-HQ/27c3-4159-en-reverse_engineering_mos_6502.mp4")
|
||||||
lengthInS = 2 * 60 + 5,
|
.setMediaId("1")
|
||||||
thumbnail = null,
|
.setMediaMetadata(MediaMetadata.Builder()
|
||||||
uniqueId = 1
|
.setTitle("Stream 2")
|
||||||
),
|
.setArtist("Yours truely")
|
||||||
PlaylistItem(
|
.setArtworkUri(null)
|
||||||
id = "6502",
|
.setDurationMs(3201000L)
|
||||||
title = "Stream 3",
|
.build())
|
||||||
creator = "The Creator 3",
|
.build(),
|
||||||
lengthInS = 29 * 60 + 5,
|
MediaItem.Builder()
|
||||||
thumbnail = null,
|
.setUri("https://ftp.fau.de/cdn.media.ccc.de/congress/2010/mp4-h264-HQ/27c3-4159-en-reverse_engineering_mos_6502.mp4")
|
||||||
uniqueId = 2
|
.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.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.content.ContextCompat.getSystemService
|
import androidx.core.content.ContextCompat.getSystemService
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
@ -44,6 +46,7 @@ import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
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.NewPlayerException
|
||||||
import net.newpipe.newplayer.RepeatMode
|
import net.newpipe.newplayer.RepeatMode
|
||||||
import net.newpipe.newplayer.ui.ContentScale
|
import net.newpipe.newplayer.ui.ContentScale
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
|
@ -144,7 +147,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
|
||||||
mutableUiState.update {
|
mutableUiState.update {
|
||||||
it.copy(playing = isPlaying, isLoading = false)
|
it.copy(playing = isPlaying, isLoading = false)
|
||||||
}
|
}
|
||||||
if(isPlaying && uiState.value.uiMode.controllerUiVisible) {
|
if (isPlaying && uiState.value.uiMode.controllerUiVisible) {
|
||||||
resetHideUiDelayedJob()
|
resetHideUiDelayedJob()
|
||||||
} else {
|
} else {
|
||||||
uiVisibilityJob?.cancel()
|
uiVisibilityJob?.cancel()
|
||||||
|
@ -209,7 +212,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
|
||||||
newPlayer.currentlyPlaying.collect { playlistItem ->
|
newPlayer.currentlyPlaying.collect { playlistItem ->
|
||||||
mutableUiState.update {
|
mutableUiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
currentlyPlaying = playlistItem ?: PlaylistItem.DEFAULT,
|
currentlyPlaying = playlistItem,
|
||||||
currentPlaylistItemIndex = newPlayer.currentlyPlayingPlaylistItem
|
currentPlaylistItemIndex = newPlayer.currentlyPlayingPlaylistItem
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -243,6 +246,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
|
||||||
Log.d(TAG, "viewmodel cleared")
|
Log.d(TAG, "viewmodel cleared")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||||
override fun initUIState(instanceState: Bundle) {
|
override fun initUIState(instanceState: Bundle) {
|
||||||
|
|
||||||
|
@ -341,18 +345,20 @@ class VideoPlayerViewModelImpl @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
private fun updateProgressInPlaylistOnce() {
|
private fun updateProgressInPlaylistOnce() {
|
||||||
var progress = 0
|
var progress = 0L
|
||||||
val currentlyPlaying = uiState.value.currentlyPlaying.uniqueId
|
val currentlyPlaying = uiState.value.currentlyPlaying?.mediaId?.toLong() ?: 0L
|
||||||
for (item in uiState.value.playList) {
|
for (item in uiState.value.playList) {
|
||||||
if (item.uniqueId == currentlyPlaying)
|
if (item.mediaId.toLong() == currentlyPlaying)
|
||||||
break;
|
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 {
|
mutableUiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
playbackPositionInPlaylistS = progress
|
playbackPositionInPlaylistS = progress.toInt()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
|
|
||||||
package net.newpipe.newplayer.ui.videoplayer
|
package net.newpipe.newplayer.ui.videoplayer
|
||||||
|
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
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.graphics.Color
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
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.ui.theme.VideoPlayerTheme
|
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.STREAMSELECT_UI_BACKGROUND_COLOR
|
||||||
import net.newpipe.newplayer.ui.videoplayer.streamselect.ChapterItem
|
import net.newpipe.newplayer.ui.videoplayer.streamselect.ChapterItem
|
||||||
import net.newpipe.newplayer.ui.videoplayer.streamselect.ChapterSelectTopBar
|
import net.newpipe.newplayer.ui.videoplayer.streamselect.ChapterSelectTopBar
|
||||||
|
@ -56,6 +57,7 @@ import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||||
|
|
||||||
val ITEM_CORNER_SHAPE = RoundedCornerShape(10.dp)
|
val ITEM_CORNER_SHAPE = RoundedCornerShape(10.dp)
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun StreamSelectUI(
|
fun StreamSelectUI(
|
||||||
isChapterSelect: Boolean = false,
|
isChapterSelect: Boolean = false,
|
||||||
|
@ -122,6 +124,7 @@ fun StreamSelectUI(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ReorderableStreamItemsList(
|
fun ReorderableStreamItemsList(
|
||||||
padding: PaddingValues,
|
padding: PaddingValues,
|
||||||
|
@ -144,10 +147,10 @@ fun ReorderableStreamItemsList(
|
||||||
verticalArrangement = Arrangement.spacedBy(5.dp),
|
verticalArrangement = Arrangement.spacedBy(5.dp),
|
||||||
state = lazyListState
|
state = lazyListState
|
||||||
) {
|
) {
|
||||||
itemsIndexed(uiState.playList, key = { _, item -> item.uniqueId }) { index, playlistItem ->
|
itemsIndexed(uiState.playList, key = { _, item -> item.mediaId.toLong() }) { index, playlistItem ->
|
||||||
ReorderableItem(
|
ReorderableItem(
|
||||||
state = reorderableLazyListState,
|
state = reorderableLazyListState,
|
||||||
key = playlistItem.uniqueId
|
key = playlistItem.mediaId.toLong()
|
||||||
) { isDragging ->
|
) { isDragging ->
|
||||||
StreamItem(
|
StreamItem(
|
||||||
playlistItem = playlistItem,
|
playlistItem = playlistItem,
|
||||||
|
@ -156,9 +159,9 @@ fun ReorderableStreamItemsList(
|
||||||
haptic = haptic,
|
haptic = haptic,
|
||||||
onDragFinished = viewModel::onStreamItemDragFinished,
|
onDragFinished = viewModel::onStreamItemDragFinished,
|
||||||
isDragging = isDragging,
|
isDragging = isDragging,
|
||||||
isCurrentlyPlaying = playlistItem.uniqueId == uiState.currentlyPlaying.uniqueId,
|
isCurrentlyPlaying = playlistItem.mediaId.toLong() == uiState.currentlyPlaying?.mediaId?.toLong(),
|
||||||
onDelete = {
|
onDelete = {
|
||||||
viewModel.removePlaylistItem(playlistItem.uniqueId)
|
viewModel.removePlaylistItem(playlistItem.mediaId.toLong())
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -180,6 +183,7 @@ fun VideoPlayerChannelSelectUIPreview() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
@Preview(device = "id:pixel_5")
|
@Preview(device = "id:pixel_5")
|
||||||
@Composable
|
@Composable
|
||||||
fun VideoPlayerStreamSelectUIPreview() {
|
fun VideoPlayerStreamSelectUIPreview() {
|
||||||
|
@ -188,9 +192,7 @@ fun VideoPlayerStreamSelectUIPreview() {
|
||||||
StreamSelectUI(
|
StreamSelectUI(
|
||||||
isChapterSelect = false,
|
isChapterSelect = false,
|
||||||
viewModel = VideoPlayerViewModelDummy(),
|
viewModel = VideoPlayerViewModelDummy(),
|
||||||
uiState = VideoPlayerUIState.DUMMY.copy(
|
uiState = VideoPlayerUIState.DUMMY
|
||||||
currentlyPlaying = PlaylistItem.DUMMY.copy(uniqueId = 1)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
package net.newpipe.newplayer.ui.videoplayer
|
package net.newpipe.newplayer.ui.videoplayer
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
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.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import net.newpipe.newplayer.R
|
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
|
||||||
|
@ -67,14 +69,14 @@ fun TopUI(
|
||||||
) {
|
) {
|
||||||
Column(horizontalAlignment = Alignment.Start, modifier = Modifier.weight(1F)) {
|
Column(horizontalAlignment = Alignment.Start, modifier = Modifier.weight(1F)) {
|
||||||
Text(
|
Text(
|
||||||
uiState.currentlyPlaying.title,
|
uiState.currentlyPlaying?.mediaMetadata?.title.toString() ?: "",
|
||||||
fontSize = 15.sp,
|
fontSize = 15.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
uiState.currentlyPlaying.creator,
|
uiState.currentlyPlaying?.mediaMetadata?.artist.toString() ?: "",
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
|
@ -131,6 +133,7 @@ fun TopUI(
|
||||||
// Preview
|
// Preview
|
||||||
///////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
@Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape")
|
@Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape")
|
||||||
@Composable
|
@Composable
|
||||||
fun VideoPlayerControllerTopUIPreview() {
|
fun VideoPlayerControllerTopUIPreview() {
|
||||||
|
|
|
@ -59,8 +59,10 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
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.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.CONTROLLER_UI_BACKGROUND_COLOR
|
||||||
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
|
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
|
||||||
import net.newpipe.newplayer.ui.videoplayer.ITEM_CORNER_SHAPE
|
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 net.newpipe.newplayer.utils.getTimeStringFromMs
|
||||||
import sh.calvin.reorderable.ReorderableCollectionItemScope
|
import sh.calvin.reorderable.ReorderableCollectionItemScope
|
||||||
|
|
||||||
|
@androidx.annotation.OptIn(UnstableApi::class)
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun StreamItem(
|
fun StreamItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
playlistItem: PlaylistItem,
|
playlistItem: MediaItem,
|
||||||
onClicked: (Long) -> Unit,
|
onClicked: (Long) -> Unit,
|
||||||
onDragFinished: () -> Unit,
|
onDragFinished: () -> Unit,
|
||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
|
@ -113,7 +116,7 @@ fun StreamItem(
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clip(ITEM_CORNER_SHAPE)
|
.clip(ITEM_CORNER_SHAPE)
|
||||||
.clickable {
|
.clickable {
|
||||||
onClicked(playlistItem.uniqueId)
|
onClicked(playlistItem.mediaId.toLong())
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
androidx.compose.animation.AnimatedVisibility(
|
androidx.compose.animation.AnimatedVisibility(
|
||||||
|
@ -152,7 +155,7 @@ fun StreamItem(
|
||||||
val contentDescription = stringResource(R.string.stream_item_thumbnail)
|
val contentDescription = stringResource(R.string.stream_item_thumbnail)
|
||||||
Thumbnail(
|
Thumbnail(
|
||||||
modifier = Modifier.fillMaxHeight(),
|
modifier = Modifier.fillMaxHeight(),
|
||||||
thumbnail = playlistItem.thumbnail,
|
thumbnail = playlistItem.mediaMetadata.artworkUri,
|
||||||
contentDescription = contentDescription,
|
contentDescription = contentDescription,
|
||||||
shape = ITEM_CORNER_SHAPE
|
shape = ITEM_CORNER_SHAPE
|
||||||
)
|
)
|
||||||
|
@ -172,7 +175,7 @@ fun StreamItem(
|
||||||
bottom = 0.5.dp
|
bottom = 0.5.dp
|
||||||
),
|
),
|
||||||
text = getTimeStringFromMs(
|
text = getTimeStringFromMs(
|
||||||
playlistItem.lengthInS * 1000L,
|
playlistItem.mediaMetadata.durationMs ?: 0L,
|
||||||
locale,
|
locale,
|
||||||
leadingZerosForMinutes = false
|
leadingZerosForMinutes = false
|
||||||
),
|
),
|
||||||
|
@ -189,14 +192,14 @@ fun StreamItem(
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = playlistItem.title,
|
text = playlistItem.mediaMetadata.title.toString(),
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = playlistItem.creator,
|
text = playlistItem.mediaMetadata.artist.toString(),
|
||||||
fontSize = 13.sp,
|
fontSize = 13.sp,
|
||||||
fontWeight = FontWeight.Light,
|
fontWeight = FontWeight.Light,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
@ -238,6 +241,7 @@ fun StreamItem(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@androidx.annotation.OptIn(UnstableApi::class)
|
||||||
@Preview(device = "spec:width=1080px,height=400px,dpi=440,orientation=landscape")
|
@Preview(device = "spec:width=1080px,height=400px,dpi=440,orientation=landscape")
|
||||||
@Composable
|
@Composable
|
||||||
fun StreamItemPreview() {
|
fun StreamItemPreview() {
|
||||||
|
@ -245,7 +249,7 @@ fun StreamItemPreview() {
|
||||||
Surface(modifier = Modifier.fillMaxSize(), color = Color.DarkGray) {
|
Surface(modifier = Modifier.fillMaxSize(), color = Color.DarkGray) {
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
StreamItem(
|
StreamItem(
|
||||||
playlistItem = PlaylistItem.DUMMY,
|
playlistItem = VideoPlayerUIState.DUMMY.currentlyPlaying!!,
|
||||||
onClicked = {},
|
onClicked = {},
|
||||||
reorderableScope = null,
|
reorderableScope = null,
|
||||||
haptic = null,
|
haptic = null,
|
||||||
|
|
|
@ -42,16 +42,18 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import net.newpipe.newplayer.R
|
import net.newpipe.newplayer.R
|
||||||
import net.newpipe.newplayer.RepeatMode
|
import net.newpipe.newplayer.RepeatMode
|
||||||
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.model.getPlaylistDurationInS
|
|
||||||
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
|
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
|
||||||
import net.newpipe.newplayer.utils.getLocale
|
import net.newpipe.newplayer.utils.getLocale
|
||||||
|
import net.newpipe.newplayer.utils.getPlaylistDurationInMS
|
||||||
import net.newpipe.newplayer.utils.getTimeStringFromMs
|
import net.newpipe.newplayer.utils.getTimeStringFromMs
|
||||||
|
|
||||||
|
@androidx.annotation.OptIn(UnstableApi::class)
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun StreamSelectTopBar(
|
fun StreamSelectTopBar(
|
||||||
|
@ -64,7 +66,7 @@ fun StreamSelectTopBar(
|
||||||
colors = topAppBarColors(containerColor = Color.Transparent),
|
colors = topAppBarColors(containerColor = Color.Transparent),
|
||||||
title = {
|
title = {
|
||||||
val locale = getLocale()!!
|
val locale = getLocale()!!
|
||||||
val duration = getPlaylistDurationInS(uiState.playList).toLong() * 1000
|
val duration = getPlaylistDurationInMS(uiState.playList)
|
||||||
val durationString = getTimeStringFromMs(timeSpanInMs = duration, locale)
|
val durationString = getTimeStringFromMs(timeSpanInMs = duration, locale)
|
||||||
val playbackPositionString = getTimeStringFromMs(
|
val playbackPositionString = getTimeStringFromMs(
|
||||||
timeSpanInMs = uiState.playbackPositionInPlaylistS.toLong() * 1000, locale = locale
|
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")
|
@Preview(device = "spec:width=1080px,height=150px,dpi=440,orientation=landscape")
|
||||||
@Composable
|
@Composable
|
||||||
fun StreamSelectTopBarPreview() {
|
fun StreamSelectTopBarPreview() {
|
||||||
|
|
|
@ -27,6 +27,8 @@ import android.content.ContextWrapper
|
||||||
import android.graphics.drawable.shapes.Shape
|
import android.graphics.drawable.shapes.Shape
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.compose.animation.core.withInfiniteAnimationFrameMillis
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.displayCutout
|
import androidx.compose.foundation.layout.displayCutout
|
||||||
|
@ -47,7 +49,10 @@ import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.core.os.ConfigurationCompat
|
import androidx.core.os.ConfigurationCompat
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.media3.common.MediaItem
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
|
import net.newpipe.newplayer.NewPlayerException
|
||||||
import net.newpipe.newplayer.R
|
import net.newpipe.newplayer.R
|
||||||
import net.newpipe.newplayer.model.EmbeddedUiConfig
|
import net.newpipe.newplayer.model.EmbeddedUiConfig
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
@ -177,4 +182,15 @@ fun Thumbnail(
|
||||||
contentDescription = contentDescription
|
contentDescription = contentDescription
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.media3.common.MediaMetadata
|
||||||
import androidx.media3.common.PlaybackException
|
import androidx.media3.common.PlaybackException
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import net.newpipe.newplayer.Chapter
|
import net.newpipe.newplayer.Chapter
|
||||||
import net.newpipe.newplayer.MediaRepository
|
import net.newpipe.newplayer.MediaRepository
|
||||||
import net.newpipe.newplayer.MetaInfo
|
|
||||||
import net.newpipe.newplayer.utils.Thumbnail
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
@ -21,28 +22,35 @@ class TestMediaRepository(val context: Context) : MediaRepository {
|
||||||
return client.newCall(request).execute()
|
return client.newCall(request).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getMetaInfo(item: String) =
|
@OptIn(UnstableApi::class)
|
||||||
|
override suspend fun getMetaInfo(item: String): MediaMetadata =
|
||||||
when (item) {
|
when (item) {
|
||||||
"6502" -> MetaInfo(
|
"6502" -> MediaMetadata.Builder()
|
||||||
title = context.getString(R.string.ccc_6502_title),
|
.setTitle(context.getString(R.string.ccc_6502_title))
|
||||||
channelName = context.getString(R.string.ccc_6502_channel),
|
.setArtist(context.getString(R.string.ccc_6502_channel))
|
||||||
thumbnail = Uri.parse(context.getString(R.string.ccc_6502_thumbnail)),
|
.setArtworkUri(Uri.parse(context.getString(R.string.ccc_6502_thumbnail)))
|
||||||
lengthInS = context.resources.getInteger(R.integer.ccc_6502_length)
|
.setDurationMs(
|
||||||
)
|
context.resources.getInteger(R.integer.ccc_6502_length).toLong() * 1000L
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
|
||||||
"imu" -> MetaInfo(
|
"imu" -> MediaMetadata.Builder()
|
||||||
title = context.getString(R.string.ccc_imu_title),
|
.setTitle(context.getString(R.string.ccc_imu_title))
|
||||||
channelName = context.getString(R.string.ccc_imu_channel),
|
.setArtist(context.getString(R.string.ccc_imu_channel))
|
||||||
thumbnail = Uri.parse(context.getString(R.string.ccc_imu_thumbnail)),
|
.setArtworkUri(Uri.parse(context.getString(R.string.ccc_imu_thumbnail)))
|
||||||
lengthInS = context.resources.getInteger(R.integer.ccc_imu_length)
|
.setDurationMs(
|
||||||
)
|
context.resources.getInteger(R.integer.ccc_imu_length).toLong() * 1000L
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
|
||||||
"portrait" -> MetaInfo(
|
"portrait" -> MediaMetadata.Builder()
|
||||||
title = context.getString(R.string.portrait_title),
|
.setTitle(context.getString(R.string.portrait_title))
|
||||||
channelName = context.getString(R.string.portrait_channel),
|
.setArtist(context.getString(R.string.portrait_channel))
|
||||||
thumbnail = null,
|
.setArtworkUri(null)
|
||||||
lengthInS = context.resources.getInteger(R.integer.portrait_length)
|
.setDurationMs(
|
||||||
)
|
context.resources.getInteger(R.integer.portrait_length).toLong() * 1000L
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
|
||||||
else -> throw Exception("Unknown stream: $item")
|
else -> throw Exception("Unknown stream: $item")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue