forward player playlist into viewmodel

This commit is contained in:
Christian Schabesberger 2024-08-26 19:16:10 +02:00
parent 99b79816f0
commit 888d518304
10 changed files with 178 additions and 57 deletions

View File

@ -21,28 +21,34 @@
package net.newpipe.newplayer
import android.net.Uri
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import net.newpipe.newplayer.utils.Thumbnail
data class Chapter(val chapterStartInMs: Long, val chapterTitle: String?)
data class MetaInfo(
val title: String,
val channelName: String,
val thumbnail: Thumbnail?,
val lengthInS: Int
)
interface MediaRepository {
suspend fun getTitle(item: String) : String
suspend fun getChannelName(item: String): String
suspend fun getThumbnail(item: String): Thumbnail
suspend fun getMetaInfo(item: String): MetaInfo
suspend fun getAvailableStreamVariants(item: String): List<String>
suspend fun getStream(item: String, streamSelector: String) : Uri
suspend fun getStream(item: String, streamSelector: String): Uri
suspend fun getAvailableSubtitleVariants(item: String): List<String>
suspend fun getSubtitle(item: String, variant: String): Uri
suspend fun getPreviewThumbnails(item: String) : HashMap<Long, Thumbnail>?
suspend fun getPreviewThumbnails(item: String): HashMap<Long, Thumbnail>?
suspend fun getChapters(item: String): List<Chapter>
suspend fun getChapterThumbnail(item: String, chapter: Long) : Thumbnail?
suspend fun getChapterThumbnail(item: String, chapter: Long): Thumbnail?
suspend fun getTimestampLink(item: String, timestampInSeconds: Long): String
suspend fun tryAndRescueError(item: String?, exception: PlaybackException) : Uri?
suspend fun tryAndRescueError(item: String?, exception: PlaybackException): Uri?
}

View File

