Rely on ExoPlayer audio tracks instead of Piped streams for selection

Co-authored-by: AudricV <74829229+AudricV@users.noreply.github.com>
This commit is contained in:
Bnyro 2023-07-16 17:31:17 +02:00
parent d21daf341d
commit 9d25d32bff
2 changed files with 177 additions and 56 deletions

View File

@ -23,6 +23,7 @@ import androidx.core.view.children
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.common.PlaybackParameters
import androidx.media3.common.Tracks
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.LoadControl
@ -39,6 +40,7 @@ import com.github.libretube.enums.SbSkipOptions
import com.github.libretube.obj.PreviewFrame
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.runBlocking
import java.util.Locale
import kotlin.math.absoluteValue
import kotlin.math.roundToInt
@ -579,4 +581,140 @@ object PlayerHelper {
}
return null
}
/**
* Get the track type string resource corresponding to ExoPlayer role flags used for audio
* track types.
*
* If the role flags doesn't have any role flags used for audio track types, the string
* resource `unknown_audio_track_type` is returned.
*
* @param context a context to get the string resources used to build the audio track type
* @param roleFlags the ExoPlayer role flags from which the audio track type will be returned
* @return the track type string resource corresponding to an ExoPlayer role flag or the
* `unknown_audio_track_type` one if no role flags corresponding to the ones used for audio
* track types is set
*/
private fun getDisplayAudioTrackTypeFromFormat(
context: Context,
@C.RoleFlags roleFlags: Int
): String {
// These role flags should not be set together, so the first role only take into account
// flag which matches
return when {
// If the flag ROLE_FLAG_DESCRIBES_VIDEO is set, return the descriptive_audio_track
// string resource
roleFlags and C.ROLE_FLAG_DESCRIBES_VIDEO == C.ROLE_FLAG_DESCRIBES_VIDEO ->
context.getString(R.string.descriptive_audio_track)
// If the flag ROLE_FLAG_DESCRIBES_VIDEO is set, return the dubbed_audio_track
// string resource
roleFlags and C.ROLE_FLAG_DUB == C.ROLE_FLAG_DUB ->
context.getString(R.string.dubbed_audio_track)
// If the flag ROLE_FLAG_DESCRIBES_VIDEO is set, return the original_or_main_audio_track
// string resource
roleFlags and C.ROLE_FLAG_MAIN == C.ROLE_FLAG_MAIN ->
context.getString(R.string.original_or_main_audio_track)
// Return the unknown_audio_track_type string resource for any other value
else -> context.getString(R.string.unknown_audio_track_type)
}
}
/**
* Get an audio track name from an audio format, using its language tag and its role flags.
*
* If the given language is `null`, the string resource `unknown_audio_language` is used
* instead and when the given role flags have no track type value used by the app, the string
* resource `unknown_audio_track_type` is used instead.
*
* @param context a context to get the string resources used to build the
* audio track name
* @param audioLanguageAndRoleFlags a pair of an audio format language tag and role flags from
* which the audio track name will be built
* @return an audio track name of an audio format language and role flags, localized according
* to the language preferences of the user
*/
fun getAudioTrackNameFromFormat(
context: Context,
audioLanguageAndRoleFlags: Pair<String?, @C.RoleFlags Int>
): String {
val audioLanguage = audioLanguageAndRoleFlags.first
return context.getString(R.string.audio_track_format)
.format(
if (audioLanguage == null) context.getString(R.string.unknown_audio_language)
else Locale.forLanguageTag(audioLanguage)
.getDisplayLanguage(
LocaleHelper.getAppLocale()
)
.ifEmpty { context.getString(R.string.unknown_audio_language) },
getDisplayAudioTrackTypeFromFormat(context, audioLanguageAndRoleFlags.second)
)
}
/**
* Get audio languages with their role flags of supported formats from ExoPlayer track groups
* and only the selected ones if requested.
*
* Duplicate audio languages with their role flags are removed.
*
* @param groups the list of [Group]s of the current tracks played by the player
* @param keepOnlySelectedTracks whether to get only the selected audio languages with their
* role flags among the supported ones
* @return a list of distinct audio languages with their role flags from the supported formats
* of the given track groups and only the selected ones if requested
*/
fun getAudioLanguagesAndRoleFlagsFromTrackGroups(
groups: List<Tracks.Group>,
keepOnlySelectedTracks: Boolean
): List<Pair<String?, @C.RoleFlags Int>> {
// Filter unsupported tracks and keep only selected tracks if requested
// Use a lambda expression to avoid checking on each audio format if we keep only selected
// tracks or not
val trackFilter = if (keepOnlySelectedTracks)
{ group: Tracks.Group, trackIndex: Int ->
group.isTrackSupported(trackIndex) && group.isTrackSelected(
trackIndex
)
} else { group: Tracks.Group, trackIndex: Int -> group.isTrackSupported(trackIndex) }
return groups.filter {
it.type == C.TRACK_TYPE_AUDIO
}.flatMap { group ->
(0 until group.length).filter {
trackFilter(group, it)
}.map { group.getTrackFormat(it) }
}.map { format ->
format.language to format.roleFlags
}.distinct()
}
/**
* Check whether the given flag is set in the given bitfield.
*
* @param bitField a bitfield
* @param flag a flag to check its presence in the given bitfield
* @return whether the given flag is set in the given bitfield
*/
private fun isFlagSet(bitField: Int, flag: Int): Boolean {
return bitField and flag == flag;
}
/**
* Check whether the given ExoPlayer role flags contain at least one flag used for audio
* track types.
*
* ExoPlayer role flags currently used for audio track types are [C.ROLE_FLAG_DESCRIBES_VIDEO],
* [C.ROLE_FLAG_DUB], [C.ROLE_FLAG_MAIN] and [C.ROLE_FLAG_ALTERNATE].
*
* @param roleFlags the ExoPlayer role flags to check, an int representing a bitfield
* @return whether the provided ExoPlayer flags contain a flag used for audio track types
*/
fun haveAudioTrackRoleFlagSet(@C.RoleFlags roleFlags: Int): Boolean {
return isFlagSet(roleFlags, C.ROLE_FLAG_DESCRIBES_VIDEO)
|| isFlagSet(roleFlags, C.ROLE_FLAG_DUB)
|| isFlagSet(roleFlags, C.ROLE_FLAG_MAIN)
|| isFlagSet(roleFlags, C.ROLE_FLAG_ALTERNATE)
}
}

