mirror of
https://github.com/libre-tube/LibreTube.git
synced 2024-12-14 14:20:30 +05:30
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:
parent
d21daf341d
commit
9d25d32bff
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
Loading…
Reference in New Issue
Block a user