Skip to content

Commit

Permalink
feat: allow replace playurl PCDN (#1059)
Browse files Browse the repository at this point in the history
完善对视频 / 直播的 PCDN 拦截. 

Fix: #1021
  • Loading branch information
cxw620 committed May 31, 2023
1 parent 3aadb6c commit a5bdac9
Show file tree
Hide file tree
Showing 13 changed files with 405 additions and 161 deletions.
31 changes: 24 additions & 7 deletions app/src/main/java/me/iacn/biliroaming/SettingDialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import me.iacn.biliroaming.BiliBiliPackage.Companion.instance
import me.iacn.biliroaming.hook.JsonHook
import me.iacn.biliroaming.hook.SplashHook
import me.iacn.biliroaming.utils.*
import me.iacn.biliroaming.utils.UposReplaceHelper.isLocatedCn
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
Expand Down Expand Up @@ -68,8 +69,9 @@ class SettingDialog(context: Context) : AlertDialog.Builder(context) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
preferenceManager.sharedPreferencesName = "biliroaming"
addPreferencesFromResource(R.xml.prefs_setting)
prefs = preferenceManager.sharedPreferences
checkUposServer()
addPreferencesFromResource(R.xml.prefs_setting)
biliprefs = currentContext.getSharedPreferences(
packageName + "_preferences",
Context.MODE_MULTI_PROCESS
Expand Down Expand Up @@ -200,6 +202,18 @@ class SettingDialog(context: Context) : AlertDialog.Builder(context) {
listView.forceSetSelection(0)
}

private fun checkUposServer() {
val currentServer = prefs.getString("upos_host", null).orEmpty()
val serverList = context.resources.getStringArray(R.array.upos_values)
if (currentServer !in serverList) {
scope.launch(Dispatchers.IO) {
val defaultServer =
if (isLocatedCn) serverList[1] else """$1"""
prefs.edit().putString("upos_host", defaultServer).apply()
}
}
}

private fun checkUpdate() {
val url = URL(context.getString(R.string.version_url))
scope.launch {
Expand Down Expand Up @@ -390,9 +404,9 @@ class SettingDialog(context: Context) : AlertDialog.Builder(context) {
when (requestCode) {
PREF_IMPORT -> {
try {
file.outputStream().use { output ->
file.bufferedWriter().use { output ->
activity.contentResolver.openInputStream(uri)
?.use { it.copyTo(output) }
?.use { it.bufferedReader().copyTo(output) }
}
} catch (e: Exception) {
Log.toast(e.message ?: "未知错误", true, alsoLog = true)
Expand All @@ -402,9 +416,12 @@ class SettingDialog(context: Context) : AlertDialog.Builder(context) {

PREF_EXPORT -> {
try {
file.inputStream().use { input ->
activity.contentResolver.openOutputStream(uri)
?.use { input.copyTo(it) }
file.bufferedReader().use { input ->
activity.contentResolver.openOutputStream(uri)?.use {
it.bufferedWriter().use { output ->
input.copyTo(output)
}
}
}
} catch (e: Exception) {
Log.toast(e.message ?: "未知错误", true, alsoLog = true)
Expand Down Expand Up @@ -519,7 +536,7 @@ class SettingDialog(context: Context) : AlertDialog.Builder(context) {
}

private fun onTestUposClick(): Boolean {
SpeedTestDialog(findPreference("upos_host") as ListPreference, activity).show()
SpeedTestDialog(activity, prefs).show()
return true
}

Expand Down
13 changes: 6 additions & 7 deletions app/src/main/java/me/iacn/biliroaming/SpeedTestDialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ package me.iacn.biliroaming
import android.app.Activity
import android.app.AlertDialog
import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
import android.preference.ListPreference
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
Expand Down Expand Up @@ -53,7 +53,7 @@ class SpeedTestAdapter(context: Context) : ArrayAdapter<SpeedTestResult>(context
}
}

class SpeedTestDialog(private val pref: ListPreference, activity: Activity) :
class SpeedTestDialog(activity: Activity, prefs: SharedPreferences) :
AlertDialog.Builder(activity) {
private val scope = MainScope()
private val speedTestDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
Expand Down Expand Up @@ -82,13 +82,12 @@ class SpeedTestDialog(private val pref: ListPreference, activity: Activity) :
view.setOnItemClickListener { _, _, pos, _ ->
val (name, value, _) = adapter.getItem(pos - 1/*headerView*/)
?: return@setOnItemClickListener
Log.d("Use UPOS $name: $value")
pref.value = value
sPrefs.edit().putString(pref.key, value).apply()
Log.toast("已启用UPOS服务器:${name}", force = true)
Log.d("Use UPOS Server $name: $value")
prefs.edit().putString("upos_host", value).apply()
Log.toast("已启用 UPOS 服务器:${name}", force = true)
}

setTitle("CDN测速")
setTitle("CDN 测速")
}

override fun show(): AlertDialog {
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/me/iacn/biliroaming/XposedInit.kt
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ class XposedInit : IXposedHookLoadPackage, IXposedHookZygoteInit {
startHook(BangumiPageAdHook(lpparam.classLoader))
startHook(VideoQualityHook(lpparam.classLoader))
startHook(PublishToFollowingHook(lpparam.classLoader))
startHook(UposReplaceHook(lpparam.classLoader))
}

lpparam.processName.endsWith(":web") -> {
Expand Down
74 changes: 73 additions & 1 deletion app/src/main/java/me/iacn/biliroaming/hook/BangumiPlayUrlHook.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import me.iacn.biliroaming.network.BiliRoamingApi.CustomServerException
import me.iacn.biliroaming.network.BiliRoamingApi.getPlayUrl
import me.iacn.biliroaming.network.BiliRoamingApi.getSeason
import me.iacn.biliroaming.utils.*
import me.iacn.biliroaming.utils.UposReplaceHelper.enableUposReplace
import me.iacn.biliroaming.utils.UposReplaceHelper.ipPCdnRegex
import me.iacn.biliroaming.utils.UposReplaceHelper.reconstructVideoUpos
import me.iacn.biliroaming.utils.UposReplaceHelper.replaceUpos
import me.iacn.biliroaming.utils.UposReplaceHelper.videoUposBackups
import org.json.JSONObject
import java.net.HttpURLConnection
import java.net.URL
Expand Down Expand Up @@ -807,7 +812,7 @@ class BangumiPlayUrlHook(classLoader: ClassLoader) : BaseHook(classLoader) {
}
}
}

reconstructVideoInfoUpos(isDownload)
if (isDownload) {
fixDownloadProto(true)
}
Expand Down Expand Up @@ -879,4 +884,71 @@ class BangumiPlayUrlHook(classLoader: ClassLoader) : BaseHook(classLoader) {
response
}
}.onFailure { Log.e(it) }.getOrDefault(response)

private fun VideoInfoKt.Dsl.reconstructVideoInfoUpos(isDownload: Boolean = false) {
if (!isDownload || !enableUposReplace) return
val newStreamList = streamList.map { stream ->
stream.copy { reconstructStreamUpos() }
}
val newDashAudio = dashAudio.map { dashItem ->
dashItem.copy { reconstructDashItemUpos() }
}
streamList.clear()
dashAudio.clear()
streamList.addAll(newStreamList)
dashAudio.addAll(newDashAudio)
}

private fun StreamKt.Dsl.reconstructStreamUpos() {
if (hasDashVideo()) {
dashVideo = dashVideo.copy {
if (!hasBaseUrl()) return@copy
val (newBaseUrl, newBackupUrl) = reconstructVideoInfoUpos(baseUrl, backupUrl)
baseUrl = newBaseUrl
backupUrl.clear()
backupUrl.addAll(newBackupUrl)
}
} else if (hasSegmentVideo()) {
segmentVideo = segmentVideo.copy {
val newSegment = segment.map { responseUrl ->
responseUrl.copy {
val (newUrl, newBackupUrl) = reconstructVideoInfoUpos(url, backupUrl)
url = newUrl
backupUrl.clear()
backupUrl.addAll(newBackupUrl)
}
}
segment.clear()
segment.addAll(newSegment)
}
}
}

private fun DashItemKt.Dsl.reconstructDashItemUpos() {
if (!hasBaseUrl()) return
val (newBaseUrl, newBackupUrl) = reconstructVideoInfoUpos(baseUrl, backupUrl)
baseUrl = newBaseUrl
backupUrl.clear()
backupUrl.addAll(newBackupUrl)
}

private fun reconstructVideoInfoUpos(
baseUrl: String, backupUrls: List<String>
): Pair<String, List<String>> {
val filteredBackupUrls = backupUrls.filter { !it.contains(ipPCdnRegex) }
val newBackupUrls = mutableListOf(
filteredBackupUrls.getOrNull(0)?.reconstructVideoUpos(videoUposBackups[0])
?: baseUrl.replaceUpos(videoUposBackups[0]),
filteredBackupUrls.getOrNull(1)?.reconstructVideoUpos(videoUposBackups[1])
?: baseUrl.replaceUpos(videoUposBackups[1]),
)
return if (baseUrl.contains(ipPCdnRegex)) {
val newBaseUrl = newBackupUrls.firstOrNull { !it.contains(ipPCdnRegex) }
?: return baseUrl to backupUrls
newBackupUrls[0] = baseUrl
newBaseUrl.reconstructVideoUpos() to newBackupUrls
} else {
baseUrl.reconstructVideoUpos() to newBackupUrls
}
}
}
88 changes: 88 additions & 0 deletions app/src/main/java/me/iacn/biliroaming/hook/UposReplaceHook.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package me.iacn.biliroaming.hook

import me.iacn.biliroaming.utils.Log
import me.iacn.biliroaming.utils.UposReplaceHelper.enableLivePcdnBlock
import me.iacn.biliroaming.utils.UposReplaceHelper.enablePcdnBlock
import me.iacn.biliroaming.utils.UposReplaceHelper.enableUposReplace
import me.iacn.biliroaming.utils.UposReplaceHelper.forceUpos
import me.iacn.biliroaming.utils.UposReplaceHelper.gotchaRegex
import me.iacn.biliroaming.utils.UposReplaceHelper.initVideoUposList
import me.iacn.biliroaming.utils.UposReplaceHelper.ipPCdnRegex
import me.iacn.biliroaming.utils.UposReplaceHelper.isNeedReplaceVideoUpos
import me.iacn.biliroaming.utils.UposReplaceHelper.liveUpos
import me.iacn.biliroaming.utils.UposReplaceHelper.reconstructVideoUpos
import me.iacn.biliroaming.utils.UposReplaceHelper.replaceUpos
import me.iacn.biliroaming.utils.UposReplaceHelper.videoUposBackups
import me.iacn.biliroaming.utils.from
import me.iacn.biliroaming.utils.getObjectFieldOrNull
import me.iacn.biliroaming.utils.getObjectFieldOrNullAs
import me.iacn.biliroaming.utils.hookBeforeConstructor
import me.iacn.biliroaming.utils.hookBeforeMethod
import me.iacn.biliroaming.utils.setObjectField


class UposReplaceHook(classLoader: ClassLoader) : BaseHook(classLoader) {
override fun startHook() {
if (!enableUposReplace || !(forceUpos || enablePcdnBlock || enableLivePcdnBlock)) return
Log.d("startHook: UposReplaceHook")
"tv.danmaku.ijk.media.player.IjkMediaAsset\$MediaAssertSegment\$Builder".from(mClassLoader)
?.run {
hookBeforeConstructor(String::class.java, Int::class.javaPrimitiveType) { param ->
val baseUrl = param.args[0] as String
if (baseUrl.contains("live-bvc")) {
if (enableLivePcdnBlock && !baseUrl.contains(gotchaRegex)) {
param.args[0] = baseUrl.replaceUpos(liveUpos)
}
} else if (baseUrl.contains(ipPCdnRegex)) {
// IP:Port type PCDN currently only exists in Live and Thai Video.
} else if (baseUrl.isNeedReplaceVideoUpos()) {
param.args[0] = baseUrl.replaceUpos()
}
}

if (!(enablePcdnBlock || forceUpos)) return@run
hookBeforeMethod("setBackupUrls", MutableCollection::class.java) { param ->
val mediaAssertSegment = param.thisObject.getObjectFieldOrNull("target")
val baseUrl =
mediaAssertSegment?.getObjectFieldOrNullAs<String>("url").orEmpty()
if (baseUrl.isEmpty()) return@hookBeforeMethod
val backupUrls = if (param.args[0] == null) {
if (baseUrl.contains("live-bvc")) return@hookBeforeMethod else {
emptyList<String>()
}
} else {
@Suppress("UNCHECKED_CAST")
// Cannot simply replace IP:Port type PCDN's host
(param.args[0] as List<String>).filter { !it.contains(ipPCdnRegex) }
.takeIf { backupUrls ->
backupUrls.isEmpty() || !backupUrls.any { it.contains("live-bvc") }
} ?: return@hookBeforeMethod
}
param.args[0] =
reconstructBackupUposList(baseUrl, backupUrls, mediaAssertSegment)
}
}
}

override fun lateInitHook() {
initVideoUposList()
}

private fun reconstructBackupUposList(
baseUrl: String, backupUrls: List<String>, mediaAssertSegment: Any?
) = mutableListOf(
backupUrls.getOrNull(0)?.reconstructVideoUpos(videoUposBackups[0]) ?: baseUrl.replaceUpos(
videoUposBackups[0], true
),
backupUrls.getOrNull(1)?.reconstructVideoUpos(videoUposBackups[1]) ?: baseUrl.replaceUpos(
videoUposBackups[1], true
),
).apply {
if (baseUrl.contains(ipPCdnRegex)) {
mediaAssertSegment?.setObjectField(
"url", backupUrls.first().replaceUpos()
)
this[0] = baseUrl
}
}
}
78 changes: 1 addition & 77 deletions app/src/main/java/me/iacn/biliroaming/network/BiliRoamingApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import android.webkit.WebView
import android.webkit.WebViewClient
import me.iacn.biliroaming.BiliBiliPackage.Companion.instance
import me.iacn.biliroaming.BuildConfig
import me.iacn.biliroaming.R
import me.iacn.biliroaming.XposedInit
import me.iacn.biliroaming.hook.BangumiSeasonHook.Companion.lastSeasonInfo
import me.iacn.biliroaming.utils.*
Expand Down Expand Up @@ -423,88 +422,13 @@ object BiliRoamingApi {
}
}

private val mcdn by lazy {
listOf(
(sPrefs.getString("upos_host", null)
?: XposedInit.moduleRes.getString(R.string.cos_host)) to ""
) + if (runCatchingOrNull { XposedInit.country.get(5L, TimeUnit.SECONDS) } == "cn") {
val uri = Uri.Builder()
.scheme("https")
.encodedAuthority("api.bilibili.com/pgc/player/api/playurl")
.encodedQuery(signQuery(mainlandTestParams, emptyMap()))
.toString()

getContent(uri)?.toJSONObject()?.optJSONObject("dash")?.optJSONArray("video")
?.asSequence<JSONObject>()
?.toList()
?.flatMap {
listOfNotNull(it.optString("base_url")) + (it.optJSONArray("backup_url")
?.asSequence<String>()?.toList() ?: emptyList())
}?.mapNotNull {
Uri.parse(it).run {
encodedAuthority?.let {
encodedAuthority to (query?.substringBefore("&e=", "") ?: "")
}
}
}?.distinct() ?: emptyList()
} else listOf(XposedInit.moduleRes.getString(R.string.akamai_host) to "")
}

private fun replaceUPOS(stream: JSONObject) {
val baseAuthority = mcdn[0]
if (baseAuthority.first == "\$1") return
val base = Uri.parse(stream.optString("base_url"))
stream.put(
"base_url",
Uri.Builder().scheme("https").encodedAuthority(baseAuthority.first)
.encodedPath(base.encodedPath)
.query(baseAuthority.second)
.encodedQuery(base.encodedQuery).toString()
)
if (mcdn.size <= 1) return
val backup = stream.optJSONArray("backup_url")?.asSequence<String>() ?: emptySequence()
val newBackup = mutableListOf<String>()
backup.mapTo(newBackup) {
val url = Uri.parse(it)
Uri.Builder().scheme("https").encodedAuthority(baseAuthority.first)
.encodedPath(url.encodedPath)
.query(baseAuthority.second).encodedQuery(url.encodedQuery).toString()
}
mcdn.subList(1, mcdn.size).mapTo(newBackup) {
Uri.Builder().scheme("https").encodedAuthority(it.first)
.encodedPath(base.encodedPath)
.query(it.second).encodedQuery(base.encodedQuery).toString()
}
newBackup.add(base.toString())
newBackup.addAll(backup)
stream.put("backup_url", JSONArray(newBackup))
}

@JvmStatic
fun getPlayUrl(queryString: String?, priorityArea: Array<String>? = null): String? {
return getFromCustomUrl(queryString, priorityArea)?.let {
runCatchingOrNull {
JSONObject(it).let { json -> json.optJSONObject("result") ?: json }.apply {
optJSONObject("dash")?.run {
for (video in optJSONArray("video").orEmpty()) {
replaceUPOS(video)
}
for (audio in optJSONArray("audio").orEmpty()) {
replaceUPOS(audio)
}
}
}.toString()
} ?: throw CustomServerException(mapOf("default" to "Not valid json $it"))
}
}

class CustomServerException(val errors: Map<String, String>) : Throwable() {
override val message: String
get() = errors.asSequence().joinToString("\n") { "${it.key}: ${it.value}" }.trim()
}

@JvmStatic
private fun getFromCustomUrl(queryString: String?, priorityArea: Array<String>?): String? {
fun getPlayUrl(queryString: String?, priorityArea: Array<String>? = null): String? {
queryString ?: return null
val twUrl = sPrefs.getString("tw_server", null)
val hkUrl = sPrefs.getString("hk_server", null)
Expand Down
Loading

0 comments on commit a5bdac9

Please sign in to comment.