make play media utilize stream selection and MediaSourceBuilder

This commit is contained in:
Christian Schabesberger 2024-09-17 13:20:19 +02:00
parent 1b4e1d4f13
commit 20c9bd67e7
4 changed files with 190 additions and 183 deletions

View File

@ -78,7 +78,6 @@ interface NewPlayer {
fun removePlaylistItem(uniqueId: Long) fun removePlaylistItem(uniqueId: Long)
fun playStream(item: String, playMode: PlayMode) fun playStream(item: String, playMode: PlayMode)
fun selectChapter(index: Int) fun selectChapter(index: Int)
fun playStream(item: String, streamVariant: StreamVariant, playMode: PlayMode)
fun release() fun release()
fun getItemLinkOfMediaItem(mediaItem: MediaItem) : String fun getItemLinkOfMediaItem(mediaItem: MediaItem) : String
} }

View File

@ -52,6 +52,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update 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.StreamSelect import net.newpipe.newplayer.utils.StreamSelect
import kotlin.random.Random import kotlin.random.Random
@ -268,17 +269,6 @@ class NewPlayerImpl(
} }
} }
override fun playStream(
item: String,
streamVariant: StreamVariant,
playMode: PlayMode
) {
launchJobAndCollectError {
val stream = toMediaSource(item, streamVariant)
internalPlayStream(stream, playMode)
}
}
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
override fun selectChapter(index: Int) { override fun selectChapter(index: Int) {
val chapters = currentChapters.value val chapters = currentChapters.value
@ -317,64 +307,21 @@ class NewPlayerImpl(
this.exoPlayer.value?.play() this.exoPlayer.value?.play()
} }
@OptIn(UnstableApi::class)
private suspend
fun toMediaSource(item: String, streamVariant: StreamVariant): MediaSource {
val dataStream = repository.getStream(item, streamVariant)
val uniqueId = Random.nextLong()
uniqueIdToIdLookup[uniqueId] = item
val mediaItemBuilder = MediaItem.Builder()
.setMediaId(uniqueId.toString())
.setUri(dataStream.streamUri)
if (dataStream.mimeType != null) {
mediaItemBuilder.setMimeType(dataStream.mimeType)
}
try {
val metadata = repository.getMetaInfo(item)
mediaItemBuilder.setMediaMetadata(metadata)
} catch (e: Exception) {
mutableErrorFlow.emit(e)
}
val mediaItem = mediaItemBuilder.build()
return ProgressiveMediaSource.Factory(httpDataSourceFactory)
.createMediaSource(mediaItem)
}
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
private suspend private suspend
fun toMediaSource(item: String, playMode: PlayMode): MediaSource { fun toMediaSource(item: String, playMode: PlayMode): MediaSource {
val availableStreamVariants = repository.getAvailableStreamVariants(item) val builder = MediaSourceBuilder(
val selectedStreamVariant = StreamSelect.selectStream( repository = repository,
item, uniqueIdToIdLookup = uniqueIdToIdLookup,
playMode, preferredLanguage = preferredStreamLanguage,
availableStreamVariants, preferredAudioId = preferredAudioVariants,
preferredVideoVariants, preferredVideoId = preferredVideoVariants,
preferredAudioVariants, playMode = playMode,
preferredStreamLanguage mutableErrorFlow = mutableErrorFlow,
) )
return builder.buildMediaSource(item)
return when (selectedStreamVariant) {
is StreamSelect.SingleSelection -> toMediaSource(
item,
selectedStreamVariant.streamVariant
)
is StreamSelect.MultiSelection -> MergingMediaSource(
toMediaSource(
item,
selectedStreamVariant.videoStream
), toMediaSource(item, selectedStreamVariant.audioStream)
)
else -> throw NewPlayerException("Unknown Stream selection type: Serious Programming error. :${selectedStreamVariant.javaClass}")
}
} }
private fun launchJobAndCollectError(task: suspend () -> Unit) = private fun launchJobAndCollectError(task: suspend () -> Unit) =

View File

@ -3,11 +3,16 @@ package net.newpipe.newplayer.utils
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.HttpDataSource import androidx.media3.datasource.HttpDataSource
import androidx.media3.exoplayer.dash.DashMediaSource import androidx.media3.exoplayer.dash.DashMediaSource
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.MergingMediaSource
import androidx.media3.exoplayer.source.ProgressiveMediaSource import androidx.media3.exoplayer.source.ProgressiveMediaSource
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import net.newpipe.newplayer.MediaRepository import net.newpipe.newplayer.MediaRepository
import net.newpipe.newplayer.NewPlayerException
import net.newpipe.newplayer.PlayMode
import net.newpipe.newplayer.StreamType import net.newpipe.newplayer.StreamType
import net.newpipe.newplayer.Stream import net.newpipe.newplayer.Stream
import kotlin.random.Random import kotlin.random.Random
@ -15,12 +20,47 @@ 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 private val httpDataSourceFactory: HttpDataSource.Factory = DefaultHttpDataSource.Factory(),
) { ) {
suspend fun buildMediaSource(item: String) { @OptIn(UnstableApi::class)
suspend fun buildMediaSource(item: String): MediaSource {
val availableStreams = repository.getStreams(item) val availableStreams = repository.getStreams(item)
val selectedStream = StreamSelect.selectStream(
item,
playMode,
availableStreams,
preferredAudioId,
preferredAudioId,
preferredLanguage
)
val mediaSource = when (selectedStream) {
is StreamSelect.SingleSelection -> {
val mediaItem = toMediaItem(item, selectedStream.stream)
val mediaItemWithMetadata = addMetadata(mediaItem, item)
toMediaSource(mediaItemWithMetadata, selectedStream.stream)
}
is StreamSelect.MultiSelection -> {
val mediaItems = ArrayList(selectedStream.streams.map { toMediaItem(item, it) })
mediaItems[0] = addMetadata(mediaItems[0], item)
val mediaSources = mediaItems.zip(selectedStream.streams)
.map { toMediaSource(it.first, it.second) }
MergingMediaSource(
true, true,
*mediaSources.toTypedArray()
)
}
else -> throw NewPlayerException("Unknown stream selection class: ${selectedStream.javaClass}")
}
return mediaSource
} }
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
@ -41,7 +81,7 @@ class MediaSourceBuilder(
} }
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
private fun toMediaSource(mediaItem: MediaItem, stream: Stream) = private fun toMediaSource(mediaItem: MediaItem, stream: Stream): MediaSource =
if (stream.streamType == StreamType.DYNAMIC) if (stream.streamType == StreamType.DYNAMIC)
DashMediaSource.Factory(httpDataSourceFactory) DashMediaSource.Factory(httpDataSourceFactory)
.createMediaSource(mediaItem) .createMediaSource(mediaItem)

