diff --git a/new-player/src/main/java/net/newpipe/newplayer/MediaRepository.kt b/new-player/src/main/java/net/newpipe/newplayer/MediaRepository.kt index eb9d32b..123b00c 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/MediaRepository.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/MediaRepository.kt @@ -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 - suspend fun getStream(item: String, streamSelector: String) : Uri + suspend fun getStream(item: String, streamSelector: String): Uri suspend fun getAvailableSubtitleVariants(item: String): List suspend fun getSubtitle(item: String, variant: String): Uri - suspend fun getPreviewThumbnails(item: String) : HashMap? + suspend fun getPreviewThumbnails(item: String): HashMap? suspend fun getChapters(item: String): List - 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? } \ No newline at end of file diff --git a/new-player/src/main/java/net/newpipe/newplayer/NewPlayer.kt b/new-player/src/main/java/net/newpipe/newplayer/NewPlayer.kt index 3507424..d4bf405 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/NewPlayer.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/NewPlayer.kt @@ -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 = 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) { + fun setPreferredStreamVariants(preferredStreamVariants: List) : 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, override val repository: MediaRepository, + override val sharingLinkWithOffsetPossible: Boolean ) : NewPlayer { var mutableErrorFlow = MutableSharedFlow() @@ -121,6 +132,7 @@ class NewPlayerImpl( override val bufferedPercentage: Int get() = internalPlayer.bufferedPercentage + override var currentPosition: Long get() = internalPlayer.currentPosition set(value) { diff --git a/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerUIState.kt b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerUIState.kt index 160806d..7dfc91c 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerUIState.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerUIState.kt @@ -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, + val chapters: List +) { 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() ) } } \ No newline at end of file diff --git a/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModel.kt b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModel.kt index a90f0cf..83facf8 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModel.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModel.kt @@ -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? diff --git a/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModelImpl.kt b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModelImpl.kt index 4534dfd..9e52922 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModelImpl.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModelImpl.kt @@ -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() + private var mutableEmbeddedPlayerDraggedDownBy = MutableSharedFlow() 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) + } + } + } + } } \ No newline at end of file diff --git a/new-player/src/main/java/net/newpipe/newplayer/playerInternals/ChapterItem.kt b/new-player/src/main/java/net/newpipe/newplayer/playerInternals/ChapterItem.kt new file mode 100644 index 0000000..3017dc0 --- /dev/null +++ b/new-player/src/main/java/net/newpipe/newplayer/playerInternals/ChapterItem.kt @@ -0,0 +1,32 @@ +/* NewPlayer + * + * @author Christian Schabesberger + * + * Copyright (C) NewPipe e.V. 2024 + * + * 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 . + * + */ + + + +package net.newpipe.newplayer.playerInternals + +import net.newpipe.newplayer.utils.Thumbnail + +data class ChapterItem( + val title: String, + val offsetInMs: Long, + val thumbnail: Thumbnail +) diff --git a/new-player/src/main/java/net/newpipe/newplayer/utils/PlayList.kt b/new-player/src/main/java/net/newpipe/newplayer/playerInternals/PlayList.kt similarity index 97% rename from new-player/src/main/java/net/newpipe/newplayer/utils/PlayList.kt rename to new-player/src/main/java/net/newpipe/newplayer/playerInternals/PlayList.kt index e3199ec..3f2822c 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/utils/PlayList.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/playerInternals/PlayList.kt @@ -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, diff --git a/new-player/src/main/java/net/newpipe/newplayer/playerInternals/PlayListItem.kt b/new-player/src/main/java/net/newpipe/newplayer/playerInternals/PlayListItem.kt new file mode 100644 index 0000000..5c97d67 --- /dev/null +++ b/new-player/src/main/java/net/newpipe/newplayer/playerInternals/PlayListItem.kt @@ -0,0 +1,56 @@ +/* NewPlayer + * + * @author Christian Schabesberger + * + * Copyright (C) NewPipe e.V. 2024 + * + * 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 . + * + */ + +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, 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 + ) + } + } + + diff --git a/test-app/src/main/java/net/newpipe/newplayer/testapp/TestMediaRepository.kt b/test-app/src/main/java/net/newpipe/newplayer/testapp/TestMediaRepository.kt index da2250e..1175b87 100644 --- a/test-app/src/main/java/net/newpipe/newplayer/testapp/TestMediaRepository.kt +++ b/test-app/src/main/java/net/newpipe/newplayer/testapp/TestMediaRepository.kt @@ -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 = when (item) { "6502" -> listOf("576p") diff --git a/test-app/src/main/res/values/test_streams.xml b/test-app/src/main/res/values/test_streams.xml index 75a8ca9..d3548e8 100644 --- a/test-app/src/main/res/values/test_streams.xml +++ b/test-app/src/main/res/values/test_streams.xml @@ -35,10 +35,13 @@ 2400000 3600000 + 3116 + Imitating generative AI videos + rongzhi https://va.media.tumblr.com/tumblr_sh62vjBX0j1z8ckep.mp4 - + 23 https://media.ccc.de/v/36c3-10694-intel_management_engine_deep_dive @@ -60,4 +63,5 @@ 3600000 https://cdn.media.ccc.de/congress/2019/36c3-10694-eng-deu-Intel_Management_Engine_deep_dive.en.srt + 3607