push player further to playlist handling
This commit is contained in:
parent
3d0fdabcf4
commit
a2e8f6c4ad
|
@ -1,20 +1,47 @@
|
||||||
|
/* 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
|
package net.newpipe.newplayer
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.media3.common.MediaItem
|
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?)
|
||||||
|
|
||||||
interface MediaRepository {
|
interface MediaRepository {
|
||||||
suspend fun getTitle(item: String) : String
|
suspend fun getTitle(item: String) : String
|
||||||
suspend fun getChannelName(item: String): String
|
suspend fun getChannelName(item: String): String
|
||||||
suspend fun getThumbnail(item: String): Bitmap
|
suspend fun getThumbnail(item: String): Thumbnail
|
||||||
|
suspend fun getAvailableStreamVariants(item: String): List<String>
|
||||||
|
suspend fun getAvailableSubtitleVariants(item: String): List<String>
|
||||||
|
|
||||||
suspend fun getAvailableStreams(item: String): List<String>
|
suspend fun getStream(item: String, streamSelector: String) : Uri
|
||||||
|
suspend fun getSubtitle(item: String, )
|
||||||
|
|
||||||
suspend fun getStream(item: String, streamSelector: String) : MediaItem
|
suspend fun getPreviewThumbnails(item: String) : HashMap<Long, Thumbnail>?
|
||||||
suspend fun getLinkWithStreamOffset(item: String) : String
|
suspend fun getChapters(item: String): List<Chapter>
|
||||||
|
suspend fun getChapterThumbnail(item: String, chapter: Long) : Thumbnail
|
||||||
|
|
||||||
suspend fun getPreviewThumbnails(item: String) : List<Bitmap>
|
suspend fun getTimestampLink(item: String, timestampInSeconds: Long)
|
||||||
suspend fun getChapters(item: String): List<Long>
|
|
||||||
suspend fun getChapterThumbnail(item: String, chapter: Long) : Bitmap
|
suspend fun tryAndRescueError(item: String?, exception: PlaybackException) : Uri?
|
||||||
}
|
}
|
|
@ -23,114 +23,218 @@ package net.newpipe.newplayer
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.util.Log
|
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.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
import androidx.media3.exoplayer.source.MediaSource
|
import androidx.media3.exoplayer.source.MediaSource
|
||||||
import java.lang.Exception
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.Exception
|
||||||
|
|
||||||
enum class PlayMode {
|
enum class PlayMode {
|
||||||
EMBEDDED_VIDEO,
|
EMBEDDED_VIDEO,
|
||||||
FULLSCREEN_VIDEO,
|
FULLSCREEN_VIDEO,
|
||||||
PIP,
|
PIP,
|
||||||
BACKGROND,
|
BACKGROUND,
|
||||||
AUDIO_FORGROUND,
|
AUDIO_FOREGROUND,
|
||||||
}
|
}
|
||||||
|
|
||||||
private val TAG = "NewPlayer"
|
private val TAG = "NewPlayer"
|
||||||
|
|
||||||
interface NewPlayer {
|
interface NewPlayer {
|
||||||
val internal_player: Player
|
// preferences
|
||||||
|
val preferredStreamVariants: List<String>
|
||||||
|
|
||||||
|
val internalPlayer: Player
|
||||||
var playWhenReady: Boolean
|
var playWhenReady: Boolean
|
||||||
val duartion: Long
|
val duration: Long
|
||||||
val bufferedPercentage: Int
|
val bufferedPercentage: Int
|
||||||
val repository: MediaRepository
|
val repository: MediaRepository
|
||||||
var currentPosition: Long
|
var currentPosition: Long
|
||||||
var fastSeekAmountSec: Int
|
var fastSeekAmountSec: Int
|
||||||
var playBackMode: PlayMode
|
var playBackMode: PlayMode
|
||||||
var playList: MutableList<String>
|
var playMode: PlayMode?
|
||||||
|
|
||||||
|
// calbacks
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
fun playModeChange(playMode: PlayMode) {}
|
||||||
|
fun onError(exception: Exception) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// methods
|
||||||
fun prepare()
|
fun prepare()
|
||||||
fun play()
|
fun play()
|
||||||
fun pause()
|
fun pause()
|
||||||
fun addToPlaylist(newItem: String)
|
fun addToPlaylist(item: String)
|
||||||
fun addListener(callbackListener: Listener)
|
fun playStream(item: String, playMode: PlayMode)
|
||||||
|
fun playStream(item: String, streamVariant: String, playMode: PlayMode)
|
||||||
//TODO: This is only temporary
|
fun addCallbackListener(listener: Listener?)
|
||||||
fun setStream(stream: MediaItem)
|
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
fun setMediaSourceFactory(mediaSourceFactory: MediaSource.Factory) {
|
fun setMediaSourceFactory(mediaSourceFactory: MediaSource.Factory) {
|
||||||
this.mediaSourceFactory = mediaSourceFactory
|
this.mediaSourceFactory = mediaSourceFactory
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setPreferredStreamVariants(preferredStreamVariants: List<String>) {
|
||||||
|
this.preferredStreamVariants = preferredStreamVariants
|
||||||
|
}
|
||||||
|
|
||||||
fun build(): NewPlayer {
|
fun build(): NewPlayer {
|
||||||
val exoPlayerBuilder = ExoPlayer.Builder(app)
|
val exoPlayerBuilder = ExoPlayer.Builder(app)
|
||||||
mediaSourceFactory?.let {
|
mediaSourceFactory?.let {
|
||||||
exoPlayerBuilder.setMediaSourceFactory(it)
|
exoPlayerBuilder.setMediaSourceFactory(it)
|
||||||
}
|
}
|
||||||
return NewPlayerImpl(exoPlayerBuilder.build(), repository = repository)
|
return NewPlayerImpl(
|
||||||
|
app = app,
|
||||||
|
internalPlayer = exoPlayerBuilder.build(),
|
||||||
|
repository = repository,
|
||||||
|
preferredStreamVariants = preferredStreamVariants
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Listener {
|
|
||||||
fun onError(exception: Exception)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class NewPlayerImpl(override val internal_player: Player, override val repository: MediaRepository) : NewPlayer {
|
class NewPlayerImpl(
|
||||||
|
val app: Application,
|
||||||
private var callbackListeners: MutableList<NewPlayer.Listener> = ArrayList()
|
override val internalPlayer: Player,
|
||||||
|
override val preferredStreamVariants: List<String>,
|
||||||
override val duartion: Long
|
override val repository: MediaRepository,
|
||||||
get() = internal_player.duration
|
) : NewPlayer {
|
||||||
|
|
||||||
override val bufferedPercentage: Int
|
override val bufferedPercentage: Int
|
||||||
get() = internal_player.bufferedPercentage
|
get() = internalPlayer.bufferedPercentage
|
||||||
override var currentPosition: Long
|
override var currentPosition: Long
|
||||||
get() = internal_player.currentPosition
|
get() = internalPlayer.currentPosition
|
||||||
set(value) {internal_player.seekTo(value)}
|
set(value) {
|
||||||
|
internalPlayer.seekTo(value)
|
||||||
|
}
|
||||||
|
|
||||||
override var fastSeekAmountSec: Int = 10
|
override var fastSeekAmountSec: Int = 10
|
||||||
override var playBackMode: PlayMode = PlayMode.EMBEDDED_VIDEO
|
override var playBackMode: PlayMode = PlayMode.EMBEDDED_VIDEO
|
||||||
override var playList: MutableList<String> = ArrayList<String>()
|
|
||||||
|
private var callbackListener: ArrayList<NewPlayer.Listener?> = ArrayList()
|
||||||
|
private var playerScope = CoroutineScope(Dispatchers.Default + Job())
|
||||||
|
|
||||||
|
override var playMode: PlayMode? = null
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
if (field != null) {
|
||||||
|
callbackListener.forEach { it?.playModeChange(field!!) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override var playWhenReady: Boolean
|
override var playWhenReady: Boolean
|
||||||
set(value) {
|
set(value) {
|
||||||
internal_player.playWhenReady = value
|
internalPlayer.playWhenReady = value
|
||||||
}
|
}
|
||||||
get() = internal_player.playWhenReady
|
get() = internalPlayer.playWhenReady
|
||||||
|
|
||||||
|
|
||||||
|
override val duration: Long
|
||||||
|
get() = internalPlayer.duration
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
internalPlayer.addListener(object: Player.Listener {
|
||||||
|
override fun onPlayerError(error: PlaybackException) {
|
||||||
|
launchJobAndCollectError {
|
||||||
|
val item = internalPlayer.currentMediaItem?.mediaId
|
||||||
|
val newUri = repository.tryAndRescueError(item, exception = error)
|
||||||
|
if (newUri != null) {
|
||||||
|
TODO("Implement handing new uri on fixed error")
|
||||||
|
} else {
|
||||||
|
callbackListener.forEach {
|
||||||
|
it?.onError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
override fun prepare() {
|
override fun prepare() {
|
||||||
internal_player.prepare()
|
internalPlayer.prepare()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun play() {
|
override fun play() {
|
||||||
if(internal_player.currentMediaItem != null) {
|
if (internalPlayer.currentMediaItem != null) {
|
||||||
internal_player.play()
|
internalPlayer.play()
|
||||||
} else {
|
} else {
|
||||||
Log.i(TAG, "Tried to start playing but no media Item was cued")
|
Log.i(TAG, "Tried to start playing but no media Item was cued")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pause() {
|
override fun pause() {
|
||||||
internal_player.pause()
|
internalPlayer.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addToPlaylist(newItem: String) {
|
override fun addToPlaylist(item: String) {
|
||||||
Log.d(TAG, "Not implemented add to playlist")
|
launchJobAndCollectError {
|
||||||
|
val mediaItem = toMediaItem(item)
|
||||||
|
internalPlayer.addMediaItem(mediaItem)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addListener(callbackListener: NewPlayer.Listener) {
|
override fun playStream(item: String, playMode: PlayMode) {
|
||||||
callbackListeners.add(callbackListener)
|
launchJobAndCollectError {
|
||||||
|
val mediaItem = toMediaItem(item)
|
||||||
|
internalPlayStream(mediaItem, playMode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setStream(stream: MediaItem) {
|
override fun playStream(item: String, streamVariant: String, playMode: PlayMode) {
|
||||||
if (internal_player.playbackState == Player.STATE_IDLE) {
|
launchJobAndCollectError {
|
||||||
internal_player.prepare()
|
val stream = toMediaItem(item)
|
||||||
|
internalPlayStream(stream, playMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun internalPlayStream(mediaItem: MediaItem, playMode: PlayMode) {
|
||||||
|
if (internalPlayer.playbackState == Player.STATE_IDLE) {
|
||||||
|
internalPlayer.prepare()
|
||||||
|
}
|
||||||
|
this.playMode = playMode
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun toMediaItem(item: String, streamVariant: String): MediaItem {
|
||||||
|
val dataStream = repository.getStream(item, streamVariant)
|
||||||
|
val mediaItem = MediaItem.Builder().setMediaId(item).setUri(dataStream)
|
||||||
|
return mediaItem.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun toMediaItem(item: String): MediaItem {
|
||||||
|
|
||||||
|
val availableStream = repository.getAvailableStreamVariants(item)
|
||||||
|
var selectedStream = availableStream[availableStream.size / 2]
|
||||||
|
for (preferredStream in preferredStreamVariants) {
|
||||||
|
if (preferredStream in availableStream) {
|
||||||
|
selectedStream = preferredStream
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal_player.setMediaItem(stream)
|
return toMediaItem(item, selectedStream)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchJobAndCollectError(task: suspend () -> Unit) =
|
||||||
|
playerScope.launch {
|
||||||
|
try {
|
||||||
|
task()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
callbackListener.forEach {
|
||||||
|
it?.onError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addCallbackListener(listener: NewPlayer.Listener?) {
|
||||||
|
callbackListener.add(listener)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
package net.newpipe.newplayer.model
|
package net.newpipe.newplayer.model
|
||||||
|
|
||||||
|
import net.newpipe.newplayer.PlayMode
|
||||||
|
|
||||||
enum class UIModeState {
|
enum class UIModeState {
|
||||||
PLACEHOLDER,
|
PLACEHOLDER,
|
||||||
|
|
||||||
|
@ -93,4 +95,16 @@ enum class UIModeState {
|
||||||
|
|
||||||
else -> this
|
else -> this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromPlayMode(playMode: PlayMode) =
|
||||||
|
when (playMode) {
|
||||||
|
PlayMode.EMBEDDED_VIDEO -> EMBEDDED_VIDEO
|
||||||
|
PlayMode.FULLSCREEN_VIDEO -> FULLSCREEN_VIDEO
|
||||||
|
PlayMode.PIP -> TODO()
|
||||||
|
PlayMode.BACKGROUND -> TODO()
|
||||||
|
PlayMode.AUDIO_FOREGROUND -> TODO()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -83,7 +83,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
|
||||||
override val uiState = mutableUiState.asStateFlow()
|
override val uiState = mutableUiState.asStateFlow()
|
||||||
|
|
||||||
override val internalPlayer: Player?
|
override val internalPlayer: Player?
|
||||||
get() = newPlayer?.internal_player
|
get() = newPlayer?.internalPlayer
|
||||||
|
|
||||||
override var minContentRatio: Float = 4F / 3F
|
override var minContentRatio: Float = 4F / 3F
|
||||||
set(value) {
|
set(value) {
|
||||||
|
|
|
@ -64,6 +64,10 @@ import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
|
||||||
import net.newpipe.newplayer.utils.getLocale
|
import net.newpipe.newplayer.utils.getLocale
|
||||||
import net.newpipe.newplayer.utils.getTimeStringFromMs
|
import net.newpipe.newplayer.utils.getTimeStringFromMs
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
|
import net.newpipe.newplayer.utils.BitmapThumbnail
|
||||||
|
import net.newpipe.newplayer.utils.OnlineThumbnail
|
||||||
|
import net.newpipe.newplayer.utils.Thumbnail
|
||||||
|
import net.newpipe.newplayer.utils.VectorThumbnail
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StreamSelectUI(
|
fun StreamSelectUI(
|
||||||
|
@ -125,7 +129,7 @@ private fun StreamSelectTopBar() {
|
||||||
private fun ChapterItem(
|
private fun ChapterItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
id: Int,
|
id: Int,
|
||||||
thumbnailUrl: String?,
|
thumbnail: Thumbnail?,
|
||||||
chapterTitle: String,
|
chapterTitle: String,
|
||||||
chapterStartInMs: Long,
|
chapterStartInMs: Long,
|
||||||
onClicked: (Int) -> Unit
|
onClicked: (Int) -> Unit
|
||||||
|
@ -141,10 +145,27 @@ private fun ChapterItem(
|
||||||
)
|
)
|
||||||
.clickable { onClicked(id) }
|
.clickable { onClicked(id) }
|
||||||
) {
|
) {
|
||||||
if (thumbnailUrl != null) {
|
val contentDescription = stringResource(R.string.chapter)
|
||||||
|
if (thumbnail != null) {
|
||||||
|
when (thumbnail) {
|
||||||
|
is OnlineThumbnail -> AsyncImage(
|
||||||
|
model = thumbnail.url,
|
||||||
|
contentDescription =contentDescription
|
||||||
|
)
|
||||||
|
|
||||||
|
is BitmapThumbnail -> Image(
|
||||||
|
bitmap = thumbnail.img,
|
||||||
|
contentDescription = contentDescription
|
||||||
|
)
|
||||||
|
|
||||||
|
is VectorThumbnail -> Image(
|
||||||
|
imageVector = thumbnail.vec,
|
||||||
|
contentDescription = contentDescription
|
||||||
|
)
|
||||||
|
}
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = thumbnailUrl,
|
model = thumbnail,
|
||||||
contentDescription = stringResource(R.string.chapter)
|
contentDescription = contentDescription
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Image(
|
Image(
|
||||||
|
@ -171,7 +192,7 @@ private fun StreamItem(
|
||||||
id: Int,
|
id: Int,
|
||||||
title: String,
|
title: String,
|
||||||
creator: String?,
|
creator: String?,
|
||||||
thumbnailUrl: String?,
|
thumbnail: Thumbnail?,
|
||||||
lengthInMs: Long,
|
lengthInMs: Long,
|
||||||
onDragStart: (Int) -> Unit,
|
onDragStart: (Int) -> Unit,
|
||||||
onDragEnd: (Int) -> Unit,
|
onDragEnd: (Int) -> Unit,
|
||||||
|
@ -180,10 +201,27 @@ private fun StreamItem(
|
||||||
val locale = getLocale()!!
|
val locale = getLocale()!!
|
||||||
Row(modifier = modifier.clickable { onClicked(id) }) {
|
Row(modifier = modifier.clickable { onClicked(id) }) {
|
||||||
Box {
|
Box {
|
||||||
if (thumbnailUrl != null) {
|
val contentDescription = stringResource(R.string.chapter)
|
||||||
|
if (thumbnail != null) {
|
||||||
|
when (thumbnail) {
|
||||||
|
is OnlineThumbnail -> AsyncImage(
|
||||||
|
model = thumbnail.url,
|
||||||
|
contentDescription =contentDescription
|
||||||
|
)
|
||||||
|
|
||||||
|
is BitmapThumbnail -> Image(
|
||||||
|
bitmap = thumbnail.img,
|
||||||
|
contentDescription = contentDescription
|
||||||
|
)
|
||||||
|
|
||||||
|
is VectorThumbnail -> Image(
|
||||||
|
imageVector = thumbnail.vec,
|
||||||
|
contentDescription = contentDescription
|
||||||
|
)
|
||||||
|
}
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = thumbnailUrl,
|
model = thumbnail,
|
||||||
contentDescription = stringResource(R.string.chapter)
|
contentDescription = contentDescription
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Image(
|
Image(
|
||||||
|
@ -209,10 +247,12 @@ private fun StreamItem(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(modifier = Modifier
|
Column(
|
||||||
.padding(8.dp)
|
modifier = Modifier
|
||||||
.weight(1f)
|
.padding(8.dp)
|
||||||
.fillMaxSize()) {
|
.weight(1f)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
Text(text = title, fontSize = 18.sp, fontWeight = FontWeight.Bold)
|
Text(text = title, fontSize = 18.sp, fontWeight = FontWeight.Bold)
|
||||||
if (creator != null) {
|
if (creator != null) {
|
||||||
Text(text = creator)
|
Text(text = creator)
|
||||||
|
@ -223,15 +263,17 @@ private fun StreamItem(
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.aspectRatio(1f)
|
.aspectRatio(1f)
|
||||||
.pointerInteropFilter {
|
.pointerInteropFilter {
|
||||||
when(it.action) {
|
when (it.action) {
|
||||||
MotionEvent.ACTION_UP -> {
|
MotionEvent.ACTION_UP -> {
|
||||||
onDragEnd(id)
|
onDragEnd(id)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
MotionEvent.ACTION_DOWN -> {
|
MotionEvent.ACTION_DOWN -> {
|
||||||
onDragStart(id)
|
onDragStart(id)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
|
@ -254,7 +296,7 @@ fun ChapterItemPreview() {
|
||||||
Surface(modifier = Modifier.fillMaxSize(), color = Color.DarkGray) {
|
Surface(modifier = Modifier.fillMaxSize(), color = Color.DarkGray) {
|
||||||
ChapterItem(
|
ChapterItem(
|
||||||
id = 0,
|
id = 0,
|
||||||
thumbnailUrl = null,
|
thumbnail = null,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
chapterTitle = "Chapter Title",
|
chapterTitle = "Chapter Title",
|
||||||
chapterStartInMs = (4 * 60 + 32) * 1000,
|
chapterStartInMs = (4 * 60 + 32) * 1000,
|
||||||
|
@ -274,7 +316,7 @@ fun StreamItemPreview() {
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
title = "Video Title",
|
title = "Video Title",
|
||||||
creator = "Video Creator",
|
creator = "Video Creator",
|
||||||
thumbnailUrl = null,
|
thumbnail = null,
|
||||||
lengthInMs = 15 * 60 * 1000,
|
lengthInMs = 15 * 60 * 1000,
|
||||||
onDragStart = {},
|
onDragStart = {},
|
||||||
onDragEnd = {},
|
onDragEnd = {},
|
||||||
|
|
|
@ -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.utils
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.drawable.VectorDrawable
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
|
||||||
|
interface Thumbnail
|
||||||
|
|
||||||
|
data class OnlineThumbnail(val url: String) : Thumbnail
|
||||||
|
data class BitmapThumbnail(val img: ImageBitmap) : Thumbnail
|
||||||
|
data class VectorThumbnail(val vec:ImageVector) : Thumbnail
|
|
@ -8,7 +8,10 @@ import android.net.Uri
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import net.newpipe.newplayer.Chapter
|
||||||
import net.newpipe.newplayer.MediaRepository
|
import net.newpipe.newplayer.MediaRepository
|
||||||
|
import net.newpipe.newplayer.utils.OnlineThumbnail
|
||||||
|
import net.newpipe.newplayer.utils.Thumbnail
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
@ -41,33 +44,20 @@ class TestMediaRepository(val context: Context) : MediaRepository {
|
||||||
|
|
||||||
override suspend fun getThumbnail(item: String) =
|
override suspend fun getThumbnail(item: String) =
|
||||||
when (item) {
|
when (item) {
|
||||||
"6502" -> withContext(Dispatchers.IO) {
|
"6502" ->
|
||||||
val response =
|
OnlineThumbnail("https://static.media.ccc.de/media/congress/2010/27c3-4159-en-reverse_engineering_mos_6502_preview.jpg")
|
||||||
get("https://static.media.ccc.de/media/congress/2010/27c3-4159-en-reverse_engineering_mos_6502_preview.jpg")
|
|
||||||
|
|
||||||
BitmapFactory.decodeStream(response.body.byteStream())
|
"portrait" ->
|
||||||
}
|
OnlineThumbnail("https://64.media.tumblr.com/13f7e4065b4c583573a9a3e40750ccf8/9e8cf97a92704864-4b/s540x810/d966c97f755384b46dbe6d5350d35d0e9d4128ad.jpg")
|
||||||
|
|
||||||
|
"imu" ->
|
||||||
"portrait" -> withContext(Dispatchers.IO) {
|
OnlineThumbnail("https://static.media.ccc.de/media/congress/2019/10694-hd_preview.jpg")
|
||||||
val response =
|
|
||||||
get("https://64.media.tumblr.com/13f7e4065b4c583573a9a3e40750ccf8/9e8cf97a92704864-4b/s540x810/d966c97f755384b46dbe6d5350d35d0e9d4128ad.jpg")
|
|
||||||
|
|
||||||
BitmapFactory.decodeStream(response.body.byteStream())
|
|
||||||
}
|
|
||||||
|
|
||||||
"imu" -> withContext(Dispatchers.IO) {
|
|
||||||
val response =
|
|
||||||
get("https://static.media.ccc.de/media/congress/2019/10694-hd_preview.jpg")
|
|
||||||
|
|
||||||
BitmapFactory.decodeStream(response.body.byteStream())
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> throw Exception("Unknown stream: $item")
|
else -> throw Exception("Unknown stream: $item")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override suspend fun getAvailableStreams(item: String): List<String> =
|
override suspend fun getAvailableStreamVariants(item: String): List<String> =
|
||||||
when (item) {
|
when (item) {
|
||||||
"6502" -> listOf("576p")
|
"6502" -> listOf("576p")
|
||||||
"portrait" -> listOf("720p")
|
"portrait" -> listOf("720p")
|
||||||
|
@ -95,15 +85,38 @@ class TestMediaRepository(val context: Context) : MediaRepository {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getPreviewThumbnails(item: String): List<Bitmap> {
|
override suspend fun getPreviewThumbnails(item: String): HashMap<Long, Thumbnail>? {
|
||||||
|
val templateUrl = when (item) {
|
||||||
|
"6502" -> context.getString(R.string.ccc_6502_preview_thumbnails)
|
||||||
|
"imu" -> context.getString(R.string.ccc_imu_preview_thumbnails)
|
||||||
|
"portrait" -> null
|
||||||
|
else -> throw Exception("Unknown stream: $item")
|
||||||
|
}
|
||||||
|
|
||||||
|
if(templateUrl != null) {
|
||||||
|
val thumbCount = when(item) {
|
||||||
|
"6502" -> 312
|
||||||
|
"imu" -> 361
|
||||||
|
else -> throw Exception("Unknown stream: $item") }
|
||||||
|
|
||||||
|
var thumbMap = HashMap<Long, Thumbnail>()
|
||||||
|
|
||||||
|
for (i in 1..thumbCount) {
|
||||||
|
val timeStamp= (i-1) * 10 * 1000
|
||||||
|
thumbMap.put(timeStamp.toLong(), OnlineThumbnail(String.format(templateUrl, i)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return thumbMap
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getChapters(item: String): List<Chapter> {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getChapters(item: String): List<Long> {
|
override suspend fun getChapterThumbnail(item: String, chapter: Long): Thumbnail {
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getChapterThumbnail(item: String, chapter: Long): Bitmap {
|
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue