From 2dc4c15dd8e18a2d47e67420f4de19d83ecb21ba Mon Sep 17 00:00:00 2001 From: Bnyro Date: Sun, 16 Jul 2023 17:11:27 +0200 Subject: [PATCH 1/7] Add support for extracting audio track types Co-authored-By: AudricV <74829229+AudricV@users.noreply.github.com> --- .../github/libretube/api/obj/PipedStream.kt | 4 +- .../github/libretube/helpers/DashHelper.kt | 53 +++++++++++++++---- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/github/libretube/api/obj/PipedStream.kt b/app/src/main/java/com/github/libretube/api/obj/PipedStream.kt index 3a67084d2..22ba3d385 100644 --- a/app/src/main/java/com/github/libretube/api/obj/PipedStream.kt +++ b/app/src/main/java/com/github/libretube/api/obj/PipedStream.kt @@ -24,7 +24,9 @@ data class PipedStream( val fps: Int? = null, val audioTrackName: String? = null, val audioTrackId: String? = null, - val contentLength: Long = -1 + val contentLength: Long = -1, + val audioTrackType: String? = null, + val audioTrackLocale: String? = null ) { private fun getQualityString(fileName: String): String { return "${fileName}_${quality?.replace(" ", "_")}_$format." + diff --git a/app/src/main/java/com/github/libretube/helpers/DashHelper.kt b/app/src/main/java/com/github/libretube/helpers/DashHelper.kt index 2d8853755..de382facf 100644 --- a/app/src/main/java/com/github/libretube/helpers/DashHelper.kt +++ b/app/src/main/java/com/github/libretube/helpers/DashHelper.kt @@ -19,8 +19,10 @@ object DashHelper { private data class AdapSetInfo( val mimeType: String, + val formats: MutableList = mutableListOf(), val audioTrackId: String? = null, - val formats: MutableList = mutableListOf() + val audioTrackType: String? = null, + val audioLocale: String? = null ) fun createManifest( @@ -75,7 +77,6 @@ object DashHelper { adapSetInfos.add( AdapSetInfo( stream.mimeType!!, - null, mutableListOf(stream) ) ) @@ -94,8 +95,10 @@ object DashHelper { adapSetInfos.add( AdapSetInfo( stream.mimeType!!, + mutableListOf(stream), stream.audioTrackId, - mutableListOf(stream) + stream.audioTrackType, + stream.audioTrackLocale ) ) } @@ -105,10 +108,22 @@ object DashHelper { adapSetElement.setAttribute("mimeType", adapSet.mimeType) adapSetElement.setAttribute("startWithSAP", "1") adapSetElement.setAttribute("subsegmentAlignment", "true") + if (adapSet.audioTrackId != null) { adapSetElement.setAttribute("lang", adapSet.audioTrackId.substring(0, 2)) + } else if (adapSet.audioLocale != null) { + adapSetElement.setAttribute("lang", adapSet.audioLocale) } + val roleElement = doc.createElement("Role") + roleElement.setAttribute("schemeIdUri", "urn:mpeg:dash:role:2011") + roleElement.setAttribute( + "value", + getRoleValueFromAudioTrackType(adapSet.audioTrackType) + ) + + adapSetElement.appendChild(roleElement) + val isVideo = adapSet.mimeType.contains("video") if (isVideo) { @@ -162,18 +177,38 @@ object DashHelper { val baseUrl = doc.createElement("BaseURL") baseUrl.appendChild(doc.createTextNode(ProxyHelper.unwrapUrl(stream.url!!, rewriteUrls))) - val segmentBase = doc.createElement("SegmentBase") + representation.appendChild(audioChannelConfiguration) + representation.appendChild(baseUrl) + representation.appendChild(createSegmentBaseElement(doc, stream)) + + return representation + } + + private fun createSegmentBaseElement( + document: Document, + stream: PipedStream + ): Element { + val segmentBase = document.createElement("SegmentBase") segmentBase.setAttribute("indexRange", "${stream.indexStart}-${stream.indexEnd}") - val initialization = doc.createElement("Initialization") + val initialization = document.createElement("Initialization") initialization.setAttribute("range", "${stream.initStart}-${stream.initEnd}") segmentBase.appendChild(initialization) - representation.appendChild(audioChannelConfiguration) - representation.appendChild(baseUrl) - representation.appendChild(segmentBase) + return segmentBase + } - return representation + private fun getRoleValueFromAudioTrackType(audioTrackType: String?): String { + if (audioTrackType == null) { + return "main" + } + + return when (audioTrackType.lowercase()) { + "descriptive" -> "description" + "dubbed" -> "dub" + "original" -> "main" + else -> "alternate" + } } private fun createVideoRepresentation( From c052075380f49bd11ce94e3eb8d22eb252932d60 Mon Sep 17 00:00:00 2001 From: Bnyro Date: Sun, 16 Jul 2023 17:18:28 +0200 Subject: [PATCH 2/7] Support track types for HLS streams Co-authored-by: AudricV <74829229+AudricV@users.noreply.github.com> --- .../libretube/ui/fragments/PlayerFragment.kt | 31 ++- .../util/YouTubeHlsPlaylistParser.kt | 258 ++++++++++++++++++ 2 files changed, 283 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/github/libretube/util/YouTubeHlsPlaylistParser.kt diff --git a/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt index 661f82fb2..78637cc55 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt @@ -46,6 +46,7 @@ import androidx.media3.common.Player import androidx.media3.datasource.DefaultDataSource import androidx.media3.datasource.cronet.CronetDataSource import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.hls.HlsMediaSource import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import androidx.recyclerview.widget.LinearLayoutManager @@ -113,6 +114,7 @@ import com.github.libretube.util.NowPlayingNotification 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 @@ -164,6 +166,11 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { private lateinit var trackSelector: DefaultTrackSelector private var captionLanguage: String? = PlayerHelper.defaultSubtitleCode + private val cronetDataSourceFactory = CronetDataSource.Factory( + CronetHelper.cronetEngine, + Executors.newCachedThreadPool() + ) + /** * Chapters and comments */ @@ -1189,13 +1196,15 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { } } + private fun createMediaItem(uri: Uri, mimeType: String) = MediaItem.Builder() + .setUri(uri) + .setMimeType(mimeType) + .setSubtitleConfigurations(subtitles) + .setMetadata(streams) + .build() + private fun setMediaSource(uri: Uri, mimeType: String) { - val mediaItem = MediaItem.Builder() - .setUri(uri) - .setMimeType(mimeType) - .setSubtitleConfigurations(subtitles) - .setMetadata(streams) - .build() + val mediaItem = createMediaItem(uri, mimeType) exoPlayer.setMediaItem(mediaItem) } @@ -1307,6 +1316,16 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { } // HLS streams.hls != null -> { + val hlsMediaSourceFactory = HlsMediaSource.Factory(cronetDataSourceFactory) + .setPlaylistParserFactory(YoutubeHlsPlaylistParser.Factory()) + + val mediaSource = hlsMediaSourceFactory.createMediaSource( + createMediaItem( + ProxyHelper.unwrapStreamUrl(streams.hls!!).toUri(), + MimeTypes.APPLICATION_M3U8, + ) + ) + exoPlayer.setMediaSource(mediaSource) ProxyHelper.unwrapStreamUrl(streams.hls!!).toUri() to MimeTypes.APPLICATION_M3U8 } // NO STREAM FOUND diff --git a/app/src/main/java/com/github/libretube/util/YouTubeHlsPlaylistParser.kt b/app/src/main/java/com/github/libretube/util/YouTubeHlsPlaylistParser.kt new file mode 100644 index 000000000..9f83b6609 --- /dev/null +++ b/app/src/main/java/com/github/libretube/util/YouTubeHlsPlaylistParser.kt @@ -0,0 +1,258 @@ +package com.github.libretube.util + +import android.net.Uri +import androidx.annotation.OptIn +import androidx.media3.common.C +import androidx.media3.common.Format +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist +import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist +import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist.Rendition +import androidx.media3.exoplayer.hls.playlist.HlsPlaylist +import androidx.media3.exoplayer.hls.playlist.HlsPlaylistParser +import androidx.media3.exoplayer.hls.playlist.HlsPlaylistParserFactory +import androidx.media3.exoplayer.upstream.ParsingLoadable +import java.io.InputStream + +/** + * A YouTube HLS playlist parser which adds role flags to audio formats with track types. + * + * YouTube does not provide descriptive audio track types in a standard way and there is no standard + * way to tell whether an audio track is a dubbed track. + * + * However, this information is still provided in the track name, a non-standard property + * (`YT-EXT-XTAGS` which has its value encoded as a protocol buffer) and the stream manifest URL. + * + * This playlist parser adds track types to audio formats which have this information, by parsing + * the manifest URL of these formats. + * + * It relies internally on a default [HlsPlaylistParser] and processes audio tracks when the + * [HlsPlaylistParser] instance used parsed the manifest. + */ +@OptIn(UnstableApi::class) +class YoutubeHlsPlaylistParser : ParsingLoadable.Parser { + + /** + * Factory to create [YoutubeHlsPlaylistParser] instances. + */ + class Factory : HlsPlaylistParserFactory { + override fun createPlaylistParser() = YoutubeHlsPlaylistParser() + + override fun createPlaylistParser( + multivariantPlaylist: HlsMultivariantPlaylist, + previousMediaPlaylist: HlsMediaPlaylist? + ) = YoutubeHlsPlaylistParser(multivariantPlaylist, previousMediaPlaylist) + } + + /** + * The [HlsPlaylistParser] instance which is used to delegate parsing of HLS manifests. + */ + private val hlsPlaylistParser: HlsPlaylistParser + + /** + * @see [HlsPlaylistParser] no-parameters constructor + */ + private constructor() { + this.hlsPlaylistParser = HlsPlaylistParser() + } + + /** + * @see [HlsPlaylistParser] constructor with [HlsMultivariantPlaylist] and [HlsMediaPlaylist] + * parameters + */ + private constructor( + multivariantPlaylist: HlsMultivariantPlaylist, + previousMediaPlaylist: HlsMediaPlaylist? + ) { + this.hlsPlaylistParser = HlsPlaylistParser(multivariantPlaylist, previousMediaPlaylist) + } + + /** + * Parse a YouTube HLS playlist. + * + * If the given HLS playlist type is not a [HlsMultivariantPlaylist], it is returned as it is. + * + * If that's the case, audios extracted from the playlist are parsed and the good audio track + * type is set to each audio, if applicable and if this information is available. + * + * @param uri the source [Uri] of the response, after any redirection. + * @param inputStream an [InputStream] from which the response data can be read. + * + * @return a [HlsPlaylist] which is either the original one parsed by the delegated + * [HlsPlaylistParser] instance or a [HlsMultivariantPlaylist] on which audio formats have been + * edited to add the role track type flags to the existing ones on them if needed + */ + override fun parse(uri: Uri, inputStream: InputStream): HlsPlaylist { + val hlsPlaylist = hlsPlaylistParser.parse(uri, inputStream) + if (hlsPlaylist !is HlsMultivariantPlaylist) { + return hlsPlaylist + } + + val hlsMultivariantPlaylist: HlsMultivariantPlaylist = hlsPlaylist + + return HlsMultivariantPlaylist( + hlsMultivariantPlaylist.baseUri, + hlsMultivariantPlaylist.tags, + hlsMultivariantPlaylist.variants, + hlsMultivariantPlaylist.videos, + getAudioRenditionsWithTrackTypeSet(hlsMultivariantPlaylist.audios), + hlsMultivariantPlaylist.subtitles, + hlsMultivariantPlaylist.closedCaptions, + // YouTube HLS playlists have only demuxed formats, so it should be not needed to parse + // the muxed format, as it would be always null in this case + hlsMultivariantPlaylist.muxedAudioFormat, + hlsMultivariantPlaylist.muxedCaptionFormats, + hlsMultivariantPlaylist.hasIndependentSegments, + hlsMultivariantPlaylist.variableDefinitions, + hlsMultivariantPlaylist.sessionKeyDrmInitData + ) + } + + /** + * Get audio renditions with track types set on them, if they are not already set. + * + * This function parses audio track types from the stream manifest URL, by parsing the `acont` + * value of the `xtags` property of the value of the `sgoap` "path parameter". + * It adds then the corresponding ExoPlayer role flag in the audio format, if it has been not + * already set (this should never be the case). + * + * Any failure when the audio track type property could not parsed when it should (audio track + * types are only available on videos with multiple audio tracks) is ignored and the stream is + * kept as it is in this case. + * + * @param hlsMultivariantPlaylistAudios the list of audio [Rendition]s of a + * [HlsMultivariantPlaylist] + * @return a new list of audio [Rendition]s with audio track types set in the role flags of the + * audio formats + */ + private fun getAudioRenditionsWithTrackTypeSet( + hlsMultivariantPlaylistAudios: List + ): List { + return hlsMultivariantPlaylistAudios.map { + // Add the audio stream as it is if no path segments has been found + // This should never happen, as YouTube always uses path segments for their HLS URLs + val pathSegments = it.url?.pathSegments ?: return@map it + + // Path segments after the videoplayback one can be also converted to query parameters + // (the contrary is also possible), so these segments work like keys and values in a map + val sgoapPathParameterNameIndex = pathSegments.indexOf(SGOAP_PATH_PARAMETER) + + // Return the audio stream as it is if no audio track type parameter has been found + if (sgoapPathParameterNameIndex == -1) { + return@map it + } + + val sgoapPathParameterValueIndex = sgoapPathParameterNameIndex + 1 + + if (sgoapPathParameterValueIndex == pathSegments.size) { + return@map it + } + + Rendition( + it.url, + createAudioFormatFromAcountValue( + pathSegments[sgoapPathParameterValueIndex], + it.format + ), + it.groupId, + it.name + ) + } + } + + /** + * Create an audio [Format] based on an existing one and the `acont` property value of the + * `xtags` one, from a `sgoap` path parameter value. + * + * If the `acont` property has been found in the `sgoap` path parameter value provided, an + * audio track type role flag is added to the existing ones, if it isn't already added, using + * [getFullAudioRoleFlags]; otherwise, the format is kept as it is. + * + * @param sgoapPathParameterValue a `sgoap` path parameter value + * @param audioFormat the audio format linked to the URL from which the + * `sgoapPathParameterValue` parameter comes from + * @return an [Format] based of the original one provided or the original one if the `acont` + * property has been not found + */ + private fun createAudioFormatFromAcountValue( + sgoapPathParameterValue: String, + audioFormat: Format + ): Format { + XTAGS_ACONT_VALUE_REGEX.find(sgoapPathParameterValue)?.groupValues?.get(1) + ?.let { acontValue -> + return audioFormat.buildUpon() + .setRoleFlags( + getFullAudioRoleFlags( + audioFormat.roleFlags, + acontValue + ) + ) + .build() + } + + // If no info about format being original, dubbed or descriptive, return the format as it is + return audioFormat + } + + /** + * Get the full audio role flags of an audio track. + * + * Full role flags are the existing flags parsed by ExoPlayer and the flags coming from the + * audio track type parsed from the `acont` property value of the stream manifest URL. + * + * The following table describes what value is parsed + * + * | `acont` value | Role flag added from [ExoPlayer track role flags][C.RoleFlags] | + * | ------------- | ------------- | + * | `dubbed` | [C.ROLE_FLAG_DUB] | + * | `descriptive` | [C.ROLE_FLAG_DESCRIBES_VIDEO] | + * | `original` | [C.ROLE_FLAG_MAIN] | + * | everything else | [C.ROLE_FLAG_ALTERNATE] | + * + * @param roleFlags the current role flags of the audio track + * @param acontValue the value of the `acont` property + * @return the full audio role flags of the audio track like described above + */ + private fun getFullAudioRoleFlags( + roleFlags: Int, + acontValue: String + ): Int { + val acontRoleFlags = when (acontValue.lowercase()) { + "dubbed" -> C.ROLE_FLAG_DUB + "descriptive" -> C.ROLE_FLAG_DESCRIBES_VIDEO + "original" -> C.ROLE_FLAG_MAIN + // Original audio tracks without other audio track should not have the `acont` property + // nor the `xtags` one, so the the track should be not set as the main one + // The alternate role flag should be the most relevant flag in this case + else -> C.ROLE_FLAG_ALTERNATE + } + + // Add this flag to the existing ones (if it has been not already added) and return the + // result of this operation + return roleFlags or acontRoleFlags + } + + companion object { + + /** + * Constant for the `sgoap` "path parameter" name. + * + * YouTube HLS streams are for most of them, the same streams delivered as the DASH ones. + * The service provide information on the original stream of an HLS stream URL in "path + * parameters", `sgovp` for video streams and `sgoap` for audio streams. + * + * This information should include, for audio streams, the track type when there is multiple + * audio tracks in a video, which is what we need to get. + */ + private const val SGOAP_PATH_PARAMETER = "sgoap" + + /** + * Regular expression to find the `acont` property value of the `xtags` property value from + * a `sgoap` "path parameter" value of a YouTube HLS streaming URL. + * + * The `acont` property provides the track type of an audio stream, when a video of the + * service has multiple audio tracks. + */ + private val XTAGS_ACONT_VALUE_REGEX = Regex("xtags=.*acont=(.[^:]+)") + } +} From d21daf341d3274c41d2355c144caa78d19cd71ef Mon Sep 17 00:00:00 2001 From: Bnyro Date: Sun, 16 Jul 2023 17:27:10 +0200 Subject: [PATCH 3/7] Add UI support for audio track types Co-authored-by: AudricV <74829229+AudricV@users.noreply.github.com> --- .../libretube/ui/fragments/PlayerFragment.kt | 52 ++++++++++++++++--- app/src/main/res/values/strings.xml | 9 ++++ 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt index 78637cc55..ad75a953a 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt @@ -1348,9 +1348,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { trackSelector = DefaultTrackSelector(requireContext()) trackSelector.updateParameters { - setPreferredAudioLanguage( - LocaleHelper.getAppLocale().language.lowercase().substring(0, 2) - ) + setPreferredAudioLanguage(LocaleHelper.getAppLocale().isO3Language) } exoPlayer = ExoPlayer.Builder(requireContext()) @@ -1433,21 +1431,59 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { .show(childFragmentManager) } - private fun getAudioStreamGroups(audioStreams: List?): Map> { + 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?): Map> { return audioStreams.orEmpty() - .groupBy { it.audioTrackName } + .groupBy { getKeyByAudioStreamGroup(it) } } override fun onAudioStreamClicked() { val audioGroups = getAudioStreamGroups(streams.audioStreams) - val audioLanguages = audioGroups.map { it.key ?: getString(R.string.default_audio_track) } + val audioLanguages = audioGroups.map { it.key } BaseBottomSheet() .setSimpleItems(audioLanguages) { index -> val audioStreams = audioGroups.values.elementAt(index) - val lang = audioStreams.firstOrNull()?.audioTrackId?.substring(0, 2) + val firstAudioStream = audioStreams.firstOrNull() trackSelector.updateParameters { - setPreferredAudioLanguage(lang) + 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 + } + ) } } .show(childFragmentManager) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 44008ad86..19e16dfcc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -445,6 +445,15 @@ YouTube (CSV) Home tab content Show search suggestions + %1$s - %2$s + Unknown audio language + Unknown audio track type + original or main + dubbed + descriptive + default or unknown + unknown or no audio + Download Service Shows a notification when downloading media. From 9d25d32bff9977b704637919fdc14d88d614c9dc Mon Sep 17 00:00:00 2001 From: Bnyro Date: Sun, 16 Jul 2023 17:31:17 +0200 Subject: [PATCH 4/7] Rely on ExoPlayer audio tracks instead of Piped streams for selection Co-authored-by: AudricV <74829229+AudricV@users.noreply.github.com> --- .../github/libretube/helpers/PlayerHelper.kt | 138 ++++++++++++++++++ .../libretube/ui/fragments/PlayerFragment.kt | 95 +++++------- 2 files changed, 177 insertions(+), 56 deletions(-) diff --git a/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt b/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt index 87c81d11a..308ae2918 100644 --- a/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt +++ b/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt @@ -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 { + 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, + keepOnlySelectedTracks: Boolean + ): List> { + // 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) + } } diff --git a/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt index ad75a953a..836c8691f 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt @@ -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?): Map> { - 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() { From df4df4fccc7f568c5f5708784ab118beaab79971 Mon Sep 17 00:00:00 2001 From: Bnyro Date: Sun, 16 Jul 2023 17:33:37 +0200 Subject: [PATCH 5/7] Ignore unknown audio track types Co-authored-by: AudricV <74829229+AudricV@users.noreply.github.com> --- .../github/libretube/helpers/DashHelper.kt | 26 +++++++++---------- .../libretube/ui/fragments/PlayerFragment.kt | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/github/libretube/helpers/DashHelper.kt b/app/src/main/java/com/github/libretube/helpers/DashHelper.kt index de382facf..6b75cb29f 100644 --- a/app/src/main/java/com/github/libretube/helpers/DashHelper.kt +++ b/app/src/main/java/com/github/libretube/helpers/DashHelper.kt @@ -115,14 +115,18 @@ object DashHelper { adapSetElement.setAttribute("lang", adapSet.audioLocale) } - val roleElement = doc.createElement("Role") - roleElement.setAttribute("schemeIdUri", "urn:mpeg:dash:role:2011") - roleElement.setAttribute( - "value", - getRoleValueFromAudioTrackType(adapSet.audioTrackType) - ) - - adapSetElement.appendChild(roleElement) + // Only add the Role element if there is a track type set + // This allows distinction between formats marked as original on YouTube and + // formats without track type info set + if (adapSet.audioTrackType != null) { + val roleElement = doc.createElement("Role") + roleElement.setAttribute("schemeIdUri", "urn:mpeg:dash:role:2011") + roleElement.setAttribute( + "value", + getRoleValueFromAudioTrackType(adapSet.audioTrackType) + ) + adapSetElement.appendChild(roleElement) + } val isVideo = adapSet.mimeType.contains("video") @@ -198,11 +202,7 @@ object DashHelper { return segmentBase } - private fun getRoleValueFromAudioTrackType(audioTrackType: String?): String { - if (audioTrackType == null) { - return "main" - } - + private fun getRoleValueFromAudioTrackType(audioTrackType: String): String { return when (audioTrackType.lowercase()) { "descriptive" -> "description" "dubbed" -> "dub" diff --git a/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt index 836c8691f..0287db45e 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt @@ -1429,7 +1429,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { } .show(childFragmentManager) } - + override fun onAudioStreamClicked() { val context = requireContext() val audioLanguagesAndRoleFlags = PlayerHelper.getAudioLanguagesAndRoleFlagsFromTrackGroups( From de97da8bffa18857ff4a366d94a6dbdf96fb2166 Mon Sep 17 00:00:00 2001 From: Bnyro Date: Sun, 16 Jul 2023 17:37:13 +0200 Subject: [PATCH 6/7] Show selected track type in player options bottom sheet Co-authored-by: AudricV <74829229+AudricV@users.noreply.github.com> --- .../libretube/ui/views/OnlinePlayerView.kt | 45 +++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/github/libretube/ui/views/OnlinePlayerView.kt b/app/src/main/java/com/github/libretube/ui/views/OnlinePlayerView.kt index 6e39366c7..39b061641 100644 --- a/app/src/main/java/com/github/libretube/ui/views/OnlinePlayerView.kt +++ b/app/src/main/java/com/github/libretube/ui/views/OnlinePlayerView.kt @@ -39,9 +39,7 @@ class OnlinePlayerView( BottomSheetItem( context.getString(R.string.audio_track), R.drawable.ic_audio, - { - trackSelector?.parameters?.preferredAudioLanguages?.firstOrNull() - } + { getCurrentAudioTrackTitle() } ) { playerOptions?.onAudioStreamClicked() }, @@ -67,6 +65,47 @@ class OnlinePlayerView( ) } + private fun getCurrentAudioTrackTitle(): String { + if (player == null) { + return context.getString(R.string.unknown_or_no_audio) + } + + // The player reference should be not changed between the null check + // and its access, so a non null assertion should be safe here + val selectedAudioLanguagesAndRoleFlags = + PlayerHelper.getAudioLanguagesAndRoleFlagsFromTrackGroups( + player!!.currentTracks.groups, + true + ) + + if (selectedAudioLanguagesAndRoleFlags.isEmpty()) { + return context.getString(R.string.unknown_or_no_audio) + } + + // At most one audio track should be selected regardless of audio + // format or quality + val firstSelectedAudioFormat = selectedAudioLanguagesAndRoleFlags[0] + + if (selectedAudioLanguagesAndRoleFlags.size == 1 + && firstSelectedAudioFormat.first == null + && !PlayerHelper.haveAudioTrackRoleFlagSet( + firstSelectedAudioFormat.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) + return context.getString(R.string.default_or_unknown_audio_track) + } + + return PlayerHelper.getAudioTrackNameFromFormat( + context, + firstSelectedAudioFormat + ) + } + fun initPlayerOptions( playerViewModel: PlayerViewModel, viewLifecycleOwner: LifecycleOwner, From 6fc4c9e4064e2afcaa26d9cdb8767ad6faf50097 Mon Sep 17 00:00:00 2001 From: Bnyro Date: Sun, 16 Jul 2023 17:38:45 +0200 Subject: [PATCH 7/7] Remove unused string for default track type Co-authored-by: AudricV <74829229+AudricV@users.noreply.github.com> --- app/src/main/res/values-ar/strings.xml | 1 - app/src/main/res/values-az/strings.xml | 1 - app/src/main/res/values-be/strings.xml | 1 - app/src/main/res/values-bn/strings.xml | 1 - app/src/main/res/values-ckb/strings.xml | 1 - app/src/main/res/values-cs/strings.xml | 1 - app/src/main/res/values-de/strings.xml | 1 - app/src/main/res/values-el/strings.xml | 1 - app/src/main/res/values-es/strings.xml | 1 - app/src/main/res/values-eu/strings.xml | 1 - app/src/main/res/values-fa/strings.xml | 1 - app/src/main/res/values-fi/strings.xml | 1 - app/src/main/res/values-fr/strings.xml | 1 - app/src/main/res/values-gu/strings.xml | 1 - app/src/main/res/values-hi/strings.xml | 1 - app/src/main/res/values-hu/strings.xml | 1 - app/src/main/res/values-in/strings.xml | 1 - app/src/main/res/values-it/strings.xml | 1 - app/src/main/res/values-iw/strings.xml | 1 - app/src/main/res/values-ja/strings.xml | 1 - app/src/main/res/values-ko/strings.xml | 1 - app/src/main/res/values-lt/strings.xml | 1 - app/src/main/res/values-lv/strings.xml | 1 - app/src/main/res/values-mr/strings.xml | 1 - app/src/main/res/values-nb-rNO/strings.xml | 1 - app/src/main/res/values-or/strings.xml | 1 - app/src/main/res/values-pa/strings.xml | 1 - app/src/main/res/values-pl/strings.xml | 1 - app/src/main/res/values-pt-rBR/strings.xml | 1 - app/src/main/res/values-pt/strings.xml | 1 - app/src/main/res/values-ro/strings.xml | 1 - app/src/main/res/values-ru/strings.xml | 1 - app/src/main/res/values-si/strings.xml | 1 - app/src/main/res/values-sr/strings.xml | 1 - app/src/main/res/values-tr/strings.xml | 1 - app/src/main/res/values-uk/strings.xml | 1 - app/src/main/res/values-vi/strings.xml | 1 - app/src/main/res/values-zh-rCN/strings.xml | 1 - app/src/main/res/values-zh-rTW/strings.xml | 1 - app/src/main/res/values/strings.xml | 1 - 40 files changed, 40 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index d18584f40..351b35ac9 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -311,7 +311,6 @@ تخطيط المشغل البديل وتظهر أشرطة الفيديو ذات الصلة كصف أعلى من التعليقات بدلا من التعليقات الواردة أدناه. مسار صوتي - الإفتراضي استخدام HLS استخدم HLS بدلا من DASH (سيكون أبطأ ، غير مستحسن) تلقائي diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index ebfe98422..6dd2900d8 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -311,7 +311,6 @@ Alternativ oynadıcı tərtibatı Əlaqəli videoları aşağı əvəzinə, şərhlər üzərində cərgə kimi göstər. Səs axını - Standart Avtomatik HLS istifadə et DASH əvəzinə HLS istifadə et (daha yavaş olacaq, tövsiyə edilmədi) diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 4001b99aa..2a5db5677 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -304,7 +304,6 @@ Прамежак часу, у які дазволена паказваць апавяшчэнні. Паказваць звязаныя відэа ў радку над каментарыямі, а не ўнізе. Гукавая дарожка - Па змаўчанні Выкарыстоўвайце HLS Аўто Фармат файла не падтрымліваецца: %1$s diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 8e3af3ba6..22025072e 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -309,7 +309,6 @@ লেয়াউট বিকল্প প্লেয়ার লেয়াউট অডিও ট্র্যাক - ডিফল্ট HLS ব্যাবহার করুন DASH এর বদলে HLS ব্যাবহার করুন (ধীরগতিসম্পন্ন হতে পারে, সুপারিশকৃত নয়) স্বয়ংক্রিয় diff --git a/app/src/main/res/values-ckb/strings.xml b/app/src/main/res/values-ckb/strings.xml index ca93332f5..ad40d685c 100644 --- a/app/src/main/res/values-ckb/strings.xml +++ b/app/src/main/res/values-ckb/strings.xml @@ -331,7 +331,6 @@ دیاریکردنی مەودای کاتی ئاگادارکردنەوەکان بۆ پیشاندانی بڵاوکراوە. پیشاندانی ڤیدیۆ هاوشێوەکان لەسەرووی بەشی لێدوانەکان. تراکی دەنگی - بنەڕەتی سنوردارکردن بۆ لێدان پێشنیارکراوەکان هەڵبژێردراوەکان diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 7cb04c3f3..b7f53133e 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -311,7 +311,6 @@ Rozložení Alternativní rozložení přehrávače Zvuková stopa - Výchozí Auto Použít HLS Použít HLS místo DASH (bude pomalejší, nedoporučuje se) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 2c941cf3e..1b3e96f94 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -309,7 +309,6 @@ Zur Playlist %1$s hinzugefügt Markierungen Tonspur - Standard Livestreams Alternatives Video-Layout HLS verwenden diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 43f86c7eb..5eb61b40c 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -367,7 +367,6 @@ Εισαγωγή λιστών αναπαραγωγής Εξαγωγή λιστών αναπαραγωγής Ηχητικό κομμάτι - Προεπιλογή Αναπαραγωγή ήχου Μετονομασία λίστας αναπαραγωγής Εισαγάγετε σχετικά βίντεο diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index e93f6d89d..2d356e072 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -311,7 +311,6 @@ Ordenar Diseño Pista de audio - Predeterminada Automatico Usar HLS en vez de DASH (podría ser lento, no recomendado) Usar HLS diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index f3c63623d..7ca3111d1 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -319,7 +319,6 @@ Erabil atximur egiteko keinua, hurbiltzeko/ urruntzeko. Tonua Audio pista - Lehenetsia Lehenetsia Bistaratu erlazionatutako bideoak errenkada gisa iruzkinen gainetik, behean agertu beharrean. HLS erabili diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 824b8dbc8..f8851d4a7 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -242,7 +242,6 @@ ترجیحات صف نوار ناوبری - پیش‌گزیده استفاده از HLS خودکار آن‌چه اکنون داغ است diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 056e2007e..3a27f7c6b 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -348,7 +348,6 @@ Ilmoitusaika Järjestys Ääniraita - Oletus Käytä HLS:ää DASHin sijaan (on hitaampi, ei suositella) Auto Rajoita suoritusaikaa diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 5729d4abe..ad8b384dd 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -311,7 +311,6 @@ Mise en page Afficher les vidéos connexes en une ligne au-dessus des commentaires, à la place d\'au-dessous. Piste audio - Par défaut Utiliser HLS Utiliser HLS à la place de DASH (sera plus lent, non recommandé) Automatique diff --git a/app/src/main/res/values-gu/strings.xml b/app/src/main/res/values-gu/strings.xml index 586d4eb5f..f4b0094c5 100644 --- a/app/src/main/res/values-gu/strings.xml +++ b/app/src/main/res/values-gu/strings.xml @@ -241,7 +241,6 @@ %1$d વિડિયો પુન:પ્રયત્ન કરો ટિપ્પણીઓ - મૂળભૂત ડાઉનલોડ્સ લાઇવસ્ટ્રીમ્સ સુધારા માટે ચકાસો diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 3c4be7753..7c1f8c77d 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -198,7 +198,6 @@ समय पट्टी पर खंडों को चिह्नित करें। संस्करण %1$s उपलब्ध है ऑडियो ट्रैक - डिफ़ॉल्ट डाउनलोडस लाइव स्ट्रीम वैकल्पिक वीडियो लेआउट diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 495249706..b0bdee121 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -310,7 +310,6 @@ Alternatív lejátszó elrendezés Sorrend A kapcsolódó videók megjelenítése egy sorban a hozzászólások felett, nem pedig alattuk. - Alapérték Hangsáv HLS használata Automatikus diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index e2d294299..a8dfa6e99 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -311,7 +311,6 @@ Tata letak pemutar alternatif Tampilkan video terkait sebagai baris di atas komentar, bukan di bawah. Trek audio - Bawaan Gunakan HLS Otomatis Gunakan HLS daripada DASH (akan lebih lambat, tidak disarankan) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index c79bd0a07..c29bca9cb 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -318,7 +318,6 @@ Disposizione Layout alternativo del player Traccia audio - Predefinita Usa HLS Usa HLS invece di DASH (sarà più lento, non consigliato) Automatica diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 4f87d7cb1..8db2f8f9c 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -311,7 +311,6 @@ פריסה פריסת נגן חלופית רצועת שמע - ברירת מחדל אוטומטי להשתמש בתזרים להשתמש בתזרים (HLS) במקום ב־DASH (אטי יותר, לא מומלץ) diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 28a78fd45..6564e8df4 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -340,7 +340,6 @@ 続ける 最新の動画を再生 音声トラック - 標準 DASHの代わりにHLSを使用します (より低速、非推奨) HLSを使う 自動 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index df2207ce8..ff90d891d 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -240,7 +240,6 @@ 재생목록 가져오기 재생목록 내보내기 오디오 트랙 - 기본값 HLS를 사용하세요 런타임으로 제한 트렌드 diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index d39e4be1d..2a09d197e 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -311,7 +311,6 @@ Rodyti susijusius vaizdo įrašus kaip eilutę virš komentarų, o ne kaip įprasta po jais. Alternatyvus grotuvo išdėstymas Garso takelis - Numatytas Automatinė Naudoti HLS Vietoj DASH naudoti HLS (veiks lėčiau, nerekomenduojama) diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 721c56144..bc0d9030b 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -339,7 +339,6 @@ Izklājums Alternatīvs atskaņotāja izklājums Rādīt saistītos video rindā virs komentāriem nevis zem tiem. - Noklusējumaa Neatbalstīts faila formāts: %1$s Izmantot HLS Automātiski diff --git a/app/src/main/res/values-mr/strings.xml b/app/src/main/res/values-mr/strings.xml index 0e18e7089..2ebab192f 100644 --- a/app/src/main/res/values-mr/strings.xml +++ b/app/src/main/res/values-mr/strings.xml @@ -336,7 +336,6 @@ मांडणी पर्यायी प्लेअर लेआउट ऑडिओ ट्रॅक - डीफॉल्ट रनटाइमची मर्यादा संबंधित व्हिडिओ समाविष्ट करा ब्राईटनेस diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 7489b9e37..931bc3452 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -308,7 +308,6 @@ Tidsperiode merknadene kan vises. Rekkefølge Vis relaterte videoer som rad over kommentarene istedenfor under. - Forvalg Lydspor Bruk HLS Bruk HLS istedenfor DASH (tregere og ikke anbefalt) diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index a640800d4..c5579d75d 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -311,7 +311,6 @@ ବିକଳ୍ପ ପ୍ଲେୟାର୍ ଲେଆଉଟ୍ ନିମ୍ନୋକ୍ତ ମନ୍ତବ୍ୟଗୁଡିକ ଉପରେ ସଂପୃକ୍ତ ଭିଡିଓଗୁଡିକ ଧାଡି ଭାବରେ ଦେଖାନ୍ତୁ । ଅଡିଓ ଟ୍ରାକ୍ - ଡିଫଲ୍ଟ HLS ବ୍ୟବହାର କରନ୍ତୁ DASH ପରିବର୍ତ୍ତେ HLS ବ୍ୟବହାର କରନ୍ତୁ (ଧୀର ହେବ, ସୁପାରିଶ ହେବ ନାହିଁ) ସ୍ଵତଃ diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index f109e8413..a90fc4bb4 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -336,7 +336,6 @@ ਲੇਆਊਟ ਸਮਾਂ ਸੀਮਤ ਕਰੋ ਜਿਸ ਵਿੱਚ ਸਟ੍ਰੀਮ ਸੂਚਨਾਵਾਂ ਦਿਖਾਈਆਂ ਜਾਂਦੀਆਂ ਹਨ। ਆਰਡਰ - ਡੀਫ਼ਾਲਟ ਵਿਕਲਪਿਕ ਪਲੇਅਰ ਲੇਆਊਟ ਹੁਣ ਕੀ ਰੁਝਾਨ ਹੈ HLS ਦੀ ਵਰਤੋਂ ਕਰੋ diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index fa633857f..327253376 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -311,7 +311,6 @@ Pokaż powiązane filmiki w wierszu nad komentarzami. Inny układ powiązanych filmów Ścieżka dźwiękowa - Domyślna HTTP Live Streaming Użyj HLS zamiast DASH (będzie wolniej, nie zalecane) Automatyczna diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 3eb3eb6f0..b3e0c6e63 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -311,7 +311,6 @@ Esquema alternativo do reprodutor Exibir os vídeos relacionados como uma linha acima dos comentários em vez de abaixo. Faixa de áudio - Padrão Usar HLS Usar HLS em vez de DASH (será mais lento, não recomendado) Automático diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 0ff060a57..1a2461cdc 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -312,7 +312,6 @@ Esquema alternativo de reprodução Mostrar os vídeos relacionados como uma linha acima dos comentários em vez de abaixo. Faixa de áudio - Padrão Usar HLS Usar HLS em vez de DASH (será mais lento, não recomendado) Automático diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 5db26b2ef..34e2ed450 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -339,7 +339,6 @@ Timpul de începere Timpul de sfârșit Restricționați timpul de notificare - Implicit Format de fișier nesuportat: %1$s Utilizați HLS Utilizați HLS în loc de DASH (va fi mai lent, nu este recomandat) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index b936cc86b..834ba7605 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -311,7 +311,6 @@ Макет Альтернативный макет плеера Аудио трек - По умолчанию Использовать HLS Использовать HLS вместо DASH (будет медленнее, не рекомендуется) Авто diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml index 79376d9f5..35df2b0b4 100644 --- a/app/src/main/res/values-si/strings.xml +++ b/app/src/main/res/values-si/strings.xml @@ -223,7 +223,6 @@ පිළිවෙල පිරිසැලසුම ශ්‍රව්‍ය පථය - පෙරනිමිය HLS භාවිතා කරන්න ස්වයං ධාවන කාලයට සීමා කරන්න diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 02fcf0ffa..1f1789a46 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -308,7 +308,6 @@ Маркери Означите сегменте на временској траци. Аудио запис - Подразумевано Стримови уживо Алтернативни изглед видео записа Подразумевано светло diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 8b29275e8..c4090479c 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -311,7 +311,6 @@ İlgili videoları, yorumların altında değil, üstünde bir satır olarak göster. Sıra Müzik parçası - Varsayılan Otomatik HLS\'yi kullan DASH yerine HLS kullan (daha yavaş olacaktır, önerilmez) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 4bb520447..02232dd35 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -311,7 +311,6 @@ Вигляд Альтернативний вигляд програвача Звукова доріжка - Типово Використовувати HLS Використовувати HLS замість DASH (працюватиме повільніше, не рекомендується) Авто diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 80886de5d..335d78522 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -310,7 +310,6 @@ Đánh dấu các phân đoạn trên thanh thời lượng. Đánh dấu Track âm thanh - Mặc định Trực tiếp Bố cục thay thế cho videos Light mặc định diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 89bd547e6..417348afb 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -311,7 +311,6 @@ 备选的播放器布局 将相关视频显示为评论上方而不是下方的一行。 音轨 - 默认 使用 HLS 使用 HLS 而不是 DASH(会更慢,不推荐) 自动 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 8375cd66c..afc12fea3 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -327,7 +327,6 @@ 調整音高 播放最新影片 音訊曲目 - 預設 匯出訂閱列表 跳過按鈕 無結果。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 19e16dfcc..76d900791 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -330,7 +330,6 @@ Alternative player layout Show the related videos as a row above the comments instead of below. Audio track - Default Unsupported file format: %1$s Use HLS Use HLS instead of DASH (will be slower, not recommended)