add code for best possible stream selection (NOT UNIT TESTED/TESTED at all)

This commit is contained in:
Christian Schabesberger 2024-09-12 15:15:04 +02:00
parent b50e63077b
commit c37e44a56e
5 changed files with 285 additions and 26 deletions

View File

@ -45,6 +45,11 @@ data class RepoMetaInfo(
val pullsDataFromNetwrok: Boolean val pullsDataFromNetwrok: Boolean
) )
data class Stream(
val streamUri: Uri,
val mimeType: String? = null
)
interface MediaRepository { interface MediaRepository {
fun getRepoInfo() : RepoMetaInfo fun getRepoInfo() : RepoMetaInfo
@ -52,7 +57,7 @@ interface MediaRepository {
suspend fun getMetaInfo(item: String): MediaMetadata suspend fun getMetaInfo(item: String): MediaMetadata
suspend fun getAvailableStreamVariants(item: String): List<StreamVariant> suspend fun getAvailableStreamVariants(item: String): List<StreamVariant>
suspend fun getStream(item: String, streamVariantSelector: StreamVariant): Uri suspend fun getStream(item: String, streamVariantSelector: StreamVariant): Stream
suspend fun getAvailableSubtitleVariants(item: String): List<String> suspend fun getAvailableSubtitleVariants(item: String): List<String>
suspend fun getSubtitle(item: String, variant: String): Uri suspend fun getSubtitle(item: String, variant: String): Uri

View File

@ -25,7 +25,6 @@ import androidx.media3.common.Player
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import java.util.stream.Stream
import kotlin.Exception import kotlin.Exception
enum class PlayMode { enum class PlayMode {
@ -45,7 +44,8 @@ enum class RepeatMode {
interface NewPlayer { interface NewPlayer {
// preferences // preferences
val preferredStreamVariants: List<String> val preferredVideoVariants: List<String>
val prefearedAudioVariants: List<String>
val preferredStreamLanguage: List<String> val preferredStreamLanguage: List<String>
val exoPlayer: StateFlow<Player?> val exoPlayer: StateFlow<Player?>

View File

@ -32,6 +32,7 @@ import androidx.media3.common.Player
import androidx.media3.common.Timeline import androidx.media3.common.Timeline
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultHttpDataSource import androidx.media3.datasource.DefaultHttpDataSource
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.ProgressiveMediaSource import androidx.media3.exoplayer.source.ProgressiveMediaSource
@ -50,6 +51,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.StreamSelect
import kotlin.random.Random import kotlin.random.Random
private const val TAG = "NewPlayerImpl" private const val TAG = "NewPlayerImpl"
@ -57,8 +59,10 @@ private const val TAG = "NewPlayerImpl"
class NewPlayerImpl( class NewPlayerImpl(
val app: Application, val app: Application,
private val repository: MediaRepository, private val repository: MediaRepository,
override val preferredStreamVariants: List<String> = emptyList(), override val preferredVideoVariants: List<String> = emptyList(),
override val preferredStreamLanguage: List<String> = emptyList() override val preferredStreamLanguage: List<String> = emptyList(),
override val prefearedAudioVariants: List<String> = emptyList(),
val httpDataSourceFactory: HttpDataSource.Factory = DefaultHttpDataSource.Factory(),
) : NewPlayer { ) : NewPlayer {
private val mutableExoPlayer = MutableStateFlow<ExoPlayer?>(null) private val mutableExoPlayer = MutableStateFlow<ExoPlayer?>(null)
@ -321,7 +325,11 @@ class NewPlayerImpl(
uniqueIdToIdLookup[uniqueId] = item uniqueIdToIdLookup[uniqueId] = item
val mediaItemBuilder = MediaItem.Builder() val mediaItemBuilder = MediaItem.Builder()
.setMediaId(uniqueId.toString()) .setMediaId(uniqueId.toString())
.setUri(dataStream) .setUri(dataStream.streamUri)
if(dataStream.mimeType != null) {
mediaItemBuilder.setMimeType(dataStream.mimeType)
}
try { try {
val metadata = repository.getMetaInfo(item) val metadata = repository.getMetaInfo(item)
@ -332,21 +340,17 @@ class NewPlayerImpl(
val mediaItem = mediaItemBuilder.build() val mediaItem = mediaItemBuilder.build()
return ProgressiveMediaSource.Factory(DefaultHttpDataSource.Factory()) return ProgressiveMediaSource.Factory(httpDataSourceFactory)
.createMediaSource(mediaItem) .createMediaSource(mediaItem)
} }
private suspend private suspend
fun toMediaSource(item: String, playMode: PlayMode): MediaSource { fun toMediaSource(item: String, playMode: PlayMode): MediaSource {
val availableStreams = repository.getAvailableStreamVariants(item) val availableStreams = repository.getAvailableStreamVariants(item)
var selectedStream = availableStreams[availableStreams.size / 2] //var selectedStream = availableStreams[availableStreams.size / 2]
for (preferredStream in preferredStreamVariants) {
for (availableStream in availableStreams) {
if (preferredStream == availableStream.streamVariantIdentifier) {
selectedStream = availableStream
}
}
}
return toMediaSource(item, selectedStream) return toMediaSource(item, selectedStream)
} }

View File

@ -0,0 +1,235 @@
package net.newpipe.newplayer.utils
import net.newpipe.newplayer.NewPlayerException
import net.newpipe.newplayer.PlayMode
import net.newpipe.newplayer.StreamType
import net.newpipe.newplayer.StreamVariant
object StreamSelect {
interface StreamSelection
data class SingleSelection(
val streamVariant: StreamVariant
) : StreamSelection
data class MultiSelection(
val videoStream: StreamVariant,
val audioStream: StreamVariant
) : StreamSelection
private fun getBestLanguageFit(
availableStreamVariants: List<StreamVariant>,
preferredLanguages: List<String>
): String? {
for (preferredLanguage in preferredLanguages) {
for (availableVariant in availableStreamVariants) {
if (availableVariant.language == preferredLanguage) {
return preferredLanguage
}
}
}
return null
}
private fun filterVariantsByLanguage(
availableStreamVariants: List<StreamVariant>,
language: String
) =
availableStreamVariants.filter { it.language == language }
private fun getBestFittingVideoIdentifier(
availableStreamVariants: List<StreamVariant>,
preferredVideoIdentifier: List<String>
): String? {
for (preferredStream in preferredVideoIdentifier) {
for (availableVariant in availableStreamVariants) {
if ((availableVariant.streamType == StreamType.AUDIO_AND_VIDEO ||
availableVariant.streamType == StreamType.VIDEO)
&& preferredStream == availableVariant.streamVariantIdentifier
) {
return preferredStream
}
}
}
return null
}
private fun getFirstVariantMatchingIdentifier(
availableStreamVariants: List<StreamVariant>,
identifier: String
): StreamVariant? {
for (variant in availableStreamVariants) {
if (variant.streamVariantIdentifier == identifier)
return variant
}
return null
}
private fun getBestFittingAudioVariant(
availableStreamVariants: List<StreamVariant>,
preferredAudioIdentifier: List<String>
): StreamVariant? {
for (preferredStream in preferredAudioIdentifier) {
for (availableStream in availableStreamVariants) {
if (availableStream.streamType == StreamType.AUDIO
&& preferredStream == availableStream.streamVariantIdentifier
) {
return availableStream
}
}
}
return null
}
private fun getVideoOnlyVariantWithMatchingIdentifier(
availableStreamVariants: List<StreamVariant>,
identifier: String
): StreamVariant? {
for (variant in availableStreamVariants) {
if (variant.streamType == StreamType.VIDEO
&& variant.streamVariantIdentifier == identifier
)
return variant
}
return null
}
private fun getDynamicStream(availableStreamVariants: List<StreamVariant>): StreamVariant? {
for (variant in availableStreamVariants) {
if (variant.streamType == StreamType.DYNAMIC) {
return variant
}
}
return null
}
private fun getNonDynamicVideoVariants(availableStreamVariants: List<StreamVariant>) =
availableStreamVariants.filter {
it.streamType == StreamType.VIDEO || it.streamType == StreamType.AUDIO_AND_VIDEO
}
private fun getNonDynamicAudioVariants(availableStreamVariants: List<StreamVariant>) =
availableStreamVariants.filter { it.streamType == StreamType.AUDIO }
fun selectStream(
item: String,
playMode: PlayMode,
availableStreamVariants: List<StreamVariant>,
preferredVideoIdentifier: List<String>,
preferredAudioIdentifier: List<String>,
preferredLanguage: List<String>
): StreamSelection {
val bestFittingLanguage = getBestLanguageFit(availableStreamVariants, preferredLanguage)
val availableVariantsInPreferredLanguage =
if (bestFittingLanguage != null) filterVariantsByLanguage(
availableStreamVariants,
bestFittingLanguage
)
else {
emptyList()
}
if (playMode == PlayMode.FULLSCREEN_VIDEO
|| playMode == PlayMode.EMBEDDED_VIDEO
|| playMode == PlayMode.PIP
) {
getDynamicStream(availableVariantsInPreferredLanguage)
?: getDynamicStream(
availableStreamVariants
)?.let {
return SingleSelection(it)
}
val bestIdentifier =
getBestFittingVideoIdentifier(
availableVariantsInPreferredLanguage,
preferredVideoIdentifier
)?.let {
val videoVariants =
getNonDynamicVideoVariants(availableVariantsInPreferredLanguage)
videoVariants[videoVariants.size / 2].streamVariantIdentifier
} ?: getBestFittingVideoIdentifier(
availableStreamVariants,
preferredVideoIdentifier
)
?: run {
val videoVariants = getNonDynamicVideoVariants(availableStreamVariants)
videoVariants[videoVariants.size / 2].streamVariantIdentifier
}
val videoOnlyStream =
getVideoOnlyVariantWithMatchingIdentifier(
availableVariantsInPreferredLanguage,
bestIdentifier
) ?: getVideoOnlyVariantWithMatchingIdentifier(
availableStreamVariants,
bestIdentifier
)
if (videoOnlyStream != null) {
getBestFittingAudioVariant(
availableVariantsInPreferredLanguage,
preferredAudioIdentifier
) ?: getBestFittingAudioVariant(availableStreamVariants, preferredAudioIdentifier)
?.let {
return MultiSelection(videoOnlyStream, it)
}
}
getFirstVariantMatchingIdentifier(availableVariantsInPreferredLanguage, bestIdentifier)
?: getFirstVariantMatchingIdentifier(availableStreamVariants, bestIdentifier)?.let {
return SingleSelection(it)
}
return SingleSelection(run {
val videoVariants =
getNonDynamicVideoVariants(availableVariantsInPreferredLanguage).let {
if (it.isNotEmpty()) {
it
} else {
getNonDynamicVideoVariants(availableStreamVariants)
}
}
if (videoVariants.isNotEmpty()) {
return@run videoVariants[videoVariants.size / 2]
} else {
throw NewPlayerException("No fitting video stream could be found for stream item item: ${item}")
}
})
} else {
getBestFittingAudioVariant(
availableVariantsInPreferredLanguage,
preferredAudioIdentifier
)
?: getBestFittingAudioVariant(
availableStreamVariants,
preferredAudioIdentifier
)?.let {
return SingleSelection(it)
}
return SingleSelection(run {
val audioVariants =
getNonDynamicAudioVariants(availableVariantsInPreferredLanguage).let{
if(it.isNotEmpty()) {
it
} else {
getNonDynamicAudioVariants(availableStreamVariants)
}
}
if (audioVariants.isNotEmpty()) {
return@run audioVariants[audioVariants.size / 2]
} else {
throw NewPlayerException("No fitting audio stream could be found for stream item item: ${item}")
}
})
}
}
}

View File

@ -9,6 +9,7 @@ import androidx.media3.common.util.UnstableApi
import net.newpipe.newplayer.Chapter import net.newpipe.newplayer.Chapter
import net.newpipe.newplayer.MediaRepository import net.newpipe.newplayer.MediaRepository
import net.newpipe.newplayer.RepoMetaInfo import net.newpipe.newplayer.RepoMetaInfo
import net.newpipe.newplayer.Stream
import net.newpipe.newplayer.StreamType import net.newpipe.newplayer.StreamType
import net.newpipe.newplayer.StreamVariant import net.newpipe.newplayer.StreamVariant
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -101,19 +102,33 @@ class TestMediaRepository(val context: Context) : MediaRepository {
override suspend fun getStream(item: String, streamVariantSelector: StreamVariant) = override suspend fun getStream(item: String, streamVariantSelector: StreamVariant) =
Uri.parse(
when (item) { when (item) {
"6502" -> context.getString(R.string.ccc_6502_video) "6502" -> Stream(
"portrait" -> context.getString(R.string.portrait_video_example) streamUri = Uri.parse(context.getString(R.string.ccc_6502_video)),
mimeType = null
)
"portrait" -> Stream(
streamUri = Uri.parse(context.getString(R.string.portrait_video_example)),
mimeType = null
)
"imu" -> when (streamVariantSelector.streamVariantIdentifier) { "imu" -> when (streamVariantSelector.streamVariantIdentifier) {
"1080p" -> context.getString(R.string.ccc_imu_1080_mp4) "1080p" -> Stream(
"576p" -> context.getString(R.string.ccc_imu_576_mp4) streamUri = Uri.parse(context.getString(R.string.ccc_imu_1080_mp4)),
mimeType = null
)
"576p" -> Stream(
streamUri = Uri.parse(context.getString(R.string.ccc_imu_576_mp4)),
mimeType = null
)
else -> throw Exception("Unknown stream selector for $item: $streamVariantSelector") else -> throw Exception("Unknown stream selector for $item: $streamVariantSelector")
} }
else -> throw Exception("Unknown stream: $item") else -> throw Exception("Unknown stream: $item")
} }
)
override suspend fun getSubtitle(item: String, variant: String) = override suspend fun getSubtitle(item: String, variant: String) =
Uri.parse( Uri.parse(