mirror of
https://github.com/libre-tube/LibreTube.git
synced 2024-12-14 14:20:30 +05:30
Merge pull request #1867 from FireMasterK/dash-support
Implement proper dash support
This commit is contained in:
commit
b4213d9449
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
package com.github.libretube.obj
|
||||||
|
|
||||||
|
data class VideoResolution(
|
||||||
|
val name: String,
|
||||||
|
val resolution: Int? = null,
|
||||||
|
val adaptiveSourceUrl: String? = null
|
||||||
|
)
|
@ -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)
|
||||||
}
|
}
|
||||||
|
174
app/src/main/java/com/github/libretube/util/DashHelper.kt
Normal file
174
app/src/main/java/com/github/libretube/util/DashHelper.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
||||||
|
@ -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" }
|
||||||
|
Loading…
Reference in New Issue
Block a user