do some commit
This commit is contained in:
parent
20c9bd67e7
commit
cde9b1f9ba
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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) =
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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) }
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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}")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue