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 e6bfd0a..45da3e3 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/MediaRepository.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/MediaRepository.kt @@ -45,6 +45,11 @@ data class RepoMetaInfo( val pullsDataFromNetwrok: Boolean ) +data class Stream( + val streamUri: Uri, + val mimeType: String? = null +) + interface MediaRepository { fun getRepoInfo() : RepoMetaInfo @@ -52,7 +57,7 @@ interface MediaRepository { suspend fun getMetaInfo(item: String): MediaMetadata suspend fun getAvailableStreamVariants(item: String): List - suspend fun getStream(item: String, streamVariantSelector: StreamVariant): Uri + suspend fun getStream(item: String, streamVariantSelector: StreamVariant): Stream suspend fun getAvailableSubtitleVariants(item: String): List suspend fun getSubtitle(item: String, variant: String): Uri 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 ea7e76f..38dc2b5 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/NewPlayer.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/NewPlayer.kt @@ -25,7 +25,6 @@ import androidx.media3.common.Player import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow -import java.util.stream.Stream import kotlin.Exception enum class PlayMode { @@ -45,7 +44,8 @@ enum class RepeatMode { interface NewPlayer { // preferences - val preferredStreamVariants: List + val preferredVideoVariants: List + val prefearedAudioVariants: List val preferredStreamLanguage: List val exoPlayer: StateFlow diff --git a/new-player/src/main/java/net/newpipe/newplayer/NewPlayerImpl.kt b/new-player/src/main/java/net/newpipe/newplayer/NewPlayerImpl.kt index 99bf7c5..30c82f8 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/NewPlayerImpl.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/NewPlayerImpl.kt @@ -32,6 +32,7 @@ import androidx.media3.common.Player import androidx.media3.common.Timeline import androidx.media3.common.util.UnstableApi import androidx.media3.datasource.DefaultHttpDataSource +import androidx.media3.datasource.HttpDataSource import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.ProgressiveMediaSource @@ -50,6 +51,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import net.newpipe.newplayer.service.NewPlayerService +import net.newpipe.newplayer.utils.StreamSelect import kotlin.random.Random private const val TAG = "NewPlayerImpl" @@ -57,8 +59,10 @@ private const val TAG = "NewPlayerImpl" class NewPlayerImpl( val app: Application, private val repository: MediaRepository, - override val preferredStreamVariants: List = emptyList(), - override val preferredStreamLanguage: List = emptyList() + override val preferredVideoVariants: List = emptyList(), + override val preferredStreamLanguage: List = emptyList(), + override val prefearedAudioVariants: List = emptyList(), + val httpDataSourceFactory: HttpDataSource.Factory = DefaultHttpDataSource.Factory(), ) : NewPlayer { private val mutableExoPlayer = MutableStateFlow(null) @@ -321,7 +325,11 @@ class NewPlayerImpl( uniqueIdToIdLookup[uniqueId] = item val mediaItemBuilder = MediaItem.Builder() .setMediaId(uniqueId.toString()) - .setUri(dataStream) + .setUri(dataStream.streamUri) + + if(dataStream.mimeType != null) { + mediaItemBuilder.setMimeType(dataStream.mimeType) + } try { val metadata = repository.getMetaInfo(item) @@ -332,21 +340,17 @@ class NewPlayerImpl( val mediaItem = mediaItemBuilder.build() - return ProgressiveMediaSource.Factory(DefaultHttpDataSource.Factory()) + return ProgressiveMediaSource.Factory(httpDataSourceFactory) .createMediaSource(mediaItem) } + + private suspend fun toMediaSource(item: String, playMode: PlayMode): MediaSource { val availableStreams = repository.getAvailableStreamVariants(item) - var selectedStream = availableStreams[availableStreams.size / 2] - for (preferredStream in preferredStreamVariants) { - for (availableStream in availableStreams) { - if (preferredStream == availableStream.streamVariantIdentifier) { - selectedStream = availableStream - } - } - } + //var selectedStream = availableStreams[availableStreams.size / 2] + return toMediaSource(item, selectedStream) } diff --git a/new-player/src/main/java/net/newpipe/newplayer/utils/StreamSelect.kt b/new-player/src/main/java/net/newpipe/newplayer/utils/StreamSelect.kt new file mode 100644 index 0000000..c076528 --- /dev/null +++ b/new-player/src/main/java/net/newpipe/newplayer/utils/StreamSelect.kt @@ -0,0 +1,235 @@ +package net.newpipe.newplayer.utils + + +import net.newpipe.newplayer.NewPlayerException +import net.newpipe.newplayer.PlayMode +import net.newpipe.newplayer.StreamType +import net.newpipe.newplayer.StreamVariant + +object StreamSelect { + + interface StreamSelection + + data class SingleSelection( + val streamVariant: StreamVariant + ) : StreamSelection + + data class MultiSelection( + val videoStream: StreamVariant, + val audioStream: StreamVariant + ) : StreamSelection + + + private fun getBestLanguageFit( + availableStreamVariants: List, + preferredLanguages: List + ): String? { + for (preferredLanguage in preferredLanguages) { + for (availableVariant in availableStreamVariants) { + if (availableVariant.language == preferredLanguage) { + return preferredLanguage + } + } + } + return null + } + + private fun filterVariantsByLanguage( + availableStreamVariants: List, + language: String + ) = + availableStreamVariants.filter { it.language == language } + + private fun getBestFittingVideoIdentifier( + availableStreamVariants: List, + preferredVideoIdentifier: List + ): String? { + for (preferredStream in preferredVideoIdentifier) { + for (availableVariant in availableStreamVariants) { + if ((availableVariant.streamType == StreamType.AUDIO_AND_VIDEO || + availableVariant.streamType == StreamType.VIDEO) + && preferredStream == availableVariant.streamVariantIdentifier + ) { + return preferredStream + } + } + } + return null + } + + private fun getFirstVariantMatchingIdentifier( + availableStreamVariants: List, + identifier: String + ): StreamVariant? { + for (variant in availableStreamVariants) { + if (variant.streamVariantIdentifier == identifier) + return variant + } + return null + } + + private fun getBestFittingAudioVariant( + availableStreamVariants: List, + preferredAudioIdentifier: List + ): StreamVariant? { + for (preferredStream in preferredAudioIdentifier) { + for (availableStream in availableStreamVariants) { + if (availableStream.streamType == StreamType.AUDIO + && preferredStream == availableStream.streamVariantIdentifier + ) { + return availableStream + } + } + } + return null + } + + private fun getVideoOnlyVariantWithMatchingIdentifier( + availableStreamVariants: List, + identifier: String + ): StreamVariant? { + for (variant in availableStreamVariants) { + if (variant.streamType == StreamType.VIDEO + && variant.streamVariantIdentifier == identifier + ) + return variant + } + return null + } + + private fun getDynamicStream(availableStreamVariants: List): StreamVariant? { + for (variant in availableStreamVariants) { + if (variant.streamType == StreamType.DYNAMIC) { + return variant + } + } + return null + } + + private fun getNonDynamicVideoVariants(availableStreamVariants: List) = + availableStreamVariants.filter { + it.streamType == StreamType.VIDEO || it.streamType == StreamType.AUDIO_AND_VIDEO + } + + private fun getNonDynamicAudioVariants(availableStreamVariants: List) = + availableStreamVariants.filter { it.streamType == StreamType.AUDIO } + + fun selectStream( + item: String, + playMode: PlayMode, + availableStreamVariants: List, + preferredVideoIdentifier: List, + preferredAudioIdentifier: List, + preferredLanguage: List + ): StreamSelection { + + val bestFittingLanguage = getBestLanguageFit(availableStreamVariants, preferredLanguage) + val availableVariantsInPreferredLanguage = + if (bestFittingLanguage != null) filterVariantsByLanguage( + availableStreamVariants, + bestFittingLanguage + ) + else { + emptyList() + } + + if (playMode == PlayMode.FULLSCREEN_VIDEO + || playMode == PlayMode.EMBEDDED_VIDEO + || playMode == PlayMode.PIP + ) { + getDynamicStream(availableVariantsInPreferredLanguage) + ?: getDynamicStream( + availableStreamVariants + )?.let { + return SingleSelection(it) + } + + val bestIdentifier = + getBestFittingVideoIdentifier( + availableVariantsInPreferredLanguage, + preferredVideoIdentifier + )?.let { + val videoVariants = + getNonDynamicVideoVariants(availableVariantsInPreferredLanguage) + videoVariants[videoVariants.size / 2].streamVariantIdentifier + } ?: getBestFittingVideoIdentifier( + availableStreamVariants, + preferredVideoIdentifier + ) + ?: run { + val videoVariants = getNonDynamicVideoVariants(availableStreamVariants) + videoVariants[videoVariants.size / 2].streamVariantIdentifier + } + + val videoOnlyStream = + getVideoOnlyVariantWithMatchingIdentifier( + availableVariantsInPreferredLanguage, + bestIdentifier + ) ?: getVideoOnlyVariantWithMatchingIdentifier( + availableStreamVariants, + bestIdentifier + ) + + if (videoOnlyStream != null) { + getBestFittingAudioVariant( + availableVariantsInPreferredLanguage, + preferredAudioIdentifier + ) ?: getBestFittingAudioVariant(availableStreamVariants, preferredAudioIdentifier) + ?.let { + return MultiSelection(videoOnlyStream, it) + } + } + + getFirstVariantMatchingIdentifier(availableVariantsInPreferredLanguage, bestIdentifier) + ?: getFirstVariantMatchingIdentifier(availableStreamVariants, bestIdentifier)?.let { + return SingleSelection(it) + } + + return SingleSelection(run { + val videoVariants = + getNonDynamicVideoVariants(availableVariantsInPreferredLanguage).let { + if (it.isNotEmpty()) { + it + } else { + getNonDynamicVideoVariants(availableStreamVariants) + } + } + if (videoVariants.isNotEmpty()) { + return@run videoVariants[videoVariants.size / 2] + } else { + throw NewPlayerException("No fitting video stream could be found for stream item item: ${item}") + } + }) + + } else { + getBestFittingAudioVariant( + availableVariantsInPreferredLanguage, + preferredAudioIdentifier + ) + ?: getBestFittingAudioVariant( + availableStreamVariants, + preferredAudioIdentifier + )?.let { + return SingleSelection(it) + } + + return SingleSelection(run { + val audioVariants = + getNonDynamicAudioVariants(availableVariantsInPreferredLanguage).let{ + if(it.isNotEmpty()) { + it + } else { + getNonDynamicAudioVariants(availableStreamVariants) + } + } + if (audioVariants.isNotEmpty()) { + return@run audioVariants[audioVariants.size / 2] + } else { + throw NewPlayerException("No fitting audio stream could be found for stream item item: ${item}") + } + }) + } + + + } +} \ No newline at end of file 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 3090053..7a7a3c5 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 @@ -9,6 +9,7 @@ import androidx.media3.common.util.UnstableApi import net.newpipe.newplayer.Chapter import net.newpipe.newplayer.MediaRepository import net.newpipe.newplayer.RepoMetaInfo +import net.newpipe.newplayer.Stream import net.newpipe.newplayer.StreamType import net.newpipe.newplayer.StreamVariant import okhttp3.OkHttpClient @@ -101,19 +102,33 @@ class TestMediaRepository(val context: Context) : MediaRepository { override suspend fun getStream(item: String, streamVariantSelector: StreamVariant) = - Uri.parse( - when (item) { - "6502" -> context.getString(R.string.ccc_6502_video) - "portrait" -> context.getString(R.string.portrait_video_example) - "imu" -> when (streamVariantSelector.streamVariantIdentifier) { - "1080p" -> context.getString(R.string.ccc_imu_1080_mp4) - "576p" -> context.getString(R.string.ccc_imu_576_mp4) - else -> throw Exception("Unknown stream selector for $item: $streamVariantSelector") - } + when (item) { + "6502" -> Stream( + streamUri = Uri.parse(context.getString(R.string.ccc_6502_video)), + mimeType = null + ) - else -> throw Exception("Unknown stream: $item") + "portrait" -> Stream( + streamUri = Uri.parse(context.getString(R.string.portrait_video_example)), + mimeType = null + ) + + "imu" -> when (streamVariantSelector.streamVariantIdentifier) { + "1080p" -> Stream( + streamUri = Uri.parse(context.getString(R.string.ccc_imu_1080_mp4)), + mimeType = null + ) + + "576p" -> Stream( + streamUri = Uri.parse(context.getString(R.string.ccc_imu_576_mp4)), + mimeType = null + ) + + else -> throw Exception("Unknown stream selector for $item: $streamVariantSelector") } - ) + + else -> throw Exception("Unknown stream: $item") + } override suspend fun getSubtitle(item: String, variant: String) = Uri.parse(