From cde9b1f9ba71200087d9b00084e192cabbe489f3 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Tue, 17 Sep 2024 15:12:40 +0200 Subject: [PATCH] do some commit --- .../java/net/newpipe/newplayer/NewPlayer.kt | 8 + .../net/newpipe/newplayer/NewPlayerImpl.kt | 88 +++++- .../newplayer/model/VideoPlayerUIState.kt | 13 +- .../model/VideoPlayerViewModelImpl.kt | 22 ++ .../newpipe/newplayer/ui/videoplayer/Menu.kt | 3 + .../newplayer/utils/MediaSourceBuilder.kt | 29 +- .../newpipe/newplayer/utils/StreamSelect.kt | 279 ----------------- .../newpipe/newplayer/utils/StreamSelector.kt | 285 ++++++++++++++++++ 8 files changed, 411 insertions(+), 316 deletions(-) delete mode 100644 new-player/src/main/java/net/newpipe/newplayer/utils/StreamSelect.kt create mode 100644 new-player/src/main/java/net/newpipe/newplayer/utils/StreamSelector.kt 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 914a471..226143a 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/NewPlayer.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/NewPlayer.kt @@ -42,6 +42,11 @@ enum class RepeatMode { REPEAT_ONE } +data class StreamVariants( + val identifiers: List, + val languages: List +) + interface NewPlayer { // preferences val preferredVideoVariants: List @@ -63,6 +68,9 @@ interface NewPlayer { var currentlyPlayingPlaylistItem: Int val currentChapters: StateFlow> + val availableStreamVariants: StateFlow + val currentlySelectedLanguage: StateFlow + val currentlySelectedStreamVariant: StateFlow // callbacks 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 90fbee7..de2ba4e 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/NewPlayerImpl.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/NewPlayerImpl.kt @@ -35,8 +35,6 @@ 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.MergingMediaSource -import androidx.media3.exoplayer.source.ProgressiveMediaSource import androidx.media3.session.MediaController import androidx.media3.session.SessionToken import com.google.common.util.concurrent.MoreExecutors @@ -53,8 +51,7 @@ 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 +import net.newpipe.newplayer.utils.StreamSelector private const val TAG = "NewPlayerImpl" @@ -73,6 +70,7 @@ class NewPlayerImpl( private var playerScope = CoroutineScope(Dispatchers.Main + Job()) private var uniqueIdToIdLookup = HashMap() + private var uniqueIdToStreamVariantSelection = HashMap() // this is used to take care of the NewPlayerService private var mediaController: MediaController? = null @@ -138,6 +136,14 @@ class NewPlayerImpl( private val mutableCurrentChapter = MutableStateFlow>(emptyList()) override val currentChapters: StateFlow> = mutableCurrentChapter.asStateFlow() + private val mutableCurrentlySelectedLanguage = MutableStateFlow(null) + override val currentlySelectedLanguage = mutableCurrentlySelectedLanguage.asStateFlow() + + private val mutableCurrentlySelectedStreamVariant = MutableStateFlow(null) + override val currentlySelectedStreamVariant = + mutableCurrentlySelectedStreamVariant.asStateFlow() + + override var currentlyPlayingPlaylistItem: Int get() = exoPlayer.value?.currentMediaItemIndex ?: -1 set(value) { @@ -147,6 +153,9 @@ class NewPlayerImpl( exoPlayer.value?.seekTo(value, 0) } + private var mutableAvailableStreamVariants = MutableStateFlow(null) + override val availableStreamVariants = mutableAvailableStreamVariants.asStateFlow() + private fun setupNewExoplayer() { val newExoPlayer = ExoPlayer.Builder(app) .setAudioAttributes(AudioAttributes.DEFAULT, true) @@ -184,6 +193,13 @@ class NewPlayerImpl( override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { super.onMediaItemTransition(mediaItem, reason) mutableCurrentlyPlaying.update { mediaItem } + if (mediaItem != null) { + val item = uniqueIdToIdLookup[mediaItem.mediaId.toLong()]!! + updateStreamVariants(item) + updateStreamSelection(mediaItem) + } else { + mutableAvailableStreamVariants.update { null } + } } }) mutableExoPlayer.update { @@ -243,7 +259,7 @@ class NewPlayerImpl( prepare() } launchJobAndCollectError { - val mediaSource = toMediaSource(item, playBackMode.value) + val mediaSource = toMediaSource(item) exoPlayer.value?.addMediaSource(mediaSource) } } @@ -262,17 +278,50 @@ class NewPlayerImpl( } } + @OptIn(UnstableApi::class) override fun playStream(item: String, playMode: PlayMode) { launchJobAndCollectError { - val mediaItem = toMediaSource(item, playMode) - internalPlayStream(mediaItem, playMode) + val mediaSource = toMediaSource(item) + updateStreamSelection(mediaSource.mediaItem) + internalPlayStream(mediaSource, playMode) + } + updateStreamVariants(item) + } + + private fun updateStreamVariants(item: String) { + launchJobAndCollectError { + val streams = repository.getStreams(item) + mutableAvailableStreamVariants.update { + StreamVariants( + identifiers = streams.map { it.identifier }, + languages = streams.mapNotNull { it.language } + ) + } + } + } + + private fun updateStreamSelection(mediaItem: MediaItem) { + val selection = uniqueIdToStreamVariantSelection[mediaItem.mediaId.toLong()] + when (selection) { + is StreamSelector.SingleSelection -> { + if (selection.stream.streamType != StreamType.DYNAMIC) { + mutableCurrentlySelectedLanguage.update { selection.stream.language } + mutableCurrentlySelectedStreamVariant.update { selection.stream.identifier } + } + } + + /* + is StreamSelector.MultiSelection -> { + TODO("TRACKSELECTION HAS TO DO THE JOB HERE") + } + */ } } @OptIn(UnstableApi::class) override fun selectChapter(index: Int) { val chapters = currentChapters.value - assert(index in 0.. Unit) = 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 7ba239e..1d70b56 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 @@ -74,7 +74,10 @@ data class VideoPlayerUIState( val repeatMode: RepeatMode, val playListDurationInS: Int, val currentlyPlaying: MediaItem?, - val currentPlaylistItemIndex: Int + val currentPlaylistItemIndex: Int, + val availableStreamVariants: List, + val availableLanguages: List, + val availableSubtitles: List ) { companion object { val DEFAULT = VideoPlayerUIState( @@ -99,10 +102,16 @@ data class VideoPlayerUIState( repeatMode = RepeatMode.DO_NOT_REPEAT, playListDurationInS = 0, currentlyPlaying = null, - currentPlaylistItemIndex = 0 + currentPlaylistItemIndex = 0, + availableLanguages = emptyList(), + availableSubtitles = emptyList(), + availableStreamVariants = emptyList() ) val DUMMY = DEFAULT.copy( + availableLanguages = listOf("German", "English", "Spanish"), + availableSubtitles = listOf("German", "English", "Spanish"), + availableStreamVariants = listOf("460p", "720p", "1080p60"), uiMode = UIModeState.EMBEDDED_VIDEO, playing = true, seekerPosition = 0.3f, 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 d75a30b..4030602 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 @@ -42,6 +42,7 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow import javax.inject.Inject import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import net.newpipe.newplayer.utils.VideoSize @@ -224,6 +225,7 @@ class VideoPlayerViewModelImpl @Inject constructor( } } } + viewModelScope.launch { newPlayer.currentChapters.collect { chapters -> mutableUiState.update { @@ -232,6 +234,26 @@ class VideoPlayerViewModelImpl @Inject constructor( } } + viewModelScope.launch { + newPlayer.availableStreamVariants.collect { availableVariants -> + if (availableVariants != null) { + mutableUiState.update { + it.copy( + availableStreamVariants = availableVariants.identifiers, + availableLanguages = availableVariants.languages + ) + } + } else { + mutableUiState.update { + it.copy( + availableLanguages = emptyList(), + availableStreamVariants = emptyList() + ) + } + } + } + } + mutableUiState.update { it.copy( playing = newPlayer.exoPlayer.value?.isPlaying ?: false, diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/Menu.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/Menu.kt index b14e2bb..95b2a74 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/Menu.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/Menu.kt @@ -20,6 +20,7 @@ package net.newpipe.newplayer.ui.videoplayer +import androidx.annotation.OptIn import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.icons.Icons @@ -51,12 +52,14 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp +import androidx.media3.common.util.UnstableApi import net.newpipe.newplayer.R import net.newpipe.newplayer.model.VideoPlayerUIState import net.newpipe.newplayer.model.VideoPlayerViewModel import net.newpipe.newplayer.model.VideoPlayerViewModelDummy import net.newpipe.newplayer.ui.theme.VideoPlayerTheme +@OptIn(UnstableApi::class) @Composable fun DropDownMenu(viewModel: VideoPlayerViewModel, uiState: VideoPlayerUIState) { var showMainMenu: Boolean by remember { mutableStateOf(false) } 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 efbf254..2c12f38 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 @@ -20,35 +20,22 @@ 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 = DefaultHttpDataSource.Factory(), + private val httpDataSourceFactory: HttpDataSource.Factory, ) { @OptIn(UnstableApi::class) - suspend fun buildMediaSource(item: String): MediaSource { - val availableStreams = repository.getStreams(item) - val selectedStream = StreamSelect.selectStream( - item, - playMode, - availableStreams, - preferredAudioId, - preferredAudioId, - preferredLanguage - ) + suspend fun buildMediaSource(selectedStream: StreamSelector.StreamSelection): MediaSource { val mediaSource = when (selectedStream) { - is StreamSelect.SingleSelection -> { - val mediaItem = toMediaItem(item, selectedStream.stream) - val mediaItemWithMetadata = addMetadata(mediaItem, item) + is StreamSelector.SingleSelection -> { + val mediaItem = toMediaItem(selectedStream.item, selectedStream.stream) + val mediaItemWithMetadata = addMetadata(mediaItem, selectedStream.item) toMediaSource(mediaItemWithMetadata, selectedStream.stream) } - is StreamSelect.MultiSelection -> { - val mediaItems = ArrayList(selectedStream.streams.map { toMediaItem(item, it) }) - mediaItems[0] = addMetadata(mediaItems[0], item) + is StreamSelector.MultiSelection -> { + val mediaItems = ArrayList(selectedStream.streams.map { toMediaItem(selectedStream.item, it) }) + mediaItems[0] = addMetadata(mediaItems[0], selectedStream.item) val mediaSources = mediaItems.zip(selectedStream.streams) .map { toMediaSource(it.first, it.second) } MergingMediaSource( 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 deleted file mode 100644 index f798fc2..0000000 --- a/new-player/src/main/java/net/newpipe/newplayer/utils/StreamSelect.kt +++ /dev/null @@ -1,279 +0,0 @@ -package net.newpipe.newplayer.utils - - -import net.newpipe.newplayer.NewPlayerException -import net.newpipe.newplayer.PlayMode -import net.newpipe.newplayer.StreamType -import net.newpipe.newplayer.Stream - -object StreamSelect { - - interface StreamSelection - - data class SingleSelection( - val stream: Stream - ) : StreamSelection - - data class MultiSelection( - val streams: List - ) : StreamSelection - - - private fun getBestLanguageFit( - availableStreams: List, preferredLanguages: List - ): String? { - for (preferredLanguage in preferredLanguages) { - for (available in availableStreams) { - if (available.language == preferredLanguage) { - return preferredLanguage - } - } - } - return null - } - - private fun filtersByLanguage( - availableStreams: List, language: String - ) = availableStreams.filter { it.language == language } - - private fun getBestFittingVideoIdentifier( - 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) { - return preferredStream - } - } - } - 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 - ): Stream? { - for (variant in availableStreams) { - if (variant.identifier == identifier) return variant - } - return null - } - - private fun getBestFittingAudio( - availableStreams: List, preferredAudioIdentifier: List - ): Stream? { - for (preferredStream in preferredAudioIdentifier) { - for (availableStream in availableStreams) { - if (availableStream.streamType == StreamType.AUDIO && preferredStream == availableStream.identifier) { - return availableStream - } - } - } - return null - } - - 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, - ): 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 - } - return null - } - - private fun getDynamicStream(availableStreams: List): Stream? { - for (variant in availableStreams) { - if (variant.streamType == StreamType.DYNAMIC) { - return variant - } - } - return null - } - - 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 - } - 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, - 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() - } - - - // 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 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) - } - - // fifth: try and get the median video and audio stream variant - - return SingleSelection(run { - val videos = getNonDynamicVideos(availablesInPreferredLanguage).ifEmpty { - getNonDynamicVideos(availableStreams) - } - - if (videos.isNotEmpty()) { - return@run videos[videos.size / 2] - } else { - throw NewPlayerException("No fitting video stream could be found for stream item item: ${item}") - } - }) - - } else { /* if(hasVideoStreams(availableStreams)) */ - - // first: try to get an audio stream variant with the best fitting identifier - - getBestFittingAudio( - 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) - } - } - if (audios.isNotEmpty()) { - return@run audios[audios.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/new-player/src/main/java/net/newpipe/newplayer/utils/StreamSelector.kt b/new-player/src/main/java/net/newpipe/newplayer/utils/StreamSelector.kt new file mode 100644 index 0000000..7f40340 --- /dev/null +++ b/new-player/src/main/java/net/newpipe/newplayer/utils/StreamSelector.kt @@ -0,0 +1,285 @@ +package net.newpipe.newplayer.utils + + +import net.newpipe.newplayer.NewPlayerException +import net.newpipe.newplayer.StreamType +import net.newpipe.newplayer.Stream + +class StreamSelector( + val preferredVideoIdentifier: List, + val preferredAudioIdentifier: List, + val preferredLanguage: List, +) { + + interface StreamSelection { + val item: String + } + + data class SingleSelection( + override val item: String, + val stream: Stream + ) : StreamSelection + + data class MultiSelection( + override val item: String, + val streams: List + ) : StreamSelection + + enum class DemuxedStreamBundeling { + DO_NOT_BUNDLE, BUNDLE_STREAMS_WITH_SAME_ID, BUNDLE_AUDIOSTREAMS_WITH_SAME_ID + } + + fun selectStream( + item: String, + availableStreams: 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() + } + + + // 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(item, 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(item, 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(item, streams) + } + val audioStream = getBestFittingAudio( + availablesInPreferredLanguage, preferredAudioIdentifier + ) ?: getBestFittingAudio(availableStreams, preferredAudioIdentifier) + ?: availableStreams.filter { it.streamType == StreamType.AUDIO }[0] + + return MultiSelection(item, 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(item, it) + } + + // fifth: try and get the median video and audio stream variant + + return SingleSelection(item, run { + val videos = getNonDynamicVideos(availablesInPreferredLanguage).ifEmpty { + getNonDynamicVideos(availableStreams) + } + + if (videos.isNotEmpty()) { + return@run videos[videos.size / 2] + } else { + throw NewPlayerException("No fitting video stream could be found for stream item item: ${item}") + } + }) + + } else { /* if(hasVideoStreams(availableStreams)) */ + + // first: try to get an audio stream variant with the best fitting identifier + + getBestFittingAudio( + availablesInPreferredLanguage, preferredAudioIdentifier + ) ?: getBestFittingAudio( + availableStreams, preferredAudioIdentifier + )?.let { + return SingleSelection(item, it) + } + + // second: try and get the median audio stream variant + + return SingleSelection(item, run { + val audios = getNonDynamicAudios(availablesInPreferredLanguage).let { + if (it.isNotEmpty()) { + it + } else { + getNonDynamicAudios(availableStreams) + } + } + if (audios.isNotEmpty()) { + return@run audios[audios.size / 2] + } else { + throw NewPlayerException("No fitting audio stream could be found for stream item item: ${item}") + } + }) + } + } + + companion object { + + private fun getBestLanguageFit( + availableStreams: List, preferredLanguages: List + ): String? { + for (preferredLanguage in preferredLanguages) { + for (available in availableStreams) { + if (available.language == preferredLanguage) { + return preferredLanguage + } + } + } + return null + } + + private fun filtersByLanguage( + availableStreams: List, language: String + ) = availableStreams.filter { it.language == language } + + private fun getBestFittingVideoIdentifier( + 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) { + return preferredStream + } + } + } + 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 + ): Stream? { + for (variant in availableStreams) { + if (variant.identifier == identifier) return variant + } + return null + } + + private fun getBestFittingAudio( + availableStreams: List, preferredAudioIdentifier: List + ): Stream? { + for (preferredStream in preferredAudioIdentifier) { + for (availableStream in availableStreams) { + if (availableStream.streamType == StreamType.AUDIO && preferredStream == availableStream.identifier) { + return availableStream + } + } + } + return null + } + + 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, + ): 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 + } + return null + } + + private fun getDynamicStream(availableStreams: List): Stream? { + for (variant in availableStreams) { + if (variant.streamType == StreamType.DYNAMIC) { + return variant + } + } + return null + } + + 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 + } + return false + } + + } +} \ No newline at end of file