mirror of
https://github.com/libre-tube/LibreTube.git
synced 2024-12-14 22:30:30 +05:30
Merge pull request #4240 from Bnyro/audio-track-types
Support for different audio track types
This commit is contained in:
commit
1fd905222d
@ -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." +
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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 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 +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() {
|
||||
|
@ -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,
|
||||
|
@ -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=(.[^:]+)")
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user