add code for best possible stream selection (NOT UNIT TESTED/TESTED at all)
This commit is contained in:
parent
b50e63077b
commit
c37e44a56e
|
@ -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
|
||||||
|
|
|
@ -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?>
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in New Issue