Merge pull request #1867 from FireMasterK/dash-support

Implement proper dash support
This commit is contained in:
Kavin 2022-11-16 18:32:53 +00:00 committed by GitHub
commit b4213d9449
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 279 additions and 141 deletions

View File

@ -103,6 +103,7 @@ dependencies {
implementation libs.exoplayer implementation libs.exoplayer
implementation(libs.exoplayer.extension.cronet) { exclude group: 'com.google.android.gms' } implementation(libs.exoplayer.extension.cronet) { exclude group: 'com.google.android.gms' }
implementation libs.exoplayer.extension.mediasession implementation libs.exoplayer.extension.mediasession
implementation libs.exoplayer.dash
/* Retrofit and Jackson */ /* Retrofit and Jackson */
implementation libs.square.retrofit implementation libs.square.retrofit

View File

@ -78,9 +78,9 @@ object PreferenceKeys {
const val PLAYER_RESIZE_MODE = "player_resize_mode" const val PLAYER_RESIZE_MODE = "player_resize_mode"
const val SB_SKIP_MANUALLY = "sb_skip_manually_key" const val SB_SKIP_MANUALLY = "sb_skip_manually_key"
const val SB_SHOW_MARKERS = "sb_show_markers" const val SB_SHOW_MARKERS = "sb_show_markers"
const val LIMIT_HLS = "limit_hls"
const val PROGRESSIVE_LOADING_INTERVAL_SIZE = "progressive_loading_interval" const val PROGRESSIVE_LOADING_INTERVAL_SIZE = "progressive_loading_interval"
const val ALTERNATIVE_PLAYER_LAYOUT = "alternative_player_layout" const val ALTERNATIVE_PLAYER_LAYOUT = "alternative_player_layout"
const val USE_HLS_OVER_DASH = "use_hls"
/** /**
* Background mode * Background mode

View File

@ -0,0 +1,7 @@
package com.github.libretube.obj
data class VideoResolution(
val name: String,
val resolution: Int? = null,
val adaptiveSourceUrl: String? = null
)

View File

@ -18,6 +18,7 @@ import android.os.Looper
import android.os.PowerManager import android.os.PowerManager
import android.text.Html import android.text.Html
import android.text.format.DateUtils import android.text.format.DateUtils
import android.util.Base64
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
@ -59,6 +60,7 @@ import com.github.libretube.extensions.query
import com.github.libretube.extensions.toID import com.github.libretube.extensions.toID
import com.github.libretube.extensions.toStreamItem import com.github.libretube.extensions.toStreamItem
import com.github.libretube.obj.ShareData import com.github.libretube.obj.ShareData
import com.github.libretube.obj.VideoResolution
import com.github.libretube.services.BackgroundMode import com.github.libretube.services.BackgroundMode
import com.github.libretube.services.DownloadService import com.github.libretube.services.DownloadService
import com.github.libretube.ui.activities.MainActivity import com.github.libretube.ui.activities.MainActivity
@ -75,6 +77,7 @@ import com.github.libretube.ui.models.PlayerViewModel
import com.github.libretube.ui.sheets.BaseBottomSheet import com.github.libretube.ui.sheets.BaseBottomSheet
import com.github.libretube.ui.sheets.PlayingQueueSheet import com.github.libretube.ui.sheets.PlayingQueueSheet
import com.github.libretube.util.BackgroundHelper import com.github.libretube.util.BackgroundHelper
import com.github.libretube.util.DashHelper
import com.github.libretube.util.ImageHelper import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NowPlayingNotification import com.github.libretube.util.NowPlayingNotification
import com.github.libretube.util.PlayerHelper import com.github.libretube.util.PlayerHelper
@ -86,22 +89,17 @@ import com.google.android.exoplayer2.DefaultLoadControl
import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.MediaItem.SubtitleConfiguration import com.google.android.exoplayer2.MediaItem.SubtitleConfiguration
import com.google.android.exoplayer2.MediaItem.fromUri
import com.google.android.exoplayer2.PlaybackException import com.google.android.exoplayer2.PlaybackException
import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.audio.AudioAttributes import com.google.android.exoplayer2.audio.AudioAttributes
import com.google.android.exoplayer2.ext.cronet.CronetDataSource import com.google.android.exoplayer2.ext.cronet.CronetDataSource
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.MergingMediaSource
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.text.Cue.TEXT_SIZE_TYPE_ABSOLUTE import com.google.android.exoplayer2.text.Cue.TEXT_SIZE_TYPE_ABSOLUTE
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.ui.CaptionStyleCompat import com.google.android.exoplayer2.ui.CaptionStyleCompat
import com.google.android.exoplayer2.ui.StyledPlayerView import com.google.android.exoplayer2.ui.StyledPlayerView
import com.google.android.exoplayer2.upstream.DataSource
import com.google.android.exoplayer2.upstream.DefaultDataSource import com.google.android.exoplayer2.upstream.DefaultDataSource
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource import com.google.android.exoplayer2.util.MimeTypes
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -169,9 +167,6 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
private lateinit var shareData: ShareData private lateinit var shareData: ShareData
private var selectedAudioSourceUrl: String? = null
private var selectedVideoSourceUrl: String? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
arguments?.let { arguments?.let {
@ -941,7 +936,10 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
} }
// update the subscribed state // update the subscribed state
binding.playerSubscribe.setupSubscriptionButton(streams.uploaderUrl?.toID(), streams.uploader) binding.playerSubscribe.setupSubscriptionButton(
streams.uploaderUrl?.toID(),
streams.uploader
)
if (token != "") { if (token != "") {
binding.relPlayerSave.setOnClickListener { binding.relPlayerSave.setOnClickListener {
@ -1072,67 +1070,24 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
return chapterIndex return chapterIndex
} }
private fun setMediaSource( private fun setMediaSource(uri: Uri, mimeType: String) {
videoUrl: String,
audioUrl: String
) {
val checkIntervalSize = when (PlayerHelper.progressiveLoadingIntervalSize) {
"default" -> ProgressiveMediaSource.DEFAULT_LOADING_CHECK_INTERVAL_BYTES
else -> PlayerHelper.progressiveLoadingIntervalSize.toInt() * 1024
}
val dataSourceFactory: DataSource.Factory =
DefaultHttpDataSource.Factory()
val videoItem: MediaItem = MediaItem.Builder()
.setUri(videoUrl.toUri())
.setSubtitleConfigurations(subtitles)
.build()
val videoSource: MediaSource =
ProgressiveMediaSource.Factory(dataSourceFactory)
.setContinueLoadingCheckIntervalBytes(checkIntervalSize)
.createMediaSource(videoItem)
val audioSource: MediaSource =
ProgressiveMediaSource.Factory(dataSourceFactory)
.setContinueLoadingCheckIntervalBytes(checkIntervalSize)
.createMediaSource(fromUri(audioUrl))
val mergeSource: MediaSource =
MergingMediaSource(videoSource, audioSource)
exoPlayer.setMediaSource(mergeSource)
}
private fun setHLSMediaSource(uri: Uri) {
val mediaItem: MediaItem = MediaItem.Builder() val mediaItem: MediaItem = MediaItem.Builder()
.setUri(uri) .setUri(uri)
.setMimeType(mimeType)
.setSubtitleConfigurations(subtitles) .setSubtitleConfigurations(subtitles)
.build() .build()
exoPlayer.setMediaItem(mediaItem) exoPlayer.setMediaItem(mediaItem)
} }
private fun getAvailableResolutions(): Pair<Array<String>, Array<String>> { private fun getAvailableResolutions(): List<VideoResolution> {
if (!this::streams.isInitialized) return Pair(arrayOf(), arrayOf()) if (!this::streams.isInitialized) return listOf()
var videosNameArray: Array<String> = arrayOf() val resolutions = mutableListOf<VideoResolution>()
var videosUrlArray: Array<String> = arrayOf()
// append hls to list if available
if (streams.hls != null) {
videosNameArray += getString(R.string.hls)
videosUrlArray += streams.hls!!
}
val videoStreams = try { val videoStreams = try {
// attempt to sort the qualities, catch if there was an error ih parsing // attempt to sort the qualities, catch if there was an error ih parsing
streams.videoStreams?.sortedBy { streams.videoStreams?.sortedBy {
it.quality it.quality?.toLong() ?: 0L
.toString()
.split("p")
.first()
.replace("p", "")
.toLong()
}?.reversed() }?.reversed()
.orEmpty() .orEmpty()
} catch (_: Exception) { } catch (_: Exception) {
@ -1142,21 +1097,44 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
for (vid in videoStreams) { for (vid in videoStreams) {
// append quality to list if it has the preferred format (e.g. MPEG) // append quality to list if it has the preferred format (e.g. MPEG)
val preferredMimeType = "video/${PlayerHelper.videoFormatPreference}" val preferredMimeType = "video/${PlayerHelper.videoFormatPreference}"
if (vid.url != null && vid.mimeType == preferredMimeType) { // preferred format if (vid.url != null && vid.mimeType == preferredMimeType) {
videosNameArray += vid.quality.toString() // avoid duplicated resolutions
videosUrlArray += vid.url!! if (resolutions.any {
} else if (vid.quality.equals("LBRY") && vid.format.equals("MP4")) { // LBRY MP4 format it.resolution == vid.quality.toString().split("p").first().toInt()
videosNameArray += "LBRY MP4" }
videosUrlArray += vid.url!! ) {
continue
}
resolutions.add(
VideoResolution(
name = vid.quality!!,
resolution = vid.quality.toString().split("p").first().toInt()
)
)
} else if (vid.quality.equals("LBRY") && vid.format.equals("MP4")) {
resolutions.add(
VideoResolution(
name = "LBRY MP4",
adaptiveSourceUrl = vid.url,
resolution = Int.MAX_VALUE
)
)
} }
} }
return Pair(videosNameArray, videosUrlArray)
if (resolutions.isEmpty()) {
return listOf(
VideoResolution(getString(R.string.hls), adaptiveSourceUrl = streams.hls)
)
}
resolutions.add(0, VideoResolution(getString(R.string.auto_quality), Int.MAX_VALUE))
return resolutions
} }
private fun setResolutionAndSubtitles() { private fun setResolutionAndSubtitles() {
// get the available resolutions
val (videosNameArray, videosUrlArray) = getAvailableResolutions()
// create a list of subtitles // create a list of subtitles
subtitles = mutableListOf() subtitles = mutableListOf()
val subtitlesNamesList = mutableListOf(context?.getString(R.string.none)!!) val subtitlesNamesList = mutableListOf(context?.getString(R.string.none)!!)
@ -1186,51 +1164,34 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
// set media source and resolution in the beginning // set media source and resolution in the beginning
setStreamSource( setStreamSource(
streams, streams
videosNameArray,
videosUrlArray
) )
} }
private fun setStreamSource( private fun setStreamSource(streams: Streams) {
streams: Streams, val defaultResolution = PlayerHelper.getDefaultResolution(requireContext()).replace("p", "")
videosNameArray: Array<String>,
videosUrlArray: Array<String>
) {
val defaultResolution = PlayerHelper.getDefaultResolution(requireContext())
if (defaultResolution != "") { if (defaultResolution != "") {
videosNameArray.forEachIndexed { index, pipedStream -> val params = trackSelector.buildUponParameters()
// search for quality preference in the available stream sources .setMaxVideoSize(Int.MAX_VALUE, defaultResolution.toInt())
if (pipedStream.contains(defaultResolution)) { .setMinVideoSize(Int.MAX_VALUE, defaultResolution.toInt())
selectedVideoSourceUrl = videosUrlArray[index] trackSelector.setParameters(params)
selectedAudioSourceUrl = selectedAudioSourceUrl ?: getAudioSource(streams.audioStreams)
setMediaSource(selectedAudioSourceUrl!!, selectedVideoSourceUrl!!)
return
}
}
} }
// if default resolution isn't set or available, use hls if available if (!PreferenceHelper.getBoolean(PreferenceKeys.USE_HLS_OVER_DASH, false) &&
if (streams.hls != null) { streams.videoStreams.orEmpty().isNotEmpty()
setHLSMediaSource(Uri.parse(streams.hls)) ) {
return val manifest = DashHelper.createManifest(streams)
}
// if nothing found, use the first list entry // encode to base64
if (videosUrlArray.isNotEmpty()) { val encoded = Base64.encodeToString(manifest.toByteArray(), Base64.DEFAULT)
val videoUri = videosUrlArray[0] val mediaItem = "data:application/dash+xml;charset=utf-8;base64,$encoded"
val audioUrl = PlayerHelper.getAudioSource(requireContext(), streams.audioStreams!!)
setMediaSource(videoUri, audioUrl)
}
}
private fun getAudioSource(audioStreams: List<PipedStream>?): String { this.setMediaSource(mediaItem.toUri(), MimeTypes.APPLICATION_MPD)
val appLanguage = Locale.getDefault().language.lowercase().substring(0, 2) } else if (streams.hls != null) {
val filteredStreams = audioStreams.orEmpty().filter { it.audioTrackId?.contains(appLanguage) ?: false } setMediaSource(streams.hls.toUri(), MimeTypes.APPLICATION_M3U8)
return PlayerHelper.getAudioSource( } else {
requireContext(), Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show()
filteredStreams.ifEmpty { audioStreams!! } }
)
} }
private fun createExoPlayer() { private fun createExoPlayer() {
@ -1264,17 +1225,10 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
// control for the track sources like subtitles and audio source // control for the track sources like subtitles and audio source
trackSelector = DefaultTrackSelector(requireContext()) trackSelector = DefaultTrackSelector(requireContext())
// limit hls to full hd val params = trackSelector.buildUponParameters().setPreferredAudioLanguage(
if ( Locale.getDefault().language.lowercase().substring(0, 2)
PreferenceHelper.getBoolean( )
PreferenceKeys.LIMIT_HLS, trackSelector.setParameters(params)
false
)
) {
val newParams = trackSelector.buildUponParameters()
.setMaxVideoSize(1920, 1080)
trackSelector.setParameters(newParams)
}
exoPlayer = ExoPlayer.Builder(requireContext()) exoPlayer = ExoPlayer.Builder(requireContext())
.setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory)) .setMediaSourceFactory(DefaultMediaSourceFactory(dataSourceFactory))
@ -1396,26 +1350,19 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
override fun onQualityClicked() { override fun onQualityClicked() {
// get the available resolutions // get the available resolutions
val (videosNameArray, videosUrlArray) = getAvailableResolutions() val resolutions = getAvailableResolutions()
// Dialog for quality selection // Dialog for quality selection
val lastPosition = exoPlayer.currentPosition
BaseBottomSheet() BaseBottomSheet()
.setSimpleItems( .setSimpleItems(
videosNameArray.toList() resolutions.map { it.name }
) { which -> ) { which ->
if ( val resolution = resolutions[which].resolution!!
videosNameArray[which] == getString(R.string.hls) ||
videosNameArray[which] == "LBRY HLS" val params = trackSelector.buildUponParameters()
) { .setMaxVideoSize(Int.MAX_VALUE, resolution)
// set the progressive media source .setMinVideoSize(Int.MAX_VALUE, resolution)
setHLSMediaSource(videosUrlArray[which].toUri()) trackSelector.setParameters(params)
} else {
selectedVideoSourceUrl = videosUrlArray[which]
selectedAudioSourceUrl = selectedAudioSourceUrl ?: getAudioSource(streams.audioStreams)
setMediaSource(selectedVideoSourceUrl!!, selectedAudioSourceUrl!!)
}
exoPlayer.seekTo(lastPosition)
} }
.show(childFragmentManager) .show(childFragmentManager)
} }
@ -1432,9 +1379,10 @@ class PlayerFragment : BaseFragment(), OnlinePlayerOptions {
BaseBottomSheet() BaseBottomSheet()
.setSimpleItems(audioLanguages) { index -> .setSimpleItems(audioLanguages) { index ->
val audioStreams = audioGroups.values.elementAt(index) val audioStreams = audioGroups.values.elementAt(index)
selectedAudioSourceUrl = PlayerHelper.getAudioSource(requireContext(), audioStreams) val lang = audioStreams.first().audioTrackId!!.substring(0, 2)
selectedVideoSourceUrl = selectedVideoSourceUrl ?: streams.videoStreams!!.first().url!! val newParams = trackSelector.buildUponParameters()
setMediaSource(selectedAudioSourceUrl!!, selectedVideoSourceUrl!!) .setPreferredAudioLanguage(lang)
trackSelector.setParameters(newParams)
} }
.show(childFragmentManager) .show(childFragmentManager)
} }

View File

@ -0,0 +1,174 @@
package com.github.libretube.util
import com.github.libretube.api.obj.PipedStream
import com.github.libretube.api.obj.Streams
import org.w3c.dom.Document
import org.w3c.dom.Element
import java.io.StringWriter
import javax.xml.parsers.DocumentBuilder
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
// Based off of https://github.com/TeamPiped/Piped/blob/master/src/utils/DashUtils.js
object DashHelper {
private val builderFactory: DocumentBuilderFactory = DocumentBuilderFactory.newInstance()
private val transformerFactory: TransformerFactory = TransformerFactory.newInstance()
private data class AdapSetInfo(
val mimeType: String,
val audioTrackId: String? = null,
val formats: MutableList<PipedStream> = mutableListOf()
)
fun createManifest(streams: Streams): String {
val builder: DocumentBuilder = builderFactory.newDocumentBuilder()
val doc = builder.newDocument()
val mpd = doc.createElement("MPD")
mpd.setAttribute("xmlns", "urn:mpeg:dash:schema:mpd:2011")
mpd.setAttribute("profiles", "urn:mpeg:dash:profile:full:2011")
mpd.setAttribute("minBufferTime", "PT1.5S")
mpd.setAttribute("type", "static")
mpd.setAttribute("mediaPresentationDuration", "PT${streams.duration}S")
val period = doc.createElement("Period")
val adapSetInfos = ArrayList<AdapSetInfo>()
for (stream in streams.videoStreams!!) {
// ignore dual format streams
if (!stream.videoOnly!!) {
continue
}
val adapSetInfo = adapSetInfos.find { it.mimeType == stream.mimeType }
if (adapSetInfo != null) {
adapSetInfo.formats.add(stream)
continue
}
adapSetInfos.add(
AdapSetInfo(
stream.mimeType!!,
null,
mutableListOf(stream)
)
)
}
for (stream in streams.audioStreams!!) {
val adapSetInfo =
adapSetInfos.find { it.mimeType == stream.mimeType && it.audioTrackId == stream.audioTrackId }
if (adapSetInfo != null) {
adapSetInfo.formats.add(stream)
continue
}
adapSetInfos.add(
AdapSetInfo(
stream.mimeType!!,
stream.audioTrackId,
mutableListOf(stream)
)
)
}
for (adapSet in adapSetInfos) {
val adapSetElement = doc.createElement("AdaptationSet")
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))
}
val isVideo = adapSet.mimeType.contains("video")
if (isVideo) {
adapSetElement.setAttribute("scanType", "progressive")
}
for (stream in adapSet.formats) {
val rep = let {
if (isVideo) {
createVideoRepresentation(doc, stream)
} else {
createAudioRepresentation(doc, stream)
}
}
adapSetElement.appendChild(rep)
}
period.appendChild(adapSetElement)
}
mpd.appendChild(period)
doc.appendChild(mpd)
val domSource = DOMSource(doc)
val writer = StringWriter()
val transformer = transformerFactory.newTransformer()
transformer.transform(domSource, StreamResult(writer))
return writer.toString()
}
private fun createAudioRepresentation(doc: Document, stream: PipedStream): Element {
val representation = doc.createElement("Representation")
representation.setAttribute("bandwidth", stream.bitrate.toString())
representation.setAttribute("codecs", stream.codec!!)
representation.setAttribute("mimeType", stream.mimeType!!)
val audioChannelConfiguration = doc.createElement("AudioChannelConfiguration")
audioChannelConfiguration.setAttribute(
"schemeIdUri",
"urn:mpeg:dash:23003:3:audio_channel_configuration:2011"
)
audioChannelConfiguration.setAttribute("value", "2")
val baseUrl = doc.createElement("BaseURL")
baseUrl.appendChild(doc.createTextNode(stream.url!!))
val segmentBase = doc.createElement("SegmentBase")
segmentBase.setAttribute("indexRange", "${stream.indexStart}-${stream.indexEnd}")
val initialization = doc.createElement("Initialization")
initialization.setAttribute("range", "${stream.initStart}-${stream.initEnd}")
segmentBase.appendChild(initialization)
representation.appendChild(audioChannelConfiguration)
representation.appendChild(baseUrl)
representation.appendChild(segmentBase)
return representation
}
private fun createVideoRepresentation(doc: Document, stream: PipedStream): Element {
val representation = doc.createElement("Representation")
representation.setAttribute("codecs", stream.codec!!)
representation.setAttribute("bandwidth", stream.bitrate.toString())
representation.setAttribute("width", stream.width.toString())
representation.setAttribute("height", stream.height.toString())
representation.setAttribute("maxPlayoutRate", "1")
representation.setAttribute("frameRate", stream.fps.toString())
val baseUrl = doc.createElement("BaseURL")
baseUrl.appendChild(doc.createTextNode(stream.url!!))
val segmentBase = doc.createElement("SegmentBase")
segmentBase.setAttribute("indexRange", "${stream.indexStart}-${stream.indexEnd}")
val initialization = doc.createElement("Initialization")
initialization.setAttribute("range", "${stream.initStart}-${stream.initEnd}")
segmentBase.appendChild(initialization)
representation.appendChild(baseUrl)
representation.appendChild(segmentBase)
return representation
}
}

View File

@ -188,7 +188,7 @@
</string-array> </string-array>
<string-array name="defres"> <string-array name="defres">
<item>@string/hls</item> <item>@string/auto_quality</item>
<item>2160p</item> <item>2160p</item>
<item>1440p</item> <item>1440p</item>
<item>1080p</item> <item>1080p</item>

View File

@ -208,7 +208,7 @@
<string name="auth_instance">Authentication instance</string> <string name="auth_instance">Authentication instance</string>
<string name="auth_instance_summary">Use a different instance for authenticated calls.</string> <string name="auth_instance_summary">Use a different instance for authenticated calls.</string>
<string name="auth_instances">Choose an auth instance</string> <string name="auth_instances">Choose an auth instance</string>
<string name="hls">Auto</string> <string name="hls">HLS</string>
<string name="github">GitHub</string> <string name="github">GitHub</string>
<string name="audio_video">Audio and video</string> <string name="audio_video">Audio and video</string>
<string name="fullscreen_orientation">Fullscreen orientation</string> <string name="fullscreen_orientation">Fullscreen orientation</string>
@ -373,6 +373,9 @@
<string name="audio_track">Audio track</string> <string name="audio_track">Audio track</string>
<string name="default_audio_track">Default</string> <string name="default_audio_track">Default</string>
<string name="unsupported_file_format">Unsupported file format!</string> <string name="unsupported_file_format">Unsupported file format!</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>
<string name="auto_quality">Auto</string>
<!-- Notification channel strings --> <!-- Notification channel strings -->
<string name="download_channel_name">Download Service</string> <string name="download_channel_name">Download Service</string>

View File

@ -46,6 +46,7 @@
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory app:title="@string/quality"> <PreferenceCategory app:title="@string/quality">
<ListPreference <ListPreference
@ -53,6 +54,7 @@
app:defaultValue="webm" app:defaultValue="webm"
app:entries="@array/playerVideoFormat" app:entries="@array/playerVideoFormat"
app:entryValues="@array/playerVideoFormatValues" app:entryValues="@array/playerVideoFormatValues"
app:isPreferenceVisible="false"
app:key="player_video_format" app:key="player_video_format"
app:title="@string/playerVideoFormat" app:title="@string/playerVideoFormat"
app:useSimpleSummaryProvider="true" /> app:useSimpleSummaryProvider="true" />
@ -62,15 +64,17 @@
app:defaultValue="all" app:defaultValue="all"
app:entries="@array/playerAudioFormat" app:entries="@array/playerAudioFormat"
app:entryValues="@array/playerAudioFormatValues" app:entryValues="@array/playerAudioFormatValues"
app:isPreferenceVisible="false"
app:key="player_audio_format" app:key="player_audio_format"
app:title="@string/playerAudioFormat" app:title="@string/playerAudioFormat"
app:useSimpleSummaryProvider="true" /> app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="false" android:defaultValue="false"
android:icon="@drawable/ic_play_filled" android:icon="@drawable/ic_list"
android:title="@string/limit_hls" android:summary="@string/hls_instead_of_dash_summary"
app:key="limit_hls" /> android:title="@string/hls_instead_of_dash"
app:key="use_hls" />
<ListPreference <ListPreference
android:icon="@drawable/ic_loading" android:icon="@drawable/ic_loading"

View File

@ -41,6 +41,7 @@ square-retrofit-converterJackson = { group = "com.squareup.retrofit2", name = "c
jacksonAnnotations = { group = "com.fasterxml.jackson.core", name = "jackson-annotations", version.ref = "jacksonAnnotations" } jacksonAnnotations = { group = "com.fasterxml.jackson.core", name = "jackson-annotations", version.ref = "jacksonAnnotations" }
desugaring = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugaring" } desugaring = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugaring" }
exoplayer-extension-cronet = { group = "com.google.android.exoplayer", name = "extension-cronet", version.ref = "exoplayer" } exoplayer-extension-cronet = { group = "com.google.android.exoplayer", name = "extension-cronet", version.ref = "exoplayer" }
exoplayer-dash = { group = "com.google.android.exoplayer", name = "exoplayer-dash", version.ref = "exoplayer" }
cronet-embedded = { group = "org.chromium.net", name = "cronet-embedded", version.ref = "cronetEmbedded" } cronet-embedded = { group = "org.chromium.net", name = "cronet-embedded", version.ref = "cronetEmbedded" }
cronet-okhttp = { group = "com.google.net.cronet", name = "cronet-okhttp", version.ref = "cronetOkHttp" } cronet-okhttp = { group = "com.google.net.cronet", name = "cronet-okhttp", version.ref = "cronetOkHttp" }
coil = { group = "io.coil-kt", name = "coil", version.ref="coil" } coil = { group = "io.coil-kt", name = "coil", version.ref="coil" }