do some commit

This commit is contained in:
Christian Schabesberger 2024-09-17 15:12:40 +02:00
parent 20c9bd67e7
commit cde9b1f9ba
8 changed files with 411 additions and 316 deletions

View File

@ -42,6 +42,11 @@ enum class RepeatMode {
REPEAT_ONE REPEAT_ONE
} }
data class StreamVariants(
val identifiers: List<String>,
val languages: List<String>
)
interface NewPlayer { interface NewPlayer {
// preferences // preferences
val preferredVideoVariants: List<String> val preferredVideoVariants: List<String>
@ -63,6 +68,9 @@ interface NewPlayer {
var currentlyPlayingPlaylistItem: Int var currentlyPlayingPlaylistItem: Int
val currentChapters: StateFlow<List<Chapter>> val currentChapters: StateFlow<List<Chapter>>
val availableStreamVariants: StateFlow<StreamVariants?>
val currentlySelectedLanguage: StateFlow<String?>
val currentlySelectedStreamVariant: StateFlow<String?>
// callbacks // callbacks

View File

@ -35,8 +35,6 @@ import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.HttpDataSource import androidx.media3.datasource.HttpDataSource
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.MediaSource 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.MediaController
import androidx.media3.session.SessionToken import androidx.media3.session.SessionToken
import com.google.common.util.concurrent.MoreExecutors import com.google.common.util.concurrent.MoreExecutors
@ -53,8 +51,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.newpipe.newplayer.service.NewPlayerService import net.newpipe.newplayer.service.NewPlayerService
import net.newpipe.newplayer.utils.MediaSourceBuilder import net.newpipe.newplayer.utils.MediaSourceBuilder
import net.newpipe.newplayer.utils.StreamSelect import net.newpipe.newplayer.utils.StreamSelector
import kotlin.random.Random
private const val TAG = "NewPlayerImpl" private const val TAG = "NewPlayerImpl"
@ -73,6 +70,7 @@ class NewPlayerImpl(
private var playerScope = CoroutineScope(Dispatchers.Main + Job()) private var playerScope = CoroutineScope(Dispatchers.Main + Job())
private var uniqueIdToIdLookup = HashMap<Long, String>() private var uniqueIdToIdLookup = HashMap<Long, String>()
private var uniqueIdToStreamVariantSelection = HashMap<Long, StreamSelector.StreamSelection>()
// this is used to take care of the NewPlayerService // this is used to take care of the NewPlayerService
private var mediaController: MediaController? = null private var mediaController: MediaController? = null
@ -138,6 +136,14 @@ class NewPlayerImpl(
private val mutableCurrentChapter = MutableStateFlow<List<Chapter>>(emptyList()) private val mutableCurrentChapter = MutableStateFlow<List<Chapter>>(emptyList())
override val currentChapters: StateFlow<List<Chapter>> = mutableCurrentChapter.asStateFlow() override val currentChapters: StateFlow<List<Chapter>> = mutableCurrentChapter.asStateFlow()
private val mutableCurrentlySelectedLanguage = MutableStateFlow<String?>(null)
override val currentlySelectedLanguage = mutableCurrentlySelectedLanguage.asStateFlow()
private val mutableCurrentlySelectedStreamVariant = MutableStateFlow<String?>(null)
override val currentlySelectedStreamVariant =
mutableCurrentlySelectedStreamVariant.asStateFlow()
override var currentlyPlayingPlaylistItem: Int override var currentlyPlayingPlaylistItem: Int
get() = exoPlayer.value?.currentMediaItemIndex ?: -1 get() = exoPlayer.value?.currentMediaItemIndex ?: -1
set(value) { set(value) {
@ -147,6 +153,9 @@ class NewPlayerImpl(
exoPlayer.value?.seekTo(value, 0) exoPlayer.value?.seekTo(value, 0)
} }
private var mutableAvailableStreamVariants = MutableStateFlow<StreamVariants?>(null)
override val availableStreamVariants = mutableAvailableStreamVariants.asStateFlow()
private fun setupNewExoplayer() { private fun setupNewExoplayer() {
val newExoPlayer = ExoPlayer.Builder(app) val newExoPlayer = ExoPlayer.Builder(app)
.setAudioAttributes(AudioAttributes.DEFAULT, true) .setAudioAttributes(AudioAttributes.DEFAULT, true)
@ -184,6 +193,13 @@ class NewPlayerImpl(
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
super.onMediaItemTransition(mediaItem, reason) super.onMediaItemTransition(mediaItem, reason)
mutableCurrentlyPlaying.update { mediaItem } mutableCurrentlyPlaying.update { mediaItem }
if (mediaItem != null) {
val item = uniqueIdToIdLookup[mediaItem.mediaId.toLong()]!!
updateStreamVariants(item)
updateStreamSelection(mediaItem)
} else {
mutableAvailableStreamVariants.update { null }
}
} }
}) })
mutableExoPlayer.update { mutableExoPlayer.update {
@ -243,7 +259,7 @@ class NewPlayerImpl(
prepare() prepare()
} }
launchJobAndCollectError { launchJobAndCollectError {
val mediaSource = toMediaSource(item, playBackMode.value) val mediaSource = toMediaSource(item)
exoPlayer.value?.addMediaSource(mediaSource) exoPlayer.value?.addMediaSource(mediaSource)
} }
} }
@ -262,17 +278,50 @@ class NewPlayerImpl(
} }
} }
@OptIn(UnstableApi::class)
override fun playStream(item: String, playMode: PlayMode) { override fun playStream(item: String, playMode: PlayMode) {
launchJobAndCollectError { launchJobAndCollectError {
val mediaItem = toMediaSource(item, playMode) val mediaSource = toMediaSource(item)
internalPlayStream(mediaItem, playMode) 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) @OptIn(UnstableApi::class)
override fun selectChapter(index: Int) { override fun selectChapter(index: Int) {
val chapters = currentChapters.value val chapters = currentChapters.value
assert(index in 0..<chapters.size) { assert(index in chapters.indices) {
throw NewPlayerException("Chapter selection out of bound: selected chapter index: $index, available chapters: ${chapters.size}") throw NewPlayerException("Chapter selection out of bound: selected chapter index: $index, available chapters: ${chapters.size}")
} }
val chapter = chapters[index] val chapter = chapters[index]
@ -289,6 +338,7 @@ class NewPlayerImpl(
null null
} }
mediaController = null mediaController = null
mutableAvailableStreamVariants.update { null }
uniqueIdToIdLookup = HashMap() uniqueIdToIdLookup = HashMap()
} }
@ -310,18 +360,28 @@ class NewPlayerImpl(
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
private suspend private suspend
fun toMediaSource(item: String, playMode: PlayMode): MediaSource { fun toMediaSource(item: String): MediaSource {
val streamSelector = StreamSelector(
preferredVideoIdentifier = preferredVideoVariants,
preferredAudioIdentifier = preferredAudioVariants,
preferredLanguage = preferredStreamLanguage
)
val builder = MediaSourceBuilder( val builder = MediaSourceBuilder(
repository = repository, repository = repository,
uniqueIdToIdLookup = uniqueIdToIdLookup, uniqueIdToIdLookup = uniqueIdToIdLookup,
preferredLanguage = preferredStreamLanguage,
preferredAudioId = preferredAudioVariants,
preferredVideoId = preferredVideoVariants,
playMode = playMode,
mutableErrorFlow = mutableErrorFlow, mutableErrorFlow = mutableErrorFlow,
httpDataSourceFactory = httpDataSourceFactory
) )
return builder.buildMediaSource(item) val selection = streamSelector.selectStream(
item,
availableStreams = repository.getStreams(item)
)
val mediaSource = builder.buildMediaSource(selection)
uniqueIdToStreamVariantSelection[mediaSource.mediaItem.mediaId.toLong()] = selection
return mediaSource
} }
private fun launchJobAndCollectError(task: suspend () -> Unit) = private fun launchJobAndCollectError(task: suspend () -> Unit) =

View File

@ -74,7 +74,10 @@ data class VideoPlayerUIState(
val repeatMode: RepeatMode, val repeatMode: RepeatMode,
val playListDurationInS: Int, val playListDurationInS: Int,
val currentlyPlaying: MediaItem?, val currentlyPlaying: MediaItem?,
val currentPlaylistItemIndex: Int val currentPlaylistItemIndex: Int,
val availableStreamVariants: List<String>,
val availableLanguages: List<String>,
val availableSubtitles: List<String>
) { ) {
companion object { companion object {
val DEFAULT = VideoPlayerUIState( val DEFAULT = VideoPlayerUIState(
@ -99,10 +102,16 @@ data class VideoPlayerUIState(
repeatMode = RepeatMode.DO_NOT_REPEAT, repeatMode = RepeatMode.DO_NOT_REPEAT,
playListDurationInS = 0, playListDurationInS = 0,
currentlyPlaying = null, currentlyPlaying = null,
currentPlaylistItemIndex = 0 currentPlaylistItemIndex = 0,
availableLanguages = emptyList(),
availableSubtitles = emptyList(),
availableStreamVariants = emptyList()
) )
val DUMMY = DEFAULT.copy( val DUMMY = DEFAULT.copy(
availableLanguages = listOf("German", "English", "Spanish"),
availableSubtitles = listOf("German", "English", "Spanish"),
availableStreamVariants = listOf("460p", "720p", "1080p60"),
uiMode = UIModeState.EMBEDDED_VIDEO, uiMode = UIModeState.EMBEDDED_VIDEO,
playing = true, playing = true,
seekerPosition = 0.3f, seekerPosition = 0.3f,

View File

@ -42,6 +42,7 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.newpipe.newplayer.utils.VideoSize import net.newpipe.newplayer.utils.VideoSize
@ -224,6 +225,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
} }
} }
} }
viewModelScope.launch { viewModelScope.launch {
newPlayer.currentChapters.collect { chapters -> newPlayer.currentChapters.collect { chapters ->
mutableUiState.update { 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 { mutableUiState.update {
it.copy( it.copy(
playing = newPlayer.exoPlayer.value?.isPlaying ?: false, playing = newPlayer.exoPlayer.value?.isPlaying ?: false,

View File

@ -20,6 +20,7 @@
package net.newpipe.newplayer.ui.videoplayer package net.newpipe.newplayer.ui.videoplayer
import androidx.annotation.OptIn
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.icons.Icons 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.tooling.preview.Preview
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.media3.common.util.UnstableApi
import net.newpipe.newplayer.R import net.newpipe.newplayer.R
import net.newpipe.newplayer.model.VideoPlayerUIState import net.newpipe.newplayer.model.VideoPlayerUIState
import net.newpipe.newplayer.model.VideoPlayerViewModel import net.newpipe.newplayer.model.VideoPlayerViewModel
import net.newpipe.newplayer.model.VideoPlayerViewModelDummy import net.newpipe.newplayer.model.VideoPlayerViewModelDummy
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
@OptIn(UnstableApi::class)
@Composable @Composable
fun DropDownMenu(viewModel: VideoPlayerViewModel, uiState: VideoPlayerUIState) { fun DropDownMenu(viewModel: VideoPlayerViewModel, uiState: VideoPlayerUIState) {
var showMainMenu: Boolean by remember { mutableStateOf(false) } var showMainMenu: Boolean by remember { mutableStateOf(false) }

View File

@ -20,35 +20,22 @@ import kotlin.random.Random
class MediaSourceBuilder( class MediaSourceBuilder(
private val repository: MediaRepository, private val repository: MediaRepository,
private val uniqueIdToIdLookup: HashMap<Long, String>, private val uniqueIdToIdLookup: HashMap<Long, String>,
private val preferredLanguage: List<String>,
private val preferredAudioId: List<String>,
private val preferredVideoId: List<String>,
private val playMode: PlayMode,
private val mutableErrorFlow: MutableSharedFlow<Exception>, private val mutableErrorFlow: MutableSharedFlow<Exception>,
private val httpDataSourceFactory: HttpDataSource.Factory = DefaultHttpDataSource.Factory(), private val httpDataSourceFactory: HttpDataSource.Factory,
) { ) {
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
suspend fun buildMediaSource(item: String): MediaSource { suspend fun buildMediaSource(selectedStream: StreamSelector.StreamSelection): MediaSource {
val availableStreams = repository.getStreams(item)
val selectedStream = StreamSelect.selectStream(
item,
playMode,
availableStreams,
preferredAudioId,
preferredAudioId,
preferredLanguage
)
val mediaSource = when (selectedStream) { val mediaSource = when (selectedStream) {
is StreamSelect.SingleSelection -> { is StreamSelector.SingleSelection -> {
val mediaItem = toMediaItem(item, selectedStream.stream) val mediaItem = toMediaItem(selectedStream.item, selectedStream.stream)
val mediaItemWithMetadata = addMetadata(mediaItem, item) val mediaItemWithMetadata = addMetadata(mediaItem, selectedStream.item)
toMediaSource(mediaItemWithMetadata, selectedStream.stream) toMediaSource(mediaItemWithMetadata, selectedStream.stream)
} }
is StreamSelect.MultiSelection -> { is StreamSelector.MultiSelection -> {
val mediaItems = ArrayList(selectedStream.streams.map { toMediaItem(item, it) }) val mediaItems = ArrayList(selectedStream.streams.map { toMediaItem(selectedStream.item, it) })
mediaItems[0] = addMetadata(mediaItems[0], item) mediaItems[0] = addMetadata(mediaItems[0], selectedStream.item)
val mediaSources = mediaItems.zip(selectedStream.streams) val mediaSources = mediaItems.zip(selectedStream.streams)
.map { toMediaSource(it.first, it.second) } .map { toMediaSource(it.first, it.second) }
MergingMediaSource( MergingMediaSource(

View File

@ -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<Stream>
) : StreamSelection
private fun getBestLanguageFit(
availableStreams: List<Stream>, preferredLanguages: List<String>
): String? {
for (preferredLanguage in preferredLanguages) {
for (available in availableStreams) {
if (available.language == preferredLanguage) {
return preferredLanguage
}
}
}
return null
}
private fun filtersByLanguage(
availableStreams: List<Stream>, language: String
) = availableStreams.filter { it.language == language }
private fun getBestFittingVideoIdentifier(
availableStreams: List<Stream>, preferredVideoIdentifier: List<String>
): 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<Stream>) =
availableStreams.filter { it.streamType == StreamType.VIDEO }.ifEmpty { null }?.let {
it[it.size / 2]
}
private fun tryAndGetMedianAudioOnlyStream(availableStreams: List<Stream>) =
availableStreams.filter { it.streamType == StreamType.AUDIO }.ifEmpty { null }?.let {
it[it.size / 2]
}
private fun getFirstMatchingIdentifier(
availableStreams: List<Stream>, identifier: String
): Stream? {
for (variant in availableStreams) {
if (variant.identifier == identifier) return variant
}
return null
}
private fun getBestFittingAudio(
availableStreams: List<Stream>, preferredAudioIdentifier: List<String>
): 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<Stream>, preferredAudioIdentifier: List<String>
): List<Stream>? {
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<Stream>
) = 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<Stream>,
): HashMap<String, ArrayList<Stream>> {
val streamsBundledByIdentifier = HashMap<String, ArrayList<Stream>>()
for (stream in availableStreams) {
streamsBundledByIdentifier[stream.identifier] ?: run {
val array = ArrayList<Stream>()
streamsBundledByIdentifier[stream.identifier] = array
array
}.add(stream)
}
return streamsBundledByIdentifier
}
private fun getVideoOnlyWithMatchingIdentifier(
availableStreams: List<Stream>, 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>): Stream? {
for (variant in availableStreams) {
if (variant.streamType == StreamType.DYNAMIC) {
return variant
}
}
return null
}
private fun getNonDynamicVideos(availableStreams: List<Stream>) = availableStreams.filter {
it.streamType == StreamType.VIDEO || it.streamType == StreamType.AUDIO_AND_VIDEO
}
private fun getNonDynamicAudios(availableStreams: List<Stream>) =
availableStreams.filter { it.streamType == StreamType.AUDIO }
private fun hasVideoStreams(availableStreams: List<Stream>): 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<Stream>,
preferredVideoIdentifier: List<String>,
preferredAudioIdentifier: List<String>,
preferredLanguage: List<String>,
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}")
}
})
}
}
}

View File

@ -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<String>,
val preferredAudioIdentifier: List<String>,
val preferredLanguage: List<String>,
) {
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<Stream>
) : StreamSelection
enum class DemuxedStreamBundeling {
DO_NOT_BUNDLE, BUNDLE_STREAMS_WITH_SAME_ID, BUNDLE_AUDIOSTREAMS_WITH_SAME_ID
}
fun selectStream(
item: String,
availableStreams: List<Stream>,
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<Stream>, preferredLanguages: List<String>
): String? {
for (preferredLanguage in preferredLanguages) {
for (available in availableStreams) {
if (available.language == preferredLanguage) {
return preferredLanguage
}
}
}
return null
}
private fun filtersByLanguage(
availableStreams: List<Stream>, language: String
) = availableStreams.filter { it.language == language }
private fun getBestFittingVideoIdentifier(
availableStreams: List<Stream>, preferredVideoIdentifier: List<String>
): 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<Stream>) =
availableStreams.filter { it.streamType == StreamType.VIDEO }.ifEmpty { null }?.let {
it[it.size / 2]
}
private fun tryAndGetMedianAudioOnlyStream(availableStreams: List<Stream>) =
availableStreams.filter { it.streamType == StreamType.AUDIO }.ifEmpty { null }?.let {
it[it.size / 2]
}
private fun getFirstMatchingIdentifier(
availableStreams: List<Stream>, identifier: String
): Stream? {
for (variant in availableStreams) {
if (variant.identifier == identifier) return variant
}
return null
}
private fun getBestFittingAudio(
availableStreams: List<Stream>, preferredAudioIdentifier: List<String>
): 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<Stream>, preferredAudioIdentifier: List<String>
): List<Stream>? {
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<Stream>
) = 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<Stream>,
): HashMap<String, ArrayList<Stream>> {
val streamsBundledByIdentifier = HashMap<String, ArrayList<Stream>>()
for (stream in availableStreams) {
streamsBundledByIdentifier[stream.identifier] ?: run {
val array = ArrayList<Stream>()
streamsBundledByIdentifier[stream.identifier] = array
array
}.add(stream)
}
return streamsBundledByIdentifier
}
private fun getVideoOnlyWithMatchingIdentifier(
availableStreams: List<Stream>, 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>): Stream? {
for (variant in availableStreams) {
if (variant.streamType == StreamType.DYNAMIC) {
return variant
}
}
return null
}
private fun getNonDynamicVideos(availableStreams: List<Stream>) = availableStreams.filter {
it.streamType == StreamType.VIDEO || it.streamType == StreamType.AUDIO_AND_VIDEO
}
private fun getNonDynamicAudios(availableStreams: List<Stream>) =
availableStreams.filter { it.streamType == StreamType.AUDIO }
private fun hasVideoStreams(availableStreams: List<Stream>): Boolean {
for (variant in availableStreams) {
if (variant.streamType == StreamType.AUDIO_AND_VIDEO || variant.streamType == StreamType.VIDEO || variant.streamType == StreamType.DYNAMIC) return true
}
return false
}
}
}