View File

@ -15,14 +15,12 @@ object StreamSelect {
) : StreamSelection ) : StreamSelection
data class MultiSelection( data class MultiSelection(
val videoStream: Stream, val streams: List<Stream>
val audioStream: Stream
) : StreamSelection ) : StreamSelection
private fun getBestLanguageFit( private fun getBestLanguageFit(
availableStreams: List<Stream>, availableStreams: List<Stream>, preferredLanguages: List<String>
preferredLanguages: List<String>
): String? { ): String? {
for (preferredLanguage in preferredLanguages) { for (preferredLanguage in preferredLanguages) {
for (available in availableStreams) { for (available in availableStreams) {
@ -35,21 +33,15 @@ object StreamSelect {
} }
private fun filtersByLanguage( private fun filtersByLanguage(
availableStreams: List<Stream>, availableStreams: List<Stream>, language: String
language: String ) = availableStreams.filter { it.language == language }
) =
availableStreams.filter { it.language == language }
private fun getBestFittingVideoIdentifier( private fun getBestFittingVideoIdentifier(
availableStreams: List<Stream>, availableStreams: List<Stream>, preferredVideoIdentifier: List<String>
preferredVideoIdentifier: List<String>
): String? { ): String? {
for (preferredStream in preferredVideoIdentifier) { for (preferredStream in preferredVideoIdentifier) {
for (available in availableStreams) { for (available in availableStreams) {
if ((available.streamType == StreamType.AUDIO_AND_VIDEO || if ((available.streamType == StreamType.AUDIO_AND_VIDEO || available.streamType == StreamType.VIDEO) && preferredStream == available.identifier) {
available.streamType == StreamType.VIDEO)
&& preferredStream == available.identifier
) {
return preferredStream return preferredStream
} }
} }
@ -57,26 +49,31 @@ object StreamSelect {
return null 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( private fun getFirstMatchingIdentifier(
availableStreams: List<Stream>, availableStreams: List<Stream>, identifier: String
identifier: String
): Stream? { ): Stream? {
for (variant in availableStreams) { for (variant in availableStreams) {
if (variant.identifier == identifier) if (variant.identifier == identifier) return variant
return variant
} }
return null return null
} }
private fun getBestFittingAudio( private fun getBestFittingAudio(
availableStreams: List<Stream>, availableStreams: List<Stream>, preferredAudioIdentifier: List<String>
preferredAudioIdentifier: List<String>
): Stream? { ): Stream? {
for (preferredStream in preferredAudioIdentifier) { for (preferredStream in preferredAudioIdentifier) {
for (availableStream in availableStreams) { for (availableStream in availableStreams) {
if (availableStream.streamType == StreamType.AUDIO if (availableStream.streamType == StreamType.AUDIO && preferredStream == availableStream.identifier) {
&& preferredStream == availableStream.identifier
) {
return availableStream return availableStream
} }
} }
@ -84,15 +81,48 @@ object StreamSelect {
return null return null
} }
private fun getVideoOnlyWithMatchingIdentifier( 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>, availableStreams: List<Stream>,
identifier: String ): 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? { ): Stream? {
for (variant in availableStreams) { for (variant in availableStreams) {
if (variant.streamType == StreamType.VIDEO if (variant.streamType == StreamType.VIDEO && variant.identifier == identifier) return variant
&& variant.identifier == identifier
)
return variant
} }
return null return null
} }
@ -106,8 +136,7 @@ object StreamSelect {
return null return null
} }
private fun getNonDynamicVideos(availableStreams: List<Stream>) = private fun getNonDynamicVideos(availableStreams: List<Stream>) = availableStreams.filter {
availableStreams.filter {
it.streamType == StreamType.VIDEO || it.streamType == StreamType.AUDIO_AND_VIDEO it.streamType == StreamType.VIDEO || it.streamType == StreamType.AUDIO_AND_VIDEO
} }
@ -116,28 +145,30 @@ object StreamSelect {
private fun hasVideoStreams(availableStreams: List<Stream>): Boolean { private fun hasVideoStreams(availableStreams: List<Stream>): Boolean {
for (variant in availableStreams) { for (variant in availableStreams) {
if (variant.streamType == StreamType.AUDIO_AND_VIDEO || variant.streamType == StreamType.VIDEO || variant.streamType == StreamType.DYNAMIC) if (variant.streamType == StreamType.AUDIO_AND_VIDEO || variant.streamType == StreamType.VIDEO || variant.streamType == StreamType.DYNAMIC) return true
return true
} }
return false return false
} }
enum class DemuxedStreamBundeling {
DO_NOT_BUNDLE, BUNDLE_STREAMS_WITH_SAME_ID, BUNDLE_AUDIOSTREAMS_WITH_SAME_ID
}
fun selectStream( fun selectStream(
item: String, item: String,
playMode: PlayMode, playMode: PlayMode,
availableStreams: List<Stream>, availableStreams: List<Stream>,
preferredVideoIdentifier: List<String>, preferredVideoIdentifier: List<String>,
preferredAudioIdentifier: List<String>, preferredAudioIdentifier: List<String>,
preferredLanguage: List<String> preferredLanguage: List<String>,
demuxedStreamBundeling: DemuxedStreamBundeling = DemuxedStreamBundeling.DO_NOT_BUNDLE
): StreamSelection { ): StreamSelection {
// filter for best fitting language stream variants // filter for best fitting language stream variants
val bestFittingLanguage = getBestLanguageFit(availableStreams, preferredLanguage) val bestFittingLanguage = getBestLanguageFit(availableStreams, preferredLanguage)
val availablesInPreferredLanguage = val availablesInPreferredLanguage = if (bestFittingLanguage != null) filtersByLanguage(
if (bestFittingLanguage != null) filtersByLanguage( availableStreams, bestFittingLanguage
availableStreams,
bestFittingLanguage
) )
else { else {
emptyList() emptyList()
@ -148,60 +179,55 @@ object StreamSelect {
if (hasVideoStreams(availableStreams)) { if (hasVideoStreams(availableStreams)) {
// first: try and get a dynamic stream variant // first: try and get a dynamic stream variant
getDynamicStream(availablesInPreferredLanguage) getDynamicStream(availablesInPreferredLanguage) ?: getDynamicStream(
?: getDynamicStream(
availableStreams availableStreams
)?.let { )?.let {
return SingleSelection(it) return SingleSelection(it)
} }
// second: try and get seperate audio and video stream variants // second: try and get separate audio and video stream variants
val bestVideoIdentifier = val bestVideoIdentifier = getBestFittingVideoIdentifier(
getBestFittingVideoIdentifier( availablesInPreferredLanguage, preferredVideoIdentifier
availablesInPreferredLanguage, ) ?: tryAndGetMedianVideoOnlyStream(availablesInPreferredLanguage)?.identifier
preferredVideoIdentifier ?: getBestFittingVideoIdentifier(
)?.let { availableStreams, preferredVideoIdentifier
val videos = ) ?: tryAndGetMedianVideoOnlyStream(availableStreams)?.identifier ?: ""
getNonDynamicVideos(availablesInPreferredLanguage)
videos[videos.size / 2].identifier
} ?: getBestFittingVideoIdentifier(
availableStreams,
preferredVideoIdentifier
)
?: run {
val videos = getNonDynamicVideos(availableStreams)
videos[videos.size / 2].identifier
}
val videoOnlyStream = if (demuxedStreamBundeling == DemuxedStreamBundeling.BUNDLE_STREAMS_WITH_SAME_ID) {
getVideoOnlyWithMatchingIdentifier( return MultiSelection(availableStreams.filter { it.streamType == StreamType.VIDEO || it.streamType == StreamType.AUDIO })
availablesInPreferredLanguage, } else {
bestVideoIdentifier
val videoOnlyStream = getVideoOnlyWithMatchingIdentifier(
availablesInPreferredLanguage, bestVideoIdentifier
) ?: getVideoOnlyWithMatchingIdentifier( ) ?: getVideoOnlyWithMatchingIdentifier(
availableStreams, availableStreams, bestVideoIdentifier
bestVideoIdentifier
) )
if (videoOnlyStream != null) { if (videoOnlyStream != null) {
getBestFittingAudio( if (demuxedStreamBundeling == DemuxedStreamBundeling.BUNDLE_AUDIOSTREAMS_WITH_SAME_ID) {
availablesInPreferredLanguage, val bestFittingAudioStreams = getBestFittingAudioStreams(availableStreams, preferredAudioIdentifier)
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) ) ?: getBestFittingAudio(availableStreams, preferredAudioIdentifier)
?.let { ?: availableStreams.filter { it.streamType == StreamType.AUDIO }[0]
return MultiSelection(videoOnlyStream, it)
} 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 // fourth: try to get a video and audio stream variant with the best fitting identifier
getFirstMatchingIdentifier( getFirstMatchingIdentifier(
availablesInPreferredLanguage, availablesInPreferredLanguage, bestVideoIdentifier
bestVideoIdentifier ) ?: getFirstMatchingIdentifier(
) availableStreams, bestVideoIdentifier
?: getFirstMatchingIdentifier(
availableStreams,
bestVideoIdentifier
)?.let { )?.let {
return SingleSelection(it) return SingleSelection(it)
} }
@ -209,8 +235,7 @@ object StreamSelect {
// fifth: try and get the median video and audio stream variant // fifth: try and get the median video and audio stream variant
return SingleSelection(run { return SingleSelection(run {
val videos = val videos = getNonDynamicVideos(availablesInPreferredLanguage).ifEmpty {
getNonDynamicVideos(availablesInPreferredLanguage).ifEmpty {
getNonDynamicVideos(availableStreams) getNonDynamicVideos(availableStreams)
} }
@ -226,12 +251,9 @@ object StreamSelect {
// first: try to get an audio stream variant with the best fitting identifier // first: try to get an audio stream variant with the best fitting identifier
getBestFittingAudio( getBestFittingAudio(
availablesInPreferredLanguage, availablesInPreferredLanguage, preferredAudioIdentifier
preferredAudioIdentifier ) ?: getBestFittingAudio(
) availableStreams, preferredAudioIdentifier
?: getBestFittingAudio(
availableStreams,
preferredAudioIdentifier
)?.let { )?.let {
return SingleSelection(it) return SingleSelection(it)
} }
@ -239,8 +261,7 @@ object StreamSelect {
// second: try and get the median audio stream variant // second: try and get the median audio stream variant
return SingleSelection(run { return SingleSelection(run {
val audios = val audios = getNonDynamicAudios(availablesInPreferredLanguage).let {
getNonDynamicAudios(availablesInPreferredLanguage).let {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
it it
} else { } else {