Skip to content

Commit

Permalink
Merge pull request #4053 from Bnyro/fallback-piped-proxy
Browse files Browse the repository at this point in the history
Option to fall back to Piped proxy when Piped proxy disabled and video unreachable
  • Loading branch information
Bnyro authored Jul 1, 2023
2 parents 19fc3fa + 10c9978 commit 78595ad
Show file tree
Hide file tree
Showing 11 changed files with 127 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ data class PipedStream(
videoId = videoId,
fileName = getQualityString(fileName),
path = Paths.get(""),
url = url?.let { ProxyHelper.unwrapIfEnabled(it) },
url = url?.let { ProxyHelper.unwrapUrl(it) },
format = format,
quality = quality,
downloadSize = contentLength
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/com/github/libretube/api/obj/Streams.kt
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ data class Streams(
path = Paths.get(""),
url = subtitles.find {
it.code == subCode
}?.url?.let { ProxyHelper.unwrapIfEnabled(it) }
}?.url?.let { ProxyHelper.unwrapUrl(it) }
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ object PreferenceKeys {
const val UNLIMITED_SEARCH_HISTORY = "unlimited_search_history"
const val SB_HIGHLIGHTS = "sb_highlights"
const val SHOW_TIME_LEFT = "show_time_left"
const val FALLBACK_PIPED_PROXY = "fallback_piped_proxy"

/**
* Background mode
Expand Down
27 changes: 20 additions & 7 deletions app/src/main/java/com/github/libretube/helpers/DashHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ object DashHelper {
val formats: MutableList<PipedStream> = mutableListOf()
)

fun createManifest(streams: Streams, supportsHdr: Boolean, audioOnly: Boolean = false): String {
fun createManifest(
streams: Streams,
supportsHdr: Boolean,
audioOnly: Boolean = false,
rewriteUrls: Boolean
): String {
val builder = builderFactory.newDocumentBuilder()

val doc = builder.newDocument()
Expand Down Expand Up @@ -113,9 +118,9 @@ object DashHelper {
for (stream in adapSet.formats) {
val rep = let {
if (isVideo) {
createVideoRepresentation(doc, stream)
createVideoRepresentation(doc, stream, rewriteUrls)
} else {
createAudioRepresentation(doc, stream)
createAudioRepresentation(doc, stream, rewriteUrls)
}
}
adapSetElement.appendChild(rep)
Expand All @@ -137,7 +142,11 @@ object DashHelper {
return writer.toString()
}

private fun createAudioRepresentation(doc: Document, stream: PipedStream): Element {
private fun createAudioRepresentation(
doc: Document,
stream: PipedStream,
rewriteUrls: Boolean
): Element {
val representation = doc.createElement("Representation")
representation.setAttribute("bandwidth", stream.bitrate.toString())
representation.setAttribute("codecs", stream.codec!!)
Expand All @@ -151,7 +160,7 @@ object DashHelper {
audioChannelConfiguration.setAttribute("value", "2")

val baseUrl = doc.createElement("BaseURL")
baseUrl.appendChild(doc.createTextNode(ProxyHelper.unwrapIfEnabled(stream.url!!)))
baseUrl.appendChild(doc.createTextNode(ProxyHelper.unwrapUrl(stream.url!!, rewriteUrls)))

val segmentBase = doc.createElement("SegmentBase")
segmentBase.setAttribute("indexRange", "${stream.indexStart}-${stream.indexEnd}")
Expand All @@ -167,7 +176,11 @@ object DashHelper {
return representation
}

private fun createVideoRepresentation(doc: Document, stream: PipedStream): Element {
private fun createVideoRepresentation(
doc: Document,
stream: PipedStream,
rewriteUrls: Boolean
): Element {
val representation = doc.createElement("Representation")
representation.setAttribute("codecs", stream.codec!!)
representation.setAttribute("bandwidth", stream.bitrate.toString())
Expand All @@ -177,7 +190,7 @@ object DashHelper {
representation.setAttribute("frameRate", stream.fps.toString())

val baseUrl = doc.createElement("BaseURL")
baseUrl.appendChild(doc.createTextNode(ProxyHelper.unwrapIfEnabled(stream.url!!)))
baseUrl.appendChild(doc.createTextNode(ProxyHelper.unwrapUrl(stream.url!!, rewriteUrls)))

val segmentBase = doc.createElement("SegmentBase")
segmentBase.setAttribute("indexRange", "${stream.indexStart}-${stream.indexEnd}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ object ImageHelper {
fun loadImage(url: String?, target: ImageView) {
// only load the image if the data saver mode is disabled
if (DataSaverMode.isEnabled(target.context) || url == null) return
val urlToLoad = ProxyHelper.unwrapIfEnabled(url)
val urlToLoad = ProxyHelper.unwrapImageUrl(url)
target.load(urlToLoad, imageLoader)
}

Expand Down
15 changes: 12 additions & 3 deletions app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,18 @@ object PlayerHelper {
/**
* Create a base64 encoded DASH stream manifest
*/
fun createDashSource(streams: Streams, context: Context, audioOnly: Boolean = false): Uri {
val supportsHdr = DisplayHelper.supportsHdr(context)
val manifest = DashHelper.createManifest(streams, supportsHdr, audioOnly)
fun createDashSource(
streams: Streams,
context: Context,
audioOnly: Boolean = false,
disableProxy: Boolean
): Uri {
val manifest = DashHelper.createManifest(
streams,
DisplayHelper.supportsHdr(context),
audioOnly,
disableProxy
)

// encode to base64
val encoded = Base64.encodeToString(manifest.toByteArray(), Base64.DEFAULT)
Expand Down
52 changes: 42 additions & 10 deletions app/src/main/java/com/github/libretube/helpers/ProxyHelper.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package com.github.libretube.helpers

import com.github.libretube.api.CronetHelper
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.constants.PreferenceKeys
import java.net.HttpURLConnection
import java.net.URL
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull

object ProxyHelper {
Expand All @@ -29,23 +33,51 @@ object ProxyHelper {
}

/**
* Load the YT url directly instead of the proxied Piped URL if enabled
* Detect whether the proxy should be used or not for a given stream URL based on user preferences
*/
fun unwrapIfEnabled(url: String): String {
if (!PreferenceHelper.getBoolean(
PreferenceKeys.DISABLE_VIDEO_IMAGE_PROXY,
false
)
) {
return url
}
suspend fun unwrapStreamUrl(url: String): String {
return if (shouldDisableProxy(url)) unwrapUrl(url) else url
}

suspend fun shouldDisableProxy(url: String) = when {
!PreferenceHelper.getBoolean(PreferenceKeys.DISABLE_VIDEO_IMAGE_PROXY, false) -> false
PreferenceHelper.getBoolean(PreferenceKeys.FALLBACK_PIPED_PROXY, true) -> isUrlUsable(
unwrapUrl(url)
)

else -> true
}

fun unwrapImageUrl(url: String) = if (
!PreferenceHelper.getBoolean(PreferenceKeys.DISABLE_VIDEO_IMAGE_PROXY, false)
) {
url
} else {
unwrapUrl(url)
}

return url.toHttpUrlOrNull()?.let {
/**
* Convert a proxied Piped url to a YouTube url that's not proxied
*/
fun unwrapUrl(url: String, unwrap: Boolean = true) = url.toHttpUrlOrNull()
?.takeIf { unwrap }?.let {
it.newBuilder()
.host(it.queryParameter("host").orEmpty())
.removeAllQueryParameters("host")
.build()
.toString()
} ?: url

/**
* Parse the Piped url to a YouTube url (or not) based on preferences
*/
private suspend fun isUrlUsable(url: String): Boolean = withContext(Dispatchers.IO) {
return@withContext runCatching {
val connection = CronetHelper.cronetEngine.openConnection(URL(url)) as HttpURLConnection
connection.requestMethod = "HEAD"
val isSuccess = connection.responseCode == 200
connection.disconnect()
return@runCatching isSuccess
}.getOrDefault(false)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ class OnlinePlayerService : LifecycleService() {

private fun playAudio(seekToPosition: Long) {
initializePlayer()
setMediaItem()
lifecycleScope.launch(Dispatchers.IO) { setMediaItem() }

// create the notification
if (!this@OnlinePlayerService::nowPlayingNotification.isInitialized) {
Expand Down Expand Up @@ -296,21 +296,27 @@ class OnlinePlayerService : LifecycleService() {
/**
* Sets the [MediaItem] with the [streams] into the [player]
*/
private fun setMediaItem() {
private suspend fun setMediaItem() {
val streams = streams ?: return

val (uri, mimeType) = if (streams.audioStreams.isNotEmpty()) {
PlayerHelper.createDashSource(streams, this, true) to MimeTypes.APPLICATION_MPD
val disableProxy = ProxyHelper.shouldDisableProxy(streams.videoStreams.first().url!!)
PlayerHelper.createDashSource(
streams,
this,
true,
disableProxy
) to MimeTypes.APPLICATION_MPD
} else {
ProxyHelper.rewriteUrl(streams.hls)?.toUri() to MimeTypes.APPLICATION_M3U8
ProxyHelper.unwrapStreamUrl(streams.hls.orEmpty()).toUri() to MimeTypes.APPLICATION_M3U8
}

val mediaItem = MediaItem.Builder()
.setUri(uri)
.setMimeType(mimeType)
.setMetadata(streams)
.build()
player?.setMediaItem(mediaItem)
withContext(Dispatchers.Main) { player?.setMediaItem(mediaItem) }
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
*/
private var sId: Int = 0
private var eId: Int = 0
private var transitioning = false
private var transitioning = true

/**
* for the player
Expand Down Expand Up @@ -454,9 +454,8 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
}

// start an intent with video as mimetype using the hls stream
val uri = Uri.parse(ProxyHelper.unwrapIfEnabled(streams.hls!!))
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, "video/*")
setDataAndType(streams.hls.orEmpty().toUri(), "video/*")
putExtra(Intent.EXTRA_TITLE, streams.title)
putExtra("title", streams.title)
putExtra("artist", streams.uploader)
Expand Down Expand Up @@ -1273,7 +1272,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
updateCaptionsLanguage(captionLanguage)

// set media source and resolution in the beginning
setStreamSource()
lifecycleScope.launch(Dispatchers.IO) { setStreamSource() }
}

private fun setPlayerResolution(resolution: Int) {
Expand All @@ -1283,11 +1282,11 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
}
}

private fun setStreamSource() {
private suspend fun setStreamSource() {
val defaultResolution = PlayerHelper.getDefaultResolution(requireContext()).replace("p", "")
if (defaultResolution.isNotEmpty()) setPlayerResolution(defaultResolution.toInt())

when {
val (uri, mimeType) = when {
// LBRY HLS
PreferenceHelper.getBoolean(
PreferenceKeys.LBRY_HLS,
Expand All @@ -1298,34 +1297,40 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
val lbryHlsUrl = streams.videoStreams.first {
it.quality!!.contains("LBRY HLS")
}.url!!
setMediaSource(lbryHlsUrl.toUri(), MimeTypes.APPLICATION_M3U8)
lbryHlsUrl.toUri() to MimeTypes.APPLICATION_M3U8
}
// DASH
!PreferenceHelper.getBoolean(
PreferenceKeys.USE_HLS_OVER_DASH,
false
) && streams.videoStreams.isNotEmpty() -> {
// only use the dash manifest generated by YT if either it's a livestream or no other source is available
val uri = streams.dash?.let { ProxyHelper.unwrapIfEnabled(it) }?.toUri().takeIf {
streams.livestream || streams.videoStreams.isEmpty()
} ?: let {
PlayerHelper.createDashSource(streams, requireContext())
}
val dashUri =
if (streams.livestream && streams.dash != null) ProxyHelper.unwrapStreamUrl(
streams.dash!!
).toUri() else {
val shouldDisableProxy =
ProxyHelper.shouldDisableProxy(streams.videoStreams.first().url!!)
PlayerHelper.createDashSource(
streams,
requireContext(),
disableProxy = shouldDisableProxy
)
}

this.setMediaSource(uri, MimeTypes.APPLICATION_MPD)
dashUri to MimeTypes.APPLICATION_MPD
}
// HLS
streams.hls != null -> {
setMediaSource(
ProxyHelper.unwrapIfEnabled(streams.hls!!).toUri(),
MimeTypes.APPLICATION_M3U8
)
ProxyHelper.unwrapStreamUrl(streams.hls!!).toUri() to MimeTypes.APPLICATION_M3U8
}
// NO STREAM FOUND
else -> {
Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show()
context?.toastFromMainDispatcher(R.string.unknown_error)
return
}
}
withContext(Dispatchers.Main) { setMediaSource(uri, mimeType) }
}

private fun createExoPlayer() {
Expand Down
10 changes: 6 additions & 4 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@
<string name="lbry_hls">LBRY HLS</string>
<string name="lbry_hls_summary">Use LBRY HLS for streaming if available.</string>
<string name="disable_proxy">Disable Piped proxy</string>
<string name="disable_proxy_summary">Load videos and images directly from YouTube\'s servers. Only enable the option if you use a VPN anyways! Note that this might not work with content from YT music.</string>
<string name="disable_proxy_summary">Load videos and images directly from YouTube\'s servers. Only enable the option if you use a VPN anyways!</string>
<string name="auto_fullscreen_shorts">Auto fullscreen on short videos</string>
<string name="channel_groups">Channel groups</string>
<string name="new_group">New</string>
Expand All @@ -428,10 +428,12 @@
<string name="change_playlist_description">Change playlist description</string>
<string name="playlist_description">Playlist description</string>
<string name="emptyPlaylistDescription">The playlist description can\'t be empty</string>
<string name="off">Disable</string>
<string name="off">Off</string>
<string name="manual">Manual</string>
<string name="automatic">Automatic</string>
<string name="visible">Show in seek bar</string>
<string name="manual">Manual skip</string>
<string name="automatic">Auto skip</string>
<string name="fallback_piped_proxy">Fallback to Piped proxy</string>
<string name="fallback_piped_proxy_desc">Load videos via the proxy if connecting to YouTube directly doesn\'t work for the current video (increases the initial loading times). If disabled, YouTube music content likely won\'t play due to YT restrictions.</string>

<!-- Backup & Restore Settings -->
<string name="import_subscriptions_from">Import subscriptions from</string>
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/res/xml/audio_video_settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,15 @@
android:title="@string/disable_proxy"
app:key="disable_video_image_proxy" />

<SwitchPreferenceCompat
android:defaultValue="false"
android:dependency="disable_video_image_proxy"
android:icon="@drawable/ic_music"
android:summary="@string/fallback_piped_proxy_desc"
android:title="@string/fallback_piped_proxy"
app:defaultValue="true"
app:key="fallback_piped_proxy" />

</PreferenceCategory>

</PreferenceScreen>

0 comments on commit 78595ad

Please sign in to comment.