@ -38,7 +38,7 @@ import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import net.newpipe.newplayer.utils.PlayList
import net.newpipe.newplayer.playerInternals.PlayList
import kotlin.Exception
enum class PlayMode {
@ -60,6 +60,7 @@ interface NewPlayer {
val duration: Long
val bufferedPercentage: Int
val repository: MediaRepository
val sharingLinkWithOffsetPossible: Boolean
var currentPosition: Long
var fastSeekAmountSec: Int
var playBackMode: PlayMode
@ -84,13 +85,21 @@ interface NewPlayer {
data class Builder(val app: Application, val repository: MediaRepository) {
private var mediaSourceFactory: MediaSource.Factory? = null
private var preferredStreamVariants: List<String> = emptyList()
private var sharingLinkWithOffsetPossible = false
fun setMediaSourceFactory(mediaSourceFactory: MediaSource.Factory) {
fun setMediaSourceFactory(mediaSourceFactory: MediaSource.Factory) : Builder {
this.mediaSourceFactory = mediaSourceFactory
return this
}
fun setPreferredStreamVariants(preferredStreamVariants: List<String>) {
fun setPreferredStreamVariants(preferredStreamVariants: List<String>) : Builder {
this.preferredStreamVariants = preferredStreamVariants
return this
}
fun setSharingLinkWithOffsetPossible(possible: Boolean) : Builder {
this.sharingLinkWithOffsetPossible = false
return this
}
fun build(): NewPlayer {
@ -102,7 +111,8 @@ interface NewPlayer {
app = app,
internalPlayer = exoPlayerBuilder.build(),
repository = repository,
preferredStreamVariants = preferredStreamVariants
preferredStreamVariants = preferredStreamVariants,
sharingLinkWithOffsetPossible = sharingLinkWithOffsetPossible
)
}
}
@ -114,6 +124,7 @@ class NewPlayerImpl(
override val internalPlayer: Player,
override val preferredStreamVariants: List<String>,
override val repository: MediaRepository,
override val sharingLinkWithOffsetPossible: Boolean
) : NewPlayer {
var mutableErrorFlow = MutableSharedFlow<Exception>()
@ -121,6 +132,7 @@ class NewPlayerImpl(
override val bufferedPercentage: Int
get() = internalPlayer.bufferedPercentage
override var currentPosition: Long
get() = internalPlayer.currentPosition
set(value) {

View File

@ -22,9 +22,10 @@ package net.newpipe.newplayer.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import net.newpipe.newplayer.Chapter
import net.newpipe.newplayer.playerInternals.PlaylistItem
import net.newpipe.newplayer.ui.ContentScale
@Parcelize
data class VideoPlayerUIState(
val uiMode: UIModeState,
val playing: Boolean,
@ -39,8 +40,10 @@ data class VideoPlayerUIState(
val fastSeekSeconds: Int,
val soundVolume: Float,
val brightness: Float?, // when null use system value
val embeddedUiConfig: EmbeddedUiConfig?
) : Parcelable {
val embeddedUiConfig: EmbeddedUiConfig?,
val playList: List<PlaylistItem>,
val chapters: List<Chapter>
) {
companion object {
val DEFAULT = VideoPlayerUIState(
// TODO: replace this with the placeholder state.
@ -59,7 +62,9 @@ data class VideoPlayerUIState(
fastSeekSeconds = 0,
soundVolume = 0f,
brightness = null,
embeddedUiConfig = null
embeddedUiConfig = null,
playList = emptyList(),
chapters = emptyList()
)
}
}

View File

@ -26,6 +26,8 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import net.newpipe.newplayer.NewPlayer
import net.newpipe.newplayer.ui.ContentScale
import net.newpipe.newplayer.utils.Thumbnail
interface VideoPlayerViewModel {
var newPlayer: NewPlayer?

View File

@ -30,6 +30,7 @@ import androidx.core.content.ContextCompat.getSystemService
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
@ -43,6 +44,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.playerInternals.getPlaylistItemsFromItemList
import net.newpipe.newplayer.ui.ContentScale
val VIDEOPLAYER_UI_STATE = "video_player_ui_state"
@ -119,7 +121,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
}
}
var mutableEmbeddedPlayerDraggedDownBy = MutableSharedFlow<Float>()
private var mutableEmbeddedPlayerDraggedDownBy = MutableSharedFlow<Float>()
override val embeddedPlayerDraggedDownBy = mutableEmbeddedPlayerDraggedDownBy.asSharedFlow()
private fun installNewPlayer() {
@ -148,6 +150,11 @@ class VideoPlayerViewModelImpl @Inject constructor(
it.copy(isLoading = isLoading)
}
}
override fun onPlaylistMetadataChanged(mediaMetadata: MediaMetadata) {
super.onPlaylistMetadataChanged(mediaMetadata)
updatePlaylist()
}
})
}
newPlayer?.let { newPlayer ->
@ -162,7 +169,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
}
}
}
updatePlaylist()
}
fun updateContentRatio(videoSize: VideoSize) {
@ -393,4 +400,18 @@ class VideoPlayerViewModelImpl @Inject constructor(
} ?: minContentRatio
private fun updatePlaylist() {
newPlayer?.let { newPlayer ->
viewModelScope.launch {
val playlist = getPlaylistItemsFromItemList(
newPlayer.playlist,
newPlayer.repository
)
mutableUiState.update {
it.copy(playList = playlist)
}
}
}
}
}

View File

@ -0,0 +1,32 @@
/* NewPlayer
*
* @author Christian Schabesberger
*
* Copyright (C) NewPipe e.V. 2024 <code(at)newpipe-ev.de>
*
* NewPlayer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPlayer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPlayer. If not, see <http://www.gnu.org/licenses/>.
*
*/
package net.newpipe.newplayer.playerInternals
import net.newpipe.newplayer.utils.Thumbnail
data class ChapterItem(
val title: String,
val offsetInMs: Long,
val thumbnail: Thumbnail
)

View File

@ -19,11 +19,9 @@
*
*/
package net.newpipe.newplayer.utils
package net.newpipe.newplayer.playerInternals
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player
import androidx.media3.common.Player.Listener
// TODO: This is cool, but it might still contains all raceconditions since two actors are mutating the
@ -33,9 +31,6 @@ import androidx.media3.common.Player.Listener
// a get element query the count of elements might have been changed by exoplayer itself
// due to this reason some functions force the user to handle elements out of bounds exceptions.
class PlayListIterator(
val exoPlayer: Player,
val fromIndex: Int,

View File

@ -0,0 +1,56 @@
/* NewPlayer
*
* @author Christian Schabesberger
*
* Copyright (C) NewPipe e.V. 2024 <code(at)newpipe-ev.de>
*
* NewPlayer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPlayer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPlayer. If not, see <http://www.gnu.org/licenses/>.
*
*/
package net.newpipe.newplayer.playerInternals
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import net.newpipe.newplayer.MediaRepository
import net.newpipe.newplayer.utils.Thumbnail
import kotlin.coroutines.coroutineContext
data class PlaylistItem(
val title: String,
val creator: String,
val id: String,
val thumbnail: Thumbnail?,
val lengthInS: Int
)
suspend fun getPlaylistItemsFromItemList(items: List<String>, mediaRepo: MediaRepository) =
with(CoroutineScope(coroutineContext)) {
items.map { item ->
Pair(item, async {
mediaRepo.getMetaInfo(item)
})
}.map {
val metaInfo = it.second.await()
PlaylistItem(
title = metaInfo.title,
creator = metaInfo.channelName,
id = it.first,
thumbnail = metaInfo.thumbnail,
lengthInS = metaInfo.lengthInS
)
}
}

View File

@ -1,16 +1,11 @@
package net.newpipe.newplayer.testapp
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.media.Image
import android.net.Uri
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.newpipe.newplayer.Chapter
import net.newpipe.newplayer.MediaRepository
import net.newpipe.newplayer.MetaInfo
import net.newpipe.newplayer.utils.OnlineThumbnail
import net.newpipe.newplayer.utils.Thumbnail
import okhttp3.OkHttpClient
@ -27,37 +22,30 @@ class TestMediaRepository(val context: Context) : MediaRepository {
return client.newCall(request).execute()
}
override suspend fun getTitle(item: String) =
override suspend fun getMetaInfo(item: String) =
when (item) {
"6502" -> "Reverse engineering the MOS 6502"
"portrait" -> "Imitating generative AI videos"
"imu" -> "Intel Management Engine deep dive "
else -> throw Exception("Unknown stream: $item")
}
override suspend fun getChannelName(item: String) =
when (item) {
"6502" -> "27c3"
"portrait" -> "无所吊谓~"
"imu" -> "36c3"
else -> throw Exception("Unknown stream: $item")
}
override suspend fun getThumbnail(item: String) =
when (item) {
"6502" ->
OnlineThumbnail("https://static.media.ccc.de/media/congress/2010/27c3-4159-en-reverse_engineering_mos_6502_preview.jpg")
"portrait" ->
OnlineThumbnail("https://64.media.tumblr.com/13f7e4065b4c583573a9a3e40750ccf8/9e8cf97a92704864-4b/s540x810/d966c97f755384b46dbe6d5350d35d0e9d4128ad.jpg")
"imu" ->
OnlineThumbnail("https://static.media.ccc.de/media/congress/2019/10694-hd_preview.jpg")
"6502" -> MetaInfo(
title = context.getString(R.string.ccc_6502_title),
channelName = context.getString(R.string.ccc_6502_channel),
thumbnail = OnlineThumbnail(context.getString(R.string.ccc_6502_thumbnail)),
lengthInS = context.resources.getInteger(R.integer.ccc_6502_length)
)
"imu" -> MetaInfo(
title = context.getString(R.string.ccc_imu_title),
channelName = context.getString(R.string.ccc_imu_channel),
thumbnail = OnlineThumbnail(context.getString(R.string.ccc_imu_thumbnail)),
lengthInS = context.resources.getInteger(R.integer.ccc_imu_length)
)
"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)
)
else -> throw Exception("Unknown stream: $item")
}
override suspend fun getAvailableStreamVariants(item: String): List<String> =
when (item) {
"6502" -> listOf("576p")

View File

@ -35,10 +35,13 @@
<item>2400000</item>
<item>3600000</item>
</integer-array>
<integer name="ccc_6502_length">3116</integer>
<!-- A thumbler Video. The creators tried to imitate an ai generated video. I found this in a Tom Scott newsletter. -->
<string name="portrait_title" translatable="false">Imitating generative AI videos</string>
<string name="portrait_channel" translatable="false">rongzhi</string>
<string name="portrait_video_example" translatable="false">https://va.media.tumblr.com/tumblr_sh62vjBX0j1z8ckep.mp4</string>
<integer name="portrait_length">23</integer>
<!-- "Intel Management Engine deep dive" a talk from 36c3 -->
<string name ="ccc_imu_link" translatable="false">https://media.ccc.de/v/36c3-10694-intel_management_engine_deep_dive</string>
@ -60,4 +63,5 @@
<item>3600000</item>
</integer-array>
<string name="ccc_imu_subtitles" translatable="false">https://cdn.media.ccc.de/congress/2019/36c3-10694-eng-deu-Intel_Management_Engine_deep_dive.en.srt</string>
<integer name="ccc_imu_length">3607</integer>
</resources>