View File

@ -56,7 +56,6 @@ import com.github.libretube.api.JsonHelper
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.ChapterSegment
import com.github.libretube.api.obj.Message
import com.github.libretube.api.obj.PipedStream
import com.github.libretube.api.obj.Segment
import com.github.libretube.api.obj.StreamItem
import com.github.libretube.api.obj.Streams
@ -115,15 +114,15 @@ import com.github.libretube.util.PlayingQueue
import com.github.libretube.util.TextUtils
import com.github.libretube.util.TextUtils.toTimeInSeconds
import com.github.libretube.util.YoutubeHlsPlaylistParser
import java.io.IOException
import java.util.*
import java.util.concurrent.Executors
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString
import retrofit2.HttpException
import java.io.IOException
import java.util.*
import java.util.concurrent.Executors
import kotlin.math.abs
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
@ -1430,63 +1429,47 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
}
.show(childFragmentManager)
}
private fun getDisplayTrackType(trackType: String?): String {
if (trackType == null) {
return getString(R.string.unknown_audio_track_type)
}
return when (trackType.lowercase()) {
"descriptive" -> getString(R.string.descriptive_audio_track)
"dubbed" -> getString(R.string.dubbed_audio_track)
"original" -> getString(R.string.original_or_main_audio_track)
else -> getString(R.string.unknown_audio_track_type)
}
}
private fun getKeyByAudioStreamGroup(audioStream: PipedStream): String {
val nullAudioLocale = audioStream.audioTrackLocale == null
if (nullAudioLocale && audioStream.audioTrackType == null) {
// A track without a locale set and a track type should be a default track
return getString(R.string.default_audio_track)
}
return getString(R.string.audio_track_format).format(
if (nullAudioLocale) getString(R.string.unknown_audio_language)
else Locale.forLanguageTag(audioStream.audioTrackLocale!!)
.getDisplayLanguage(LocaleHelper.getAppLocale())
.ifEmpty { getString(R.string.unknown_audio_language) },
getDisplayTrackType(audioStream.audioTrackType))
}
private fun getAudioStreamGroups(audioStreams: List<PipedStream>?): Map<String, List<PipedStream>> {
return audioStreams.orEmpty()
.groupBy { getKeyByAudioStreamGroup(it) }
}
override fun onAudioStreamClicked() {
val audioGroups = getAudioStreamGroups(streams.audioStreams)
val audioLanguages = audioGroups.map { it.key }
val context = requireContext()
val audioLanguagesAndRoleFlags = PlayerHelper.getAudioLanguagesAndRoleFlagsFromTrackGroups(
exoPlayer.currentTracks.groups, false
)
val audioLanguages = audioLanguagesAndRoleFlags.map {
PlayerHelper.getAudioTrackNameFromFormat(context, it)
}
val baseBottomSheet = BaseBottomSheet()
BaseBottomSheet()
.setSimpleItems(audioLanguages) { index ->
val audioStreams = audioGroups.values.elementAt(index)
val firstAudioStream = audioStreams.firstOrNull()
if (audioLanguagesAndRoleFlags.isEmpty()) {
baseBottomSheet.setSimpleItems(
listOf(context.getString(R.string.unknown_or_no_audio)),
null
)
} else if (audioLanguagesAndRoleFlags.size == 1
&& audioLanguagesAndRoleFlags[0].first == null
&& !PlayerHelper.haveAudioTrackRoleFlagSet(
audioLanguagesAndRoleFlags[0].second
)
) {
// Regardless of audio format or quality, if there is only one audio stream which has
// no language and no role flags, it should mean that there is only a single audio
// track which has no language or track type set in the video played
// Consider it as the default audio track (or unknown)
baseBottomSheet.setSimpleItems(
listOf(context.getString(R.string.default_or_unknown_audio_track)),
null
)
} else {
baseBottomSheet.setSimpleItems(audioLanguages) { index ->
val selectedAudioFormat = audioLanguagesAndRoleFlags[index]
trackSelector.updateParameters {
setPreferredAudioLanguage(firstAudioStream?.audioTrackLocale)
setPreferredAudioRoleFlags(
if (firstAudioStream?.audioTrackType == null) {
C.ROLE_FLAG_MAIN
} else when (firstAudioStream.audioTrackType.lowercase()) {
"descriptive" -> C.ROLE_FLAG_DESCRIBES_VIDEO
"dubbed" -> C.ROLE_FLAG_DUB
"original" -> C.ROLE_FLAG_MAIN
else -> C.ROLE_FLAG_ALTERNATE
}
)
setPreferredAudioLanguage(selectedAudioFormat.first)
setPreferredAudioRoleFlags(selectedAudioFormat.second)
}
}
.show(childFragmentManager)
}
baseBottomSheet.show(childFragmentManager)
}
override fun onStatsClicked() {