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 package net.newpipe.newplayer
import android.net.Uri import android.net.Uri
import androidx.media3.common.MediaItem
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?) 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 { interface MediaRepository {
suspend fun getTitle(item: String) : String
suspend fun getChannelName(item: String): String suspend fun getMetaInfo(item: String): MetaInfo
suspend fun getThumbnail(item: String): Thumbnail
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
suspend fun getAvailableSubtitleVariants(item: String): List<String> suspend fun getAvailableSubtitleVariants(item: String): List<String>
suspend fun getSubtitle(item: String, variant: String): Uri 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 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 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.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.newpipe.newplayer.utils.PlayList import net.newpipe.newplayer.playerInternals.PlayList
import kotlin.Exception import kotlin.Exception
enum class PlayMode { enum class PlayMode {
@ -60,6 +60,7 @@ interface NewPlayer {
val duration: Long val duration: Long
val bufferedPercentage: Int val bufferedPercentage: Int
val repository: MediaRepository val repository: MediaRepository
val sharingLinkWithOffsetPossible: Boolean
var currentPosition: Long var currentPosition: Long
var fastSeekAmountSec: Int var fastSeekAmountSec: Int
var playBackMode: PlayMode var playBackMode: PlayMode
@ -84,13 +85,21 @@ interface NewPlayer {
data class Builder(val app: Application, val repository: MediaRepository) { data class Builder(val app: Application, val repository: MediaRepository) {
private var mediaSourceFactory: MediaSource.Factory? = null private var mediaSourceFactory: MediaSource.Factory? = null
private var preferredStreamVariants: List<String> = emptyList() private var preferredStreamVariants: List<String> = emptyList()
private var sharingLinkWithOffsetPossible = false
fun setMediaSourceFactory(mediaSourceFactory: MediaSource.Factory) { fun setMediaSourceFactory(mediaSourceFactory: MediaSource.Factory) : Builder {
this.mediaSourceFactory = mediaSourceFactory this.mediaSourceFactory = mediaSourceFactory
return this
} }
fun setPreferredStreamVariants(preferredStreamVariants: List<String>) { fun setPreferredStreamVariants(preferredStreamVariants: List<String>) : Builder {
this.preferredStreamVariants = preferredStreamVariants this.preferredStreamVariants = preferredStreamVariants
return this
}
fun setSharingLinkWithOffsetPossible(possible: Boolean) : Builder {
this.sharingLinkWithOffsetPossible = false
return this
} }
fun build(): NewPlayer { fun build(): NewPlayer {
@ -102,7 +111,8 @@ interface NewPlayer {
app = app, app = app,
internalPlayer = exoPlayerBuilder.build(), internalPlayer = exoPlayerBuilder.build(),
repository = repository, repository = repository,
preferredStreamVariants = preferredStreamVariants preferredStreamVariants = preferredStreamVariants,
sharingLinkWithOffsetPossible = sharingLinkWithOffsetPossible
) )
} }
} }
@ -114,6 +124,7 @@ class NewPlayerImpl(
override val internalPlayer: Player, override val internalPlayer: Player,
override val preferredStreamVariants: List<String>, override val preferredStreamVariants: List<String>,
override val repository: MediaRepository, override val repository: MediaRepository,
override val sharingLinkWithOffsetPossible: Boolean
) : NewPlayer { ) : NewPlayer {
var mutableErrorFlow = MutableSharedFlow<Exception>() var mutableErrorFlow = MutableSharedFlow<Exception>()
@ -121,6 +132,7 @@ class NewPlayerImpl(
override val bufferedPercentage: Int override val bufferedPercentage: Int
get() = internalPlayer.bufferedPercentage get() = internalPlayer.bufferedPercentage
override var currentPosition: Long override var currentPosition: Long
get() = internalPlayer.currentPosition get() = internalPlayer.currentPosition
set(value) { set(value) {

View File

@ -22,9 +22,10 @@ package net.newpipe.newplayer.model
import android.os.Parcelable import android.os.Parcelable
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import net.newpipe.newplayer.Chapter
import net.newpipe.newplayer.playerInternals.PlaylistItem
import net.newpipe.newplayer.ui.ContentScale import net.newpipe.newplayer.ui.ContentScale
@Parcelize
data class VideoPlayerUIState( data class VideoPlayerUIState(
val uiMode: UIModeState, val uiMode: UIModeState,
val playing: Boolean, val playing: Boolean,
@ -39,8 +40,10 @@ data class VideoPlayerUIState(
val fastSeekSeconds: Int, val fastSeekSeconds: Int,
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?,
) : Parcelable { val playList: List<PlaylistItem>,
val chapters: List<Chapter>
) {
companion object { companion object {
val DEFAULT = VideoPlayerUIState( val DEFAULT = VideoPlayerUIState(
// TODO: replace this with the placeholder state. // TODO: replace this with the placeholder state.
@ -59,7 +62,9 @@ data class VideoPlayerUIState(
fastSeekSeconds = 0, fastSeekSeconds = 0,
soundVolume = 0f, soundVolume = 0f,
brightness = null, 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 kotlinx.coroutines.flow.StateFlow
import net.newpipe.newplayer.NewPlayer import net.newpipe.newplayer.NewPlayer
import net.newpipe.newplayer.ui.ContentScale import net.newpipe.newplayer.ui.ContentScale
import net.newpipe.newplayer.utils.Thumbnail
interface VideoPlayerViewModel { interface VideoPlayerViewModel {
var newPlayer: NewPlayer? var newPlayer: NewPlayer?

View File

@ -30,6 +30,7 @@ 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.MediaMetadata
import androidx.media3.common.Player import androidx.media3.common.Player
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -43,6 +44,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.playerInternals.getPlaylistItemsFromItemList
import net.newpipe.newplayer.ui.ContentScale import net.newpipe.newplayer.ui.ContentScale
val VIDEOPLAYER_UI_STATE = "video_player_ui_state" 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() override val embeddedPlayerDraggedDownBy = mutableEmbeddedPlayerDraggedDownBy.asSharedFlow()
private fun installNewPlayer() { private fun installNewPlayer() {
@ -148,6 +150,11 @@ class VideoPlayerViewModelImpl @Inject constructor(
it.copy(isLoading = isLoading) it.copy(isLoading = isLoading)
} }
} }
override fun onPlaylistMetadataChanged(mediaMetadata: MediaMetadata) {
super.onPlaylistMetadataChanged(mediaMetadata)
updatePlaylist()
}
}) })
} }
newPlayer?.let { newPlayer -> newPlayer?.let { newPlayer ->
@ -162,7 +169,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
} }
} }
} }
updatePlaylist()
} }
fun updateContentRatio(videoSize: VideoSize) { fun updateContentRatio(videoSize: VideoSize) {
@ -393,4 +400,18 @@ class VideoPlayerViewModelImpl @Inject constructor(
} ?: minContentRatio } ?: 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
import androidx.media3.common.Player.Listener
// TODO: This is cool, but it might still contains all raceconditions since two actors are mutating the // 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 // 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. // due to this reason some functions force the user to handle elements out of bounds exceptions.
class PlayListIterator( class PlayListIterator(
val exoPlayer: Player, val exoPlayer: Player,
val fromIndex: Int, 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 package net.newpipe.newplayer.testapp
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.media.Image
import android.net.Uri import android.net.Uri
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
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.OnlineThumbnail import net.newpipe.newplayer.utils.OnlineThumbnail
import net.newpipe.newplayer.utils.Thumbnail import net.newpipe.newplayer.utils.Thumbnail
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -27,37 +22,30 @@ class TestMediaRepository(val context: Context) : MediaRepository {
return client.newCall(request).execute() return client.newCall(request).execute()
} }
override suspend fun getTitle(item: String) = override suspend fun getMetaInfo(item: String) =
when (item) { when (item) {
"6502" -> "Reverse engineering the MOS 6502" "6502" -> MetaInfo(
"portrait" -> "Imitating generative AI videos" title = context.getString(R.string.ccc_6502_title),
"imu" -> "Intel Management Engine deep dive " channelName = context.getString(R.string.ccc_6502_channel),
else -> throw Exception("Unknown stream: $item") thumbnail = OnlineThumbnail(context.getString(R.string.ccc_6502_thumbnail)),
} lengthInS = context.resources.getInteger(R.integer.ccc_6502_length)
)
override suspend fun getChannelName(item: String) = "imu" -> MetaInfo(
when (item) { title = context.getString(R.string.ccc_imu_title),
"6502" -> "27c3" channelName = context.getString(R.string.ccc_imu_channel),
"portrait" -> "无所吊谓~" thumbnail = OnlineThumbnail(context.getString(R.string.ccc_imu_thumbnail)),
"imu" -> "36c3" lengthInS = context.resources.getInteger(R.integer.ccc_imu_length)
else -> throw Exception("Unknown stream: $item") )
} "portrait" -> MetaInfo(
title = context.getString(R.string.portrait_title),
override suspend fun getThumbnail(item: String) = channelName = context.getString(R.string.portrait_channel),
when (item) { thumbnail = null,
"6502" -> lengthInS = context.resources.getInteger(R.integer.portrait_length)
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")
else -> throw Exception("Unknown stream: $item") else -> throw Exception("Unknown stream: $item")
} }
override suspend fun getAvailableStreamVariants(item: String): List<String> = override suspend fun getAvailableStreamVariants(item: String): List<String> =
when (item) { when (item) {
"6502" -> listOf("576p") "6502" -> listOf("576p")

View File

@ -35,10 +35,13 @@
<item>2400000</item> <item>2400000</item>
<item>3600000</item> <item>3600000</item>
</integer-array> </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. --> <!-- 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> <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 --> <!-- "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> <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> <item>3600000</item>
</integer-array> </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> <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> </resources>