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 5d1c263..914a471 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/NewPlayer.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/NewPlayer.kt @@ -78,7 +78,6 @@ interface NewPlayer { fun removePlaylistItem(uniqueId: Long) fun playStream(item: String, playMode: PlayMode) fun selectChapter(index: Int) - fun playStream(item: String, streamVariant: StreamVariant, playMode: PlayMode) fun release() fun getItemLinkOfMediaItem(mediaItem: MediaItem) : String } 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 aef380f..90fbee7 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/NewPlayerImpl.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/NewPlayerImpl.kt @@ -52,6 +52,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.MediaSourceBuilder import net.newpipe.newplayer.utils.StreamSelect import kotlin.random.Random @@ -268,17 +269,6 @@ class NewPlayerImpl( } } - override fun playStream( - item: String, - streamVariant: StreamVariant, - playMode: PlayMode - ) { - launchJobAndCollectError { - val stream = toMediaSource(item, streamVariant) - internalPlayStream(stream, playMode) - } - } - @OptIn(UnstableApi::class) override fun selectChapter(index: Int) { val chapters = currentChapters.value @@ -317,64 +307,21 @@ class NewPlayerImpl( this.exoPlayer.value?.play() } - @OptIn(UnstableApi::class) - private suspend - fun toMediaSource(item: String, streamVariant: StreamVariant): MediaSource { - val dataStream = repository.getStream(item, streamVariant) - - val uniqueId = Random.nextLong() - uniqueIdToIdLookup[uniqueId] = item - val mediaItemBuilder = MediaItem.Builder() - .setMediaId(uniqueId.toString()) - .setUri(dataStream.streamUri) - - if (dataStream.mimeType != null) { - mediaItemBuilder.setMimeType(dataStream.mimeType) - } - - try { - val metadata = repository.getMetaInfo(item) - mediaItemBuilder.setMediaMetadata(metadata) - } catch (e: Exception) { - mutableErrorFlow.emit(e) - } - - val mediaItem = mediaItemBuilder.build() - - return ProgressiveMediaSource.Factory(httpDataSourceFactory) - .createMediaSource(mediaItem) - } - @OptIn(UnstableApi::class) private suspend fun toMediaSource(item: String, playMode: PlayMode): MediaSource { - val availableStreamVariants = repository.getAvailableStreamVariants(item) - val selectedStreamVariant = StreamSelect.selectStream( - item, - playMode, - availableStreamVariants, - preferredVideoVariants, - preferredAudioVariants, - preferredStreamLanguage + val builder = MediaSourceBuilder( + repository = repository, + uniqueIdToIdLookup = uniqueIdToIdLookup, + preferredLanguage = preferredStreamLanguage, + preferredAudioId = preferredAudioVariants, + preferredVideoId = preferredVideoVariants, + playMode = playMode, + mutableErrorFlow = mutableErrorFlow, ) - - return when (selectedStreamVariant) { - is StreamSelect.SingleSelection -> toMediaSource( - item, - selectedStreamVariant.streamVariant - ) - - is StreamSelect.MultiSelection -> MergingMediaSource( - toMediaSource( - item, - selectedStreamVariant.videoStream - ), toMediaSource(item, selectedStreamVariant.audioStream) - ) - - else -> throw NewPlayerException("Unknown Stream selection type: Serious Programming error. :${selectedStreamVariant.javaClass}") - } + return builder.buildMediaSource(item) } private fun launchJobAndCollectError(task: suspend () -> Unit) = diff --git a/new-player/src/main/java/net/newpipe/newplayer/utils/MediaSourceBuilder.kt b/new-player/src/main/java/net/newpipe/newplayer/utils/MediaSourceBuilder.kt index 7d010bc..efbf254 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/utils/MediaSourceBuilder.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/utils/MediaSourceBuilder.kt @@ -3,11 +3,16 @@ package net.newpipe.newplayer.utils import androidx.annotation.OptIn import androidx.media3.common.MediaItem import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.DefaultHttpDataSource import androidx.media3.datasource.HttpDataSource import androidx.media3.exoplayer.dash.DashMediaSource +import androidx.media3.exoplayer.source.MediaSource +import androidx.media3.exoplayer.source.MergingMediaSource import androidx.media3.exoplayer.source.ProgressiveMediaSource import kotlinx.coroutines.flow.MutableSharedFlow import net.newpipe.newplayer.MediaRepository +import net.newpipe.newplayer.NewPlayerException +import net.newpipe.newplayer.PlayMode import net.newpipe.newplayer.StreamType import net.newpipe.newplayer.Stream import kotlin.random.Random @@ -15,12 +20,47 @@ import kotlin.random.Random class MediaSourceBuilder( private val repository: MediaRepository, private val uniqueIdToIdLookup: HashMap, + private val preferredLanguage: List, + private val preferredAudioId: List, + private val preferredVideoId: List, + private val playMode: PlayMode, private val mutableErrorFlow: MutableSharedFlow, - private val httpDataSourceFactory: HttpDataSource.Factory + private val httpDataSourceFactory: HttpDataSource.Factory = DefaultHttpDataSource.Factory(), ) { - suspend fun buildMediaSource(item: String) { + @OptIn(UnstableApi::class) + suspend fun buildMediaSource(item: String): MediaSource { val availableStreams = repository.getStreams(item) + val selectedStream = StreamSelect.selectStream( + item, + playMode, + availableStreams, + preferredAudioId, + preferredAudioId, + preferredLanguage + ) + val mediaSource = when (selectedStream) { + is StreamSelect.SingleSelection -> { + val mediaItem = toMediaItem(item, selectedStream.stream) + val mediaItemWithMetadata = addMetadata(mediaItem, item) + toMediaSource(mediaItemWithMetadata, selectedStream.stream) + } + + is StreamSelect.MultiSelection -> { + val mediaItems = ArrayList(selectedStream.streams.map { toMediaItem(item, it) }) + mediaItems[0] = addMetadata(mediaItems[0], item) + val mediaSources = mediaItems.zip(selectedStream.streams) + .map { toMediaSource(it.first, it.second) } + MergingMediaSource( + true, true, + *mediaSources.toTypedArray() + ) + } + + else -> throw NewPlayerException("Unknown stream selection class: ${selectedStream.javaClass}") + } + + return mediaSource } @OptIn(UnstableApi::class) @@ -41,7 +81,7 @@ class MediaSourceBuilder( } @OptIn(UnstableApi::class) - private fun toMediaSource(mediaItem: MediaItem, stream: Stream) = + private fun toMediaSource(mediaItem: MediaItem, stream: Stream): MediaSource = if (stream.streamType == StreamType.DYNAMIC) DashMediaSource.Factory(httpDataSourceFactory) .createMediaSource(mediaItem) 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 index a82cce0..f798fc2 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/utils/StreamSelect.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/utils/StreamSelect.kt @@ -15,14 +15,12 @@ object StreamSelect { ) : StreamSelection data class MultiSelection( - val videoStream: Stream, - val audioStream: Stream + val streams: List ) : StreamSelection private fun getBestLanguageFit( - availableStreams: List, - preferredLanguages: List + availableStreams: List, preferredLanguages: List ): String? { for (preferredLanguage in preferredLanguages) { for (available in availableStreams) { @@ -35,21 +33,15 @@ object StreamSelect { } private fun filtersByLanguage( - availableStreams: List, - language: String - ) = - availableStreams.filter { it.language == language } + availableStreams: List, language: String + ) = availableStreams.filter { it.language == language } private fun getBestFittingVideoIdentifier( - availableStreams: List, - preferredVideoIdentifier: List + availableStreams: List, preferredVideoIdentifier: List ): String? { for (preferredStream in preferredVideoIdentifier) { for (available in availableStreams) { - if ((available.streamType == StreamType.AUDIO_AND_VIDEO || - available.streamType == StreamType.VIDEO) - && preferredStream == available.identifier - ) { + if ((available.streamType == StreamType.AUDIO_AND_VIDEO || available.streamType == StreamType.VIDEO) && preferredStream == available.identifier) { return preferredStream } } @@ -57,26 +49,31 @@ object StreamSelect { return null } + private fun tryAndGetMedianVideoOnlyStream(availableStreams: List) = + availableStreams.filter { it.streamType == StreamType.VIDEO }.ifEmpty { null }?.let { + it[it.size / 2] + } + + private fun tryAndGetMedianAudioOnlyStream(availableStreams: List) = + availableStreams.filter { it.streamType == StreamType.AUDIO }.ifEmpty { null }?.let { + it[it.size / 2] + } + private fun getFirstMatchingIdentifier( - availableStreams: List, - identifier: String + availableStreams: List, identifier: String ): Stream? { for (variant in availableStreams) { - if (variant.identifier == identifier) - return variant + if (variant.identifier == identifier) return variant } return null } private fun getBestFittingAudio( - availableStreams: List, - preferredAudioIdentifier: List + availableStreams: List, preferredAudioIdentifier: List ): Stream? { for (preferredStream in preferredAudioIdentifier) { for (availableStream in availableStreams) { - if (availableStream.streamType == StreamType.AUDIO - && preferredStream == availableStream.identifier - ) { + if (availableStream.streamType == StreamType.AUDIO && preferredStream == availableStream.identifier) { return availableStream } } @@ -84,15 +81,48 @@ object StreamSelect { return null } - private fun getVideoOnlyWithMatchingIdentifier( + private fun getBestFittingAudioStreams( + availableStreams: List, preferredAudioIdentifier: List + ): List? { + val bundles = + bundledStreamsWithSameIdentifier(availableStreams.filter { it.streamType == StreamType.AUDIO }) + for (preferred in preferredAudioIdentifier) { + val streams = bundles[preferred] + if (streams != null) { + return streams + } + } + return null + } + + private fun getDemuxedStreams( + availableStreams: List + ) = availableStreams.filter { + it.streamType == StreamType.AUDIO || it.streamType == StreamType.VIDEO + } + + /** + * This is needed to bundle streams with the same quality but different languages + */ + private fun bundledStreamsWithSameIdentifier( availableStreams: List, - identifier: String + ): HashMap> { + val streamsBundledByIdentifier = HashMap>() + for (stream in availableStreams) { + streamsBundledByIdentifier[stream.identifier] ?: run { + val array = ArrayList() + streamsBundledByIdentifier[stream.identifier] = array + array + }.add(stream) + } + return streamsBundledByIdentifier + } + + private fun getVideoOnlyWithMatchingIdentifier( + availableStreams: List, identifier: String ): Stream? { for (variant in availableStreams) { - if (variant.streamType == StreamType.VIDEO - && variant.identifier == identifier - ) - return variant + if (variant.streamType == StreamType.VIDEO && variant.identifier == identifier) return variant } return null } @@ -106,113 +136,108 @@ object StreamSelect { return null } - private fun getNonDynamicVideos(availableStreams: List) = - availableStreams.filter { - it.streamType == StreamType.VIDEO || it.streamType == StreamType.AUDIO_AND_VIDEO - } + private fun getNonDynamicVideos(availableStreams: List) = availableStreams.filter { + it.streamType == StreamType.VIDEO || it.streamType == StreamType.AUDIO_AND_VIDEO + } private fun getNonDynamicAudios(availableStreams: List) = availableStreams.filter { it.streamType == StreamType.AUDIO } private fun hasVideoStreams(availableStreams: List): Boolean { for (variant in availableStreams) { - if (variant.streamType == StreamType.AUDIO_AND_VIDEO || variant.streamType == StreamType.VIDEO || variant.streamType == StreamType.DYNAMIC) - return true + if (variant.streamType == StreamType.AUDIO_AND_VIDEO || variant.streamType == StreamType.VIDEO || variant.streamType == StreamType.DYNAMIC) return true } return false } + enum class DemuxedStreamBundeling { + DO_NOT_BUNDLE, BUNDLE_STREAMS_WITH_SAME_ID, BUNDLE_AUDIOSTREAMS_WITH_SAME_ID + } + fun selectStream( item: String, playMode: PlayMode, availableStreams: List, preferredVideoIdentifier: List, preferredAudioIdentifier: List, - preferredLanguage: List + preferredLanguage: List, + demuxedStreamBundeling: DemuxedStreamBundeling = DemuxedStreamBundeling.DO_NOT_BUNDLE ): StreamSelection { // filter for best fitting language stream variants val bestFittingLanguage = getBestLanguageFit(availableStreams, preferredLanguage) - val availablesInPreferredLanguage = - if (bestFittingLanguage != null) filtersByLanguage( - availableStreams, - bestFittingLanguage - ) - else { - emptyList() - } + val availablesInPreferredLanguage = if (bestFittingLanguage != null) filtersByLanguage( + availableStreams, bestFittingLanguage + ) + else { + emptyList() + } // is it a video stream or a pure audio stream? if (hasVideoStreams(availableStreams)) { // first: try and get a dynamic stream variant - getDynamicStream(availablesInPreferredLanguage) - ?: getDynamicStream( - availableStreams - )?.let { - return SingleSelection(it) - } - - // second: try and get seperate audio and video stream variants - - val bestVideoIdentifier = - getBestFittingVideoIdentifier( - availablesInPreferredLanguage, - preferredVideoIdentifier - )?.let { - val videos = - getNonDynamicVideos(availablesInPreferredLanguage) - videos[videos.size / 2].identifier - } ?: getBestFittingVideoIdentifier( - availableStreams, - preferredVideoIdentifier - ) - ?: run { - val videos = getNonDynamicVideos(availableStreams) - videos[videos.size / 2].identifier - } - - val videoOnlyStream = - getVideoOnlyWithMatchingIdentifier( - availablesInPreferredLanguage, - bestVideoIdentifier - ) ?: getVideoOnlyWithMatchingIdentifier( - availableStreams, - bestVideoIdentifier - ) - - if (videoOnlyStream != null) { - getBestFittingAudio( - availablesInPreferredLanguage, - preferredAudioIdentifier - ) ?: getBestFittingAudio(availableStreams, preferredAudioIdentifier) - ?.let { - return MultiSelection(videoOnlyStream, it) - } + getDynamicStream(availablesInPreferredLanguage) ?: getDynamicStream( + availableStreams + )?.let { + return SingleSelection(it) } + // second: try and get separate audio and video stream variants + + val bestVideoIdentifier = getBestFittingVideoIdentifier( + availablesInPreferredLanguage, preferredVideoIdentifier + ) ?: tryAndGetMedianVideoOnlyStream(availablesInPreferredLanguage)?.identifier + ?: getBestFittingVideoIdentifier( + availableStreams, preferredVideoIdentifier + ) ?: tryAndGetMedianVideoOnlyStream(availableStreams)?.identifier ?: "" + + if (demuxedStreamBundeling == DemuxedStreamBundeling.BUNDLE_STREAMS_WITH_SAME_ID) { + return MultiSelection(availableStreams.filter { it.streamType == StreamType.VIDEO || it.streamType == StreamType.AUDIO }) + } else { + + val videoOnlyStream = getVideoOnlyWithMatchingIdentifier( + availablesInPreferredLanguage, bestVideoIdentifier + ) ?: getVideoOnlyWithMatchingIdentifier( + availableStreams, bestVideoIdentifier + ) + + if (videoOnlyStream != null) { + if (demuxedStreamBundeling == DemuxedStreamBundeling.BUNDLE_AUDIOSTREAMS_WITH_SAME_ID) { + val bestFittingAudioStreams = getBestFittingAudioStreams(availableStreams, preferredAudioIdentifier) + ?: listOf(availableStreams.filter { it.streamType == StreamType.AUDIO }[0]) + + val streams = mutableListOf(videoOnlyStream) + streams.addAll(bestFittingAudioStreams) + return MultiSelection(streams) + } + val audioStream = getBestFittingAudio( + availablesInPreferredLanguage, preferredAudioIdentifier + ) ?: getBestFittingAudio(availableStreams, preferredAudioIdentifier) + ?: availableStreams.filter { it.streamType == StreamType.AUDIO }[0] + + return MultiSelection(listOf(videoOnlyStream, audioStream)) + } /* if (vdeioOnlyStream != null) */ + } /* else (demuxedStreamBundeling == DemuxedStreamBundeling.BUNDLE_STREAMS_WITH_SAME_ID) */ + // fourth: try to get a video and audio stream variant with the best fitting identifier getFirstMatchingIdentifier( - availablesInPreferredLanguage, - bestVideoIdentifier - ) - ?: getFirstMatchingIdentifier( - availableStreams, - bestVideoIdentifier - )?.let { - return SingleSelection(it) - } + availablesInPreferredLanguage, bestVideoIdentifier + ) ?: getFirstMatchingIdentifier( + availableStreams, bestVideoIdentifier + )?.let { + return SingleSelection(it) + } // fifth: try and get the median video and audio stream variant return SingleSelection(run { - val videos = - getNonDynamicVideos(availablesInPreferredLanguage).ifEmpty { - getNonDynamicVideos(availableStreams) - } + val videos = getNonDynamicVideos(availablesInPreferredLanguage).ifEmpty { + getNonDynamicVideos(availableStreams) + } if (videos.isNotEmpty()) { return@run videos[videos.size / 2] @@ -226,27 +251,23 @@ object StreamSelect { // first: try to get an audio stream variant with the best fitting identifier getBestFittingAudio( - availablesInPreferredLanguage, - preferredAudioIdentifier - ) - ?: getBestFittingAudio( - availableStreams, - preferredAudioIdentifier - )?.let { - return SingleSelection(it) - } + availablesInPreferredLanguage, preferredAudioIdentifier + ) ?: getBestFittingAudio( + availableStreams, preferredAudioIdentifier + )?.let { + return SingleSelection(it) + } // second: try and get the median audio stream variant return SingleSelection(run { - val audios = - getNonDynamicAudios(availablesInPreferredLanguage).let { - if (it.isNotEmpty()) { - it - } else { - getNonDynamicAudios(availableStreams) - } + val audios = getNonDynamicAudios(availablesInPreferredLanguage).let { + if (it.isNotEmpty()) { + it + } else { + getNonDynamicAudios(availableStreams) } + } if (audios.isNotEmpty()) { return@run audios[audios.size / 2] } else {