Merge pull request #4240 from Bnyro/audio-track-types

Support for different audio track types
This commit is contained in:
Bnyro 2023-07-16 20:12:48 +02:00 committed by GitHub
commit 1fd905222d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 558 additions and 79 deletions

View File

@ -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." +

View File

@ -19,8 +19,10 @@ object DashHelper {
private data class AdapSetInfo(
val mimeType: String,
val formats: MutableList<PipedStream> = mutableListOf(),
val audioTrackId: String? = null,
val formats: MutableList<PipedStream> = 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,8 +108,24 @@ 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)
}
// 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")
@ -162,18 +181,34 @@ 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 {
return when (audioTrackType.lowercase()) {
"descriptive" -> "description"
"dubbed" -> "dub"
"original" -> "main"
else -> "alternate"
}
}
private fun createVideoRepresentation(

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

@ -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
@ -55,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
@ -113,15 +113,16 @@ 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 java.io.IOException
import java.util.*
import java.util.concurrent.Executors
import com.github.libretube.util.YoutubeHlsPlaylistParser
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)
@ -164,6 +165,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 +1195,15 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
}
}
private fun setMediaSource(uri: Uri, mimeType: String) {
val mediaItem = MediaItem.Builder()
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 = createMediaItem(uri, mimeType)
exoPlayer.setMediaItem(mediaItem)
}
@ -1307,6 +1315,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
@ -1329,9 +1347,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())
@ -1414,24 +1430,46 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
.show(childFragmentManager)
}
private fun getAudioStreamGroups(audioStreams: List<PipedStream>?): Map<String?, List<PipedStream>> {
return audioStreams.orEmpty()
.groupBy { it.audioTrackName }
}
override fun onAudioStreamClicked() {
val audioGroups = getAudioStreamGroups(streams.audioStreams)
val audioLanguages = audioGroups.map { it.key ?: getString(R.string.default_audio_track) }
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 lang = audioStreams.firstOrNull()?.audioTrackId?.substring(0, 2)
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(lang)
setPreferredAudioLanguage(selectedAudioFormat.first)
setPreferredAudioRoleFlags(selectedAudioFormat.second)
}
}
.show(childFragmentManager)
}
baseBottomSheet.show(childFragmentManager)
}
override fun onStatsClicked() {

View File

@ -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,

View File

@ -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<HlsPlaylist> {
/**
* 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<Rendition>
): List<Rendition> {
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=(.[^:]+)")
}
}

View File

@ -311,7 +311,6 @@
<string name="alternative_player_layout">تخطيط المشغل البديل</string>
<string name="alternative_player_layout_summary">وتظهر أشرطة الفيديو ذات الصلة كصف أعلى من التعليقات بدلا من التعليقات الواردة أدناه.</string>
<string name="audio_track">مسار صوتي</string>
<string name="default_audio_track">الإفتراضي</string>
<string name="hls_instead_of_dash">استخدام HLS</string>
<string name="hls_instead_of_dash_summary">استخدم HLS بدلا من DASH (سيكون أبطأ ، غير مستحسن)</string>
<string name="auto_quality">تلقائي</string>

View File

@ -311,7 +311,6 @@
<string name="alternative_player_layout">Alternativ oynadıcı tərtibatı</string>
<string name="alternative_player_layout_summary">Əlaqəli videoları aşağı əvəzinə, şərhlər üzərində cərgə kimi göstər.</string>
<string name="audio_track">Səs axını</string>
<string name="default_audio_track">Standart</string>
<string name="auto_quality">Avtomatik</string>
<string name="hls_instead_of_dash">HLS istifadə et</string>
<string name="hls_instead_of_dash_summary">DASH əvəzinə HLS istifadə et (daha yavaş olacaq, tövsiyə edilmədi)</string>

View File

@ -304,7 +304,6 @@
<string name="notification_time_summary">Прамежак часу, у які дазволена паказваць апавяшчэнні.</string>
<string name="alternative_player_layout_summary">Паказваць звязаныя відэа ў радку над каментарыямі, а не ўнізе.</string>
<string name="audio_track">Гукавая дарожка</string>
<string name="default_audio_track">Па змаўчанні</string>
<string name="hls_instead_of_dash">Выкарыстоўвайце HLS</string>
<string name="auto_quality">Аўто</string>
<string name="unsupported_file_format">Фармат файла не падтрымліваецца: %1$s</string>

View File

@ -309,7 +309,6 @@
<string name="layout">লেয়াউট</string>
<string name="alternative_player_layout">বিকল্প প্লেয়ার লেয়াউট</string>
<string name="audio_track">অডিও ট্র্যাক</string>
<string name="default_audio_track">ডিফল্ট</string>
<string name="hls_instead_of_dash">HLS ব্যাবহার করুন</string>
<string name="hls_instead_of_dash_summary">DASH এর বদলে HLS ব্যাবহার করুন (ধীরগতিসম্পন্ন হতে পারে, সুপারিশকৃত নয়)</string>
<string name="auto_quality">স্বয়ংক্রিয়</string>

View File

@ -331,7 +331,6 @@
<string name="notification_time_summary">دیاریکردنی مەودای کاتی ئاگادارکردنەوەکان بۆ پیشاندانی بڵاوکراوە.</string>
<string name="alternative_player_layout_summary">پیشاندانی ڤیدیۆ هاوشێوەکان لەسەرووی بەشی لێدوانەکان.</string>
<string name="audio_track">تراکی دەنگی</string>
<string name="default_audio_track">بنەڕەتی</string>
<string name="limit_to_runtime">سنوردارکردن بۆ لێدان</string>
<string name="trends">پێشنیارکراوەکان</string>
<string name="featured">هەڵبژێردراوەکان</string>

View File

@ -311,7 +311,6 @@
<string name="layout">Rozložení</string>
<string name="alternative_player_layout">Alternativní rozložení přehrávače</string>
<string name="audio_track">Zvuková stopa</string>
<string name="default_audio_track">Výchozí</string>
<string name="auto_quality">Auto</string>
<string name="hls_instead_of_dash">Použít HLS</string>
<string name="hls_instead_of_dash_summary">Použít HLS místo DASH (bude pomalejší, nedoporučuje se)</string>

View File

@ -309,7 +309,6 @@
<string name="added_to_playlist">Zur Playlist %1$s hinzugefügt</string>
<string name="sb_markers">Markierungen</string>
<string name="audio_track">Tonspur</string>
<string name="default_audio_track">Standard</string>
<string name="livestreams">Livestreams</string>
<string name="alternative_videos_layout">Alternatives Video-Layout</string>
<string name="hls_instead_of_dash">HLS verwenden</string>

View File

@ -367,7 +367,6 @@
<string name="import_playlists">Εισαγωγή λιστών αναπαραγωγής</string>
<string name="export_playlists">Εξαγωγή λιστών αναπαραγωγής</string>
<string name="audio_track">Ηχητικό κομμάτι</string>
<string name="default_audio_track">Προεπιλογή</string>
<string name="audio_player">Αναπαραγωγή ήχου</string>
<string name="renamePlaylist">Μετονομασία λίστας αναπαραγωγής</string>
<string name="queue_insert_related_videos">Εισαγάγετε σχετικά βίντεο</string>

View File

@ -311,7 +311,6 @@
<string name="navbar_order">Ordenar</string>
<string name="layout">Diseño</string>
<string name="audio_track">Pista de audio</string>
<string name="default_audio_track">Predeterminada</string>
<string name="auto_quality">Automatico</string>
<string name="hls_instead_of_dash_summary">Usar HLS en vez de DASH (podría ser lento, no recomendado)</string>
<string name="hls_instead_of_dash">Usar HLS</string>

View File

@ -319,7 +319,6 @@
<string name="pinch_control_summary">Erabil atximur egiteko keinua, hurbiltzeko/ urruntzeko.</string>
<string name="playback_pitch">Tonua</string>
<string name="audio_track">Audio pista</string>
<string name="default_audio_track">Lehenetsia</string>
<string name="defaults">Lehenetsia</string>
<string name="alternative_player_layout_summary">Bistaratu erlazionatutako bideoak errenkada gisa iruzkinen gainetik, behean agertu beharrean.</string>
<string name="hls_instead_of_dash">HLS erabili</string>

View File

@ -242,7 +242,6 @@
<string name="preferences">ترجیحات</string>
<string name="queue">صف</string>
<string name="navigation_bar">نوار ناوبری</string>
<string name="default_audio_track">پیش‌گزیده</string>
<string name="hls_instead_of_dash">استفاده از HLS</string>
<string name="auto_quality">خودکار</string>
<string name="trending">آن‌چه اکنون داغ است</string>

View File

@ -348,7 +348,6 @@
<string name="notification_time">Ilmoitusaika</string>
<string name="navbar_order">Järjestys</string>
<string name="audio_track">Ääniraita</string>
<string name="default_audio_track">Oletus</string>
<string name="hls_instead_of_dash_summary">Käytä HLS:ää DASHin sijaan (on hitaampi, ei suositella)</string>
<string name="auto_quality">Auto</string>
<string name="limit_to_runtime">Rajoita suoritusaikaa</string>

View File

@ -311,7 +311,6 @@
<string name="layout">Mise en page</string>
<string name="alternative_player_layout_summary">Afficher les vidéos connexes en une ligne au-dessus des commentaires, à la place d\'au-dessous.</string>
<string name="audio_track">Piste audio</string>
<string name="default_audio_track">Par défaut</string>
<string name="hls_instead_of_dash">Utiliser HLS</string>
<string name="hls_instead_of_dash_summary">Utiliser HLS à la place de DASH (sera plus lent, non recommandé)</string>
<string name="auto_quality">Automatique</string>

View File

@ -241,7 +241,6 @@
<string name="videoCount">%1$d વિડિયો</string>
<string name="retry">પુન:પ્રયત્ન કરો</string>
<string name="comments">ટિપ્પણીઓ</string>
<string name="default_audio_track">મૂળભૂત</string>
<string name="downloads">ડાઉનલોડ્સ</string>
<string name="livestreams">લાઇવસ્ટ્રીમ્સ</string>
<string name="update_summary">સુધારા માટે ચકાસો</string>

View File

@ -198,7 +198,6 @@
<string name="sb_markers_summary">समय पट्टी पर खंडों को चिह्नित करें।</string>
<string name="update_available">संस्करण %1$s उपलब्ध है</string>
<string name="audio_track">ऑडियो ट्रैक</string>
<string name="default_audio_track">डिफ़ॉल्ट</string>
<string name="downloads">डाउनलोडस</string>
<string name="livestreams">लाइव स्ट्रीम</string>
<string name="alternative_videos_layout">वैकल्पिक वीडियो लेआउट</string>

View File

@ -310,7 +310,6 @@
<string name="alternative_player_layout">Alternatív lejátszó elrendezés</string>
<string name="navbar_order">Sorrend</string>
<string name="alternative_player_layout_summary">A kapcsolódó videók megjelenítése egy sorban a hozzászólások felett, nem pedig alattuk.</string>
<string name="default_audio_track">Alapérték</string>
<string name="audio_track">Hangsáv</string>
<string name="hls_instead_of_dash">HLS használata</string>
<string name="auto_quality">Automatikus</string>

View File

@ -311,7 +311,6 @@
<string name="alternative_player_layout">Tata letak pemutar alternatif</string>
<string name="alternative_player_layout_summary">Tampilkan video terkait sebagai baris di atas komentar, bukan di bawah.</string>
<string name="audio_track">Trek audio</string>
<string name="default_audio_track">Bawaan</string>
<string name="hls_instead_of_dash">Gunakan HLS</string>
<string name="auto_quality">Otomatis</string>
<string name="hls_instead_of_dash_summary">Gunakan HLS daripada DASH (akan lebih lambat, tidak disarankan)</string>

View File

@ -318,7 +318,6 @@
<string name="layout">Disposizione</string>
<string name="alternative_player_layout">Layout alternativo del player</string>
<string name="audio_track">Traccia audio</string>
<string name="default_audio_track">Predefinita</string>
<string name="hls_instead_of_dash">Usa HLS</string>
<string name="hls_instead_of_dash_summary">Usa HLS invece di DASH (sarà più lento, non consigliato)</string>
<string name="auto_quality">Automatica</string>

View File

@ -311,7 +311,6 @@
<string name="layout">פריסה</string>
<string name="alternative_player_layout">פריסת נגן חלופית</string>
<string name="audio_track">רצועת שמע</string>
<string name="default_audio_track">ברירת מחדל</string>
<string name="auto_quality">אוטומטי</string>
<string name="hls_instead_of_dash">להשתמש בתזרים</string>
<string name="hls_instead_of_dash_summary">להשתמש בתזרים (HLS) במקום ב־DASH (אטי יותר, לא מומלץ)</string>

View File

@ -340,7 +340,6 @@
<string name="proceed">続ける</string>
<string name="play_latest_videos">最新の動画を再生</string>
<string name="audio_track">音声トラック</string>
<string name="default_audio_track">標準</string>
<string name="hls_instead_of_dash_summary">DASHの代わりにHLSを使用します (より低速、非推奨)</string>
<string name="hls_instead_of_dash">HLSを使う</string>
<string name="auto_quality">自動</string>

View File

@ -240,7 +240,6 @@
<string name="import_playlists">재생목록 가져오기</string>
<string name="export_playlists">재생목록 내보내기</string>
<string name="audio_track">오디오 트랙</string>
<string name="default_audio_track">기본값</string>
<string name="hls_instead_of_dash">HLS를 사용하세요</string>
<string name="limit_to_runtime">런타임으로 제한</string>
<string name="trends">트렌드</string>

View File

@ -311,7 +311,6 @@
<string name="alternative_player_layout_summary">Rodyti susijusius vaizdo įrašus kaip eilutę virš komentarų, o ne kaip įprasta po jais.</string>
<string name="alternative_player_layout">Alternatyvus grotuvo išdėstymas</string>
<string name="audio_track">Garso takelis</string>
<string name="default_audio_track">Numatytas</string>
<string name="auto_quality">Automatinė</string>
<string name="hls_instead_of_dash">Naudoti HLS</string>
<string name="hls_instead_of_dash_summary">Vietoj DASH naudoti HLS (veiks lėčiau, nerekomenduojama)</string>

View File

@ -339,7 +339,6 @@
<string name="layout">Izklājums</string>
<string name="alternative_player_layout">Alternatīvs atskaņotāja izklājums</string>
<string name="alternative_player_layout_summary">Rādīt saistītos video rindā virs komentāriem nevis zem tiem.</string>
<string name="default_audio_track">Noklusējumaa</string>
<string name="unsupported_file_format">Neatbalstīts faila formāts: %1$s</string>
<string name="hls_instead_of_dash">Izmantot HLS</string>
<string name="auto_quality">Automātiski</string>

View File

@ -336,7 +336,6 @@
<string name="layout">मांडणी</string>
<string name="alternative_player_layout">पर्यायी प्लेअर लेआउट</string>
<string name="audio_track">ऑडिओ ट्रॅक</string>
<string name="default_audio_track">डीफॉल्ट</string>
<string name="limit_to_runtime">रनटाइमची मर्यादा</string>
<string name="queue_insert_related_videos">संबंधित व्हिडिओ समाविष्ट करा</string>
<string name="brightness">ब्राईटनेस</string>

View File

@ -308,7 +308,6 @@
<string name="notification_time_summary">Tidsperiode merknadene kan vises.</string>
<string name="navbar_order">Rekkefølge</string>
<string name="alternative_player_layout_summary">Vis relaterte videoer som rad over kommentarene istedenfor under.</string>
<string name="default_audio_track">Forvalg</string>
<string name="audio_track">Lydspor</string>
<string name="hls_instead_of_dash">Bruk HLS</string>
<string name="hls_instead_of_dash_summary">Bruk HLS istedenfor DASH (tregere og ikke anbefalt)</string>

View File

@ -311,7 +311,6 @@
<string name="alternative_player_layout">ବିକଳ୍ପ ପ୍ଲେୟାର୍ ଲେଆଉଟ୍</string>
<string name="alternative_player_layout_summary">ନିମ୍ନୋକ୍ତ ମନ୍ତବ୍ୟଗୁଡିକ ଉପରେ ସଂପୃକ୍ତ ଭିଡିଓଗୁଡିକ ଧାଡି ଭାବରେ ଦେଖାନ୍ତୁ ।</string>
<string name="audio_track">ଅଡିଓ ଟ୍ରାକ୍</string>
<string name="default_audio_track">ଡିଫଲ୍ଟ</string>
<string name="hls_instead_of_dash">HLS ବ୍ୟବହାର କରନ୍ତୁ</string>
<string name="hls_instead_of_dash_summary">DASH ପରିବର୍ତ୍ତେ HLS ବ୍ୟବହାର କରନ୍ତୁ (ଧୀର ହେବ, ସୁପାରିଶ ହେବ ନାହିଁ)</string>
<string name="auto_quality">ସ୍ଵତଃ</string>

View File

@ -336,7 +336,6 @@
<string name="layout">ਲੇਆਊਟ</string>
<string name="notification_time_summary">ਸਮਾਂ ਸੀਮਤ ਕਰੋ ਜਿਸ ਵਿੱਚ ਸਟ੍ਰੀਮ ਸੂਚਨਾਵਾਂ ਦਿਖਾਈਆਂ ਜਾਂਦੀਆਂ ਹਨ।</string>
<string name="navbar_order">ਆਰਡਰ</string>
<string name="default_audio_track">ਡੀਫ਼ਾਲਟ</string>
<string name="alternative_player_layout">ਵਿਕਲਪਿਕ ਪਲੇਅਰ ਲੇਆਊਟ</string>
<string name="trending">ਹੁਣ ਕੀ ਰੁਝਾਨ ਹੈ</string>
<string name="hls_instead_of_dash">HLS ਦੀ ਵਰਤੋਂ ਕਰੋ</string>

View File

@ -311,7 +311,6 @@
<string name="alternative_player_layout_summary">Pokaż powiązane filmiki w wierszu nad komentarzami.</string>
<string name="alternative_player_layout">Inny układ powiązanych filmów</string>
<string name="audio_track">Ścieżka dźwiękowa</string>
<string name="default_audio_track">Domyślna</string>
<string name="hls_instead_of_dash">HTTP Live Streaming</string>
<string name="hls_instead_of_dash_summary">Użyj HLS zamiast DASH (będzie wolniej, nie zalecane)</string>
<string name="auto_quality">Automatyczna</string>

View File

@ -311,7 +311,6 @@
<string name="alternative_player_layout">Esquema alternativo do reprodutor</string>
<string name="alternative_player_layout_summary">Exibir os vídeos relacionados como uma linha acima dos comentários em vez de abaixo.</string>
<string name="audio_track">Faixa de áudio</string>
<string name="default_audio_track">Padrão</string>
<string name="hls_instead_of_dash">Usar HLS</string>
<string name="hls_instead_of_dash_summary">Usar HLS em vez de DASH (será mais lento, não recomendado)</string>
<string name="auto_quality">Automático</string>

View File

@ -312,7 +312,6 @@
<string name="alternative_player_layout">Esquema alternativo de reprodução</string>
<string name="alternative_player_layout_summary">Mostrar os vídeos relacionados como uma linha acima dos comentários em vez de abaixo.</string>
<string name="audio_track">Faixa de áudio</string>
<string name="default_audio_track">Padrão</string>
<string name="hls_instead_of_dash">Usar HLS</string>
<string name="hls_instead_of_dash_summary">Usar HLS em vez de DASH (será mais lento, não recomendado)</string>
<string name="auto_quality">Automático</string>

View File

@ -339,7 +339,6 @@
<string name="start_time">Timpul de începere</string>
<string name="end_time">Timpul de sfârșit</string>
<string name="notification_time">Restricționați timpul de notificare</string>
<string name="default_audio_track">Implicit</string>
<string name="unsupported_file_format">Format de fișier nesuportat: %1$s</string>
<string name="hls_instead_of_dash">Utilizați HLS</string>
<string name="hls_instead_of_dash_summary">Utilizați HLS în loc de DASH (va fi mai lent, nu este recomandat)</string>

View File

@ -311,7 +311,6 @@
<string name="layout">Макет</string>
<string name="alternative_player_layout">Альтернативный макет плеера</string>
<string name="audio_track">Аудио трек</string>
<string name="default_audio_track">По умолчанию</string>
<string name="hls_instead_of_dash">Использовать HLS</string>
<string name="hls_instead_of_dash_summary">Использовать HLS вместо DASH (будет медленнее, не рекомендуется)</string>
<string name="auto_quality">Авто</string>

View File

@ -223,7 +223,6 @@
<string name="navbar_order">පිළිවෙල</string>
<string name="layout">පිරිසැලසුම</string>
<string name="audio_track">ශ්‍රව්‍ය පථය</string>
<string name="default_audio_track">පෙරනිමිය</string>
<string name="hls_instead_of_dash">HLS භාවිතා කරන්න</string>
<string name="auto_quality">ස්වයං</string>
<string name="limit_to_runtime">ධාවන කාලයට සීමා කරන්න</string>

View File

@ -308,7 +308,6 @@
<string name="sb_markers">Маркери</string>
<string name="sb_markers_summary">Означите сегменте на временској траци.</string>
<string name="audio_track">Аудио запис</string>
<string name="default_audio_track">Подразумевано</string>
<string name="livestreams">Стримови уживо</string>
<string name="alternative_videos_layout">Алтернативни изглед видео записа</string>
<string name="defaultIconLight">Подразумевано светло</string>

View File

@ -311,7 +311,6 @@
<string name="alternative_player_layout_summary">İlgili videoları, yorumların altında değil, üstünde bir satır olarak göster.</string>
<string name="navbar_order">Sıra</string>
<string name="audio_track">Müzik parçası</string>
<string name="default_audio_track">Varsayılan</string>
<string name="auto_quality">Otomatik</string>
<string name="hls_instead_of_dash">HLS\'yi kullan</string>
<string name="hls_instead_of_dash_summary">DASH yerine HLS kullan (daha yavaş olacaktır, önerilmez)</string>

View File

@ -311,7 +311,6 @@
<string name="layout">Вигляд</string>
<string name="alternative_player_layout">Альтернативний вигляд програвача</string>
<string name="audio_track">Звукова доріжка</string>
<string name="default_audio_track">Типово</string>
<string name="hls_instead_of_dash">Використовувати HLS</string>
<string name="hls_instead_of_dash_summary">Використовувати HLS замість DASH (працюватиме повільніше, не рекомендується)</string>
<string name="auto_quality">Авто</string>

View File

@ -310,7 +310,6 @@
<string name="sb_markers_summary">Đánh dấu các phân đoạn trên thanh thời lượng.</string>
<string name="sb_markers">Đánh dấu</string>
<string name="audio_track">Track âm thanh</string>
<string name="default_audio_track">Mặc định</string>
<string name="livestreams">Trực tiếp</string>
<string name="alternative_videos_layout">Bố cục thay thế cho videos</string>
<string name="defaultIconLight">Light mặc định</string>

View File

@ -311,7 +311,6 @@
<string name="alternative_player_layout">备选的播放器布局</string>
<string name="alternative_player_layout_summary">将相关视频显示为评论上方而不是下方的一行。</string>
<string name="audio_track">音轨</string>
<string name="default_audio_track">默认</string>
<string name="hls_instead_of_dash">使用 HLS</string>
<string name="hls_instead_of_dash_summary">使用 HLS 而不是 DASH会更慢不推荐</string>
<string name="auto_quality">自动</string>

View File

@ -327,7 +327,6 @@
<string name="pinch_control">調整音高</string>
<string name="play_latest_videos">播放最新影片</string>
<string name="audio_track">音訊曲目</string>
<string name="default_audio_track">預設</string>
<string name="export_subscriptions">匯出訂閱列表</string>
<string name="skip_buttons">跳過按鈕</string>
<string name="no_search_result">無結果。</string>

View File

@ -330,7 +330,6 @@
<string name="alternative_player_layout">Alternative player layout</string>
<string name="alternative_player_layout_summary">Show the related videos as a row above the comments instead of below.</string>
<string name="audio_track">Audio track</string>
<string name="default_audio_track">Default</string>
<string name="unsupported_file_format">Unsupported file format: %1$s</string>
<string name="hls_instead_of_dash">Use HLS</string>
<string name="hls_instead_of_dash_summary">Use HLS instead of DASH (will be slower, not recommended)</string>
@ -445,6 +444,15 @@
<string name="import_format_youtube_csv">YouTube (CSV)</string>
<string name="home_tab_content">Home tab content</string>
<string name="show_search_suggestions">Show search suggestions</string>
<string name="audio_track_format">%1$s - %2$s</string>
<string name="unknown_audio_language">Unknown audio language</string>
<string name="unknown_audio_track_type">Unknown audio track type</string>
<string name="original_or_main_audio_track">original or main</string>
<string name="dubbed_audio_track">dubbed</string>
<string name="descriptive_audio_track">descriptive</string>
<string name="default_or_unknown_audio_track">default or unknown</string>
<string name="unknown_or_no_audio">unknown or no audio</string>
<!-- Notification channel strings -->
<string name="download_channel_name">Download Service</string>
<string name="download_channel_description">Shows a notification when downloading media.</string>