Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions app/src/main/java/me/iacn/biliroaming/BiliBiliPackage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ class BiliBiliPackage constructor(private val mClassLoader: ClassLoader, mContex
val vipQualityTrialService by Weak { mHookInfo.vipQualityTrialService.class_ from mClassLoader }
val livePlayUrlSelectUtilClass by Weak { mHookInfo.liveQuality.selectUtil.class_ from mClassLoader }
val liveRTCSourceServiceImplClass by Weak { mHookInfo.liveQuality.sourceService.class_ from mClassLoader }
val defaultRequestInterceptClass by Weak { mHookInfo.liveQuality.interceptor.class_ from mClassLoader }
val httpUrlClass by Weak { mHookInfo.okHttp.httpUrl.class_ from mClassLoader }

// for v8.17.0+
val useNewMossFunc = instance.viewMossClass?.declaredMethods?.any {
Expand Down Expand Up @@ -335,10 +337,14 @@ class BiliBiliPackage constructor(private val mClassLoader: ClassLoader, mContex

fun onFeedClicked() = mHookInfo.cardClickProcessor.onFeedClicked.orNull

fun parseUriMethod() = mHookInfo.liveQuality.selectUtil.parseUri.orNull
fun buildSelectorDataMethod() = mHookInfo.liveQuality.selectUtil.buildSelectorData.orNull

fun switchAutoMethod() = mHookInfo.liveQuality.sourceService.switchAuto.orNull

fun interceptMethod() = mHookInfo.liveQuality.interceptor.intercept.orNull

fun httpUrlParseMethod() = mHookInfo.okHttp.httpUrl.parse.orNull

private fun readHookInfo(context: Context): Configs.HookInfo {
try {
val hookInfoFile = File(context.cacheDir, Constant.HOOK_INFO_FILE_NAME)
Expand Down Expand Up @@ -543,6 +549,9 @@ class BiliBiliPackage constructor(private val mClassLoader: ClassLoader, mContex
}.firstOrNull {
it.declaringClass?.name?.startsWith("okhttp3") == true
}?.declaringClass ?: return@okHttp
val parseMethod = urlClass.declaredMethods.firstOrNull {
it.isStatic && it.returnType == urlClass && it.parameterCount == 1 && it.parameterTypes[0] == String::class.java
} ?: return@okHttp
responseBodyClass ?: return@okHttp
val getMethod = dexHelper.findMethodUsingString(
"No subtype found for:",
Expand Down Expand Up @@ -591,6 +600,10 @@ class BiliBiliPackage constructor(private val mClassLoader: ClassLoader, mContex
class_ = class_ { name = getMethod.declaringClass.name }
get = method { name = getMethod.name }
}
httpUrl = httpUrl {
class_ = class_ { name = urlClass.name }
parse = method { name = parseMethod.name }
}
}
fastJson = fastJson {
val fastJsonClass = dexHelper.findMethodUsingString(
Expand Down Expand Up @@ -2161,7 +2174,7 @@ class BiliBiliPackage constructor(private val mClassLoader: ClassLoader, mContex
).map {
dexHelper.decodeMethodIndex(it)?.declaringClass
}.firstOrNull() ?: return@liveQuality
val parseUriMethod = utilClass.declaredMethods.firstOrNull {
val buildSelectorDataMethod = utilClass.declaredMethods.firstOrNull {
it.returnType == selectorDataClass && it.parameterCount == 1 && it.parameterTypes[0] == Uri::class.java
} ?: return@liveQuality
val switchAutoMethod = dexHelper.findMethodUsingString(
Expand All @@ -2178,14 +2191,35 @@ class BiliBiliPackage constructor(private val mClassLoader: ClassLoader, mContex
).map {
dexHelper.decodeMethodIndex(it)
}.firstOrNull() ?: return@liveQuality
val interceptorClass = dexHelper.findMethodUsingString(
"inject common param to body failure : ",
false,
-1,
-1,
null,
-1,
null,
null,
null,
true
).map {
dexHelper.decodeMethodIndex(it)?.declaringClass
}.firstOrNull() ?: return@liveQuality
val interceptMethod = interceptorClass.declaredMethods.firstOrNull {
it.isPublic && it.parameterCount == 1 && it.returnType == it.parameterTypes[0]
} ?: return@liveQuality
selectUtil = livePlayUrlSelectUtil {
class_ = class_ { name = utilClass.name }
parseUri = method { name = parseUriMethod.name }
buildSelectorData = method { name = buildSelectorDataMethod.name }
}
sourceService = liveRTCSourceServiceImpl {
class_ = class_ { name = switchAutoMethod.declaringClass.name }
switchAuto = method { name = switchAutoMethod.name }
}
interceptor = defaultRequestIntercept {
class_ = class_ { name = interceptorClass.name }
intercept = method { name = interceptMethod.name }
}
}

dexHelper.close()
Expand Down
134 changes: 87 additions & 47 deletions app/src/main/java/me/iacn/biliroaming/hook/LiveQualityHook.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,26 @@ package me.iacn.biliroaming.hook

import android.net.Uri
import me.iacn.biliroaming.BiliBiliPackage.Companion.instance
import me.iacn.biliroaming.BuildConfig
import me.iacn.biliroaming.utils.*
import org.json.JSONArray
import org.json.JSONObject

class LiveQualityHook(classLoader: ClassLoader) : BaseHook(classLoader) {

companion object {
private const val TAG = "LiveQualityHook"
}

@Volatile
private var newQn: String = ""

private fun debug(msg: () -> String) {
if (BuildConfig.DEBUG) {
Log.d("[$TAG] - ${msg()}")
}
}

override fun startHook() {
val liveQuality = sPrefs.getString("live_quality", "0")?.toIntOrNull() ?: 0
if (liveQuality <= 0) {
Expand All @@ -16,6 +30,36 @@ class LiveQualityHook(classLoader: ClassLoader) : BaseHook(classLoader) {

val canSwitchLiveRoom = !sPrefs.getBoolean("forbid_switch_live_room", false)

instance.defaultRequestInterceptClass?.hookBeforeAllMethods(instance.interceptMethod()) { param ->
val request = param.args[0]
val httpUrl = request.getObjectField(instance.urlField())
val url = httpUrl.toString()
if (!url.startsWith("https://api.live.bilibili.com/xlive/app-room/v2/index/getRoomPlayInfo?")) {
return@hookBeforeAllMethods
}

debug { "oldHttpUrl: $url" }

val uri = Uri.parse(httpUrl.toString())
val qn = uri.getQueryParameter("qn")
if (qn.isNullOrEmpty() || qn == "0") {
val builder = uri.buildUpon().clearQuery()
for (name in uri.queryParameterNames) {
if (name == "qn") {
builder.appendQueryParameter(name, newQn.ifEmpty { liveQuality.toString() })
} else {
builder.appendQueryParameter(name, uri.getQueryParameter(name))
}
}
val newHttpUrl = instance.httpUrlClass?.callStaticMethod(
instance.httpUrlParseMethod(),
builder.build().toString()
)
debug { "newHttpUrl: $newHttpUrl" }
request.setObjectField(instance.urlField(), newHttpUrl)
}
}

instance.retrofitResponseClass?.hookBeforeAllConstructors { param ->
val url = getRetrofitUrl(param.args[0]) ?: return@hookBeforeAllConstructors
val body = param.args[1] ?: return@hookBeforeAllConstructors
Expand All @@ -25,28 +69,27 @@ class LiveQualityHook(classLoader: ClassLoader) : BaseHook(classLoader) {
// 处理上下滑动切换直播间
url.startsWith("https://api.live.bilibili.com/xlive/app-interface/v2/room/recList?") && canSwitchLiveRoom -> {
val data = body.getObjectField("data") ?: return@hookBeforeAllConstructors
val info = JSONObject(instance.fastJsonClass?.callStaticMethod("toJSONString", data).toString())
val info = JSONObject(
instance.fastJsonClass?.callStaticMethod("toJSONString", data).toString()
)
if (fixLiveRoomFeedInfo(info, liveQuality)) {
body.setObjectField(
"data",
instance.fastJsonClass?.callStaticMethod(instance.fastJsonParse(), info.toString(), data.javaClass)
instance.fastJsonClass?.callStaticMethod(
instance.fastJsonParse(),
info.toString(),
data.javaClass
)
)
}
}
url.startsWith("https://api.live.bilibili.com/xlive/app-room/v2/index/getRoomPlayInfo?") -> {
val uri = Uri.parse(url)
val reqQn = uri.getQueryParameter("qn")
if (!reqQn.isNullOrEmpty() && reqQn != "0") {
return@hookBeforeAllConstructors
}

BuildConfig.DEBUG && url.startsWith("https://api.live.bilibili.com/xlive/app-room/v2/index/getRoomPlayInfo?") -> {
val data = body.getObjectField("data") ?: return@hookBeforeAllConstructors
val info = JSONObject(instance.fastJsonClass?.callStaticMethod("toJSONString", data).toString())
if (fixRoomPlayInfo(info, liveQuality)) {
body.setObjectField(
"data",
instance.fastJsonClass?.callStaticMethod(instance.fastJsonParse(), info.toString(), data.javaClass)
)
}
val info = JSONObject(
instance.fastJsonClass?.callStaticMethod("toJSONString", data).toString()
)
printCodec(info)
}
}
}
Expand All @@ -59,26 +102,26 @@ class LiveQualityHook(classLoader: ClassLoader) : BaseHook(classLoader) {
}

instance.livePlayUrlSelectUtilClass?.hookBeforeMethod(
instance.parseUriMethod(),
instance.buildSelectorDataMethod(),
Uri::class.java
) { param ->
val originalUri = param.args[0] as Uri
if (!originalUri.isLive()) {
return@hookBeforeMethod
}

val newQuality = findQuality(
debug { "originalLiveUrl: $originalUri" }

newQn = findQuality(
JSONArray(originalUri.getQueryParameter("accept_quality")),
liveQuality
).toString()
debug { "newQn: $newQn" }

param.args[0] = originalUri.replaceQuery { name, oldVal ->
when {
"current_qn" == name -> newQuality
"current_quality" == name -> newQuality
name.endsWith("current_qn") -> "0"
else -> oldVal
}
param.args[0] = originalUri.removeQuery { name ->
name.startsWith("playurl")
}.also {
debug { "newLiveUrl: $it" }
}
}
}
Expand All @@ -100,48 +143,45 @@ class LiveQualityHook(classLoader: ClassLoader) : BaseHook(classLoader) {
}
}

private fun Uri.replaceQuery(replacer: (String, String) -> String?): Uri {
private fun Uri.removeQuery(predicate: (String) -> Boolean): Uri {
val newBuilder = buildUpon().clearQuery()
for (name in queryParameterNames) {
val newValue = replacer(name, getQueryParameter(name) ?: "")
newValue?.let { newBuilder.appendQueryParameter(name, it) }
val value = getQueryParameter(name) ?: ""
if (!predicate(name)) {
newBuilder.appendQueryParameter(name, value)
}
}
return newBuilder.build()
}

private fun fixLiveRoomFeedInfo(info: JSONObject, expectQuality: Int): Boolean {
val feedList = info.optJSONArray("list") ?: return false
feedList.iterator().forEach { feedData ->
debug { "oldFeedData: ${feedData.toString(2)}" }
val newQuality = findQuality(feedData.getJSONArray("accept_quality"), expectQuality)
feedData.put("current_qn", newQuality)
feedData.put("current_quality", newQuality)
feedData.apply {
put("current_qn", newQuality)
put("current_quality", newQuality)
put("play_url", "")
put("play_url_h265", "")
put("playurl_infos", JSONArray())
}
debug { "newFeedData: ${feedData.toString(2)}" }
}
return true
}

private fun fixRoomPlayInfo(info: JSONObject, expectQuality: Int): Boolean {
val playUrlInfo = info.optJSONObject("playurl_info") ?: return false
private fun printCodec(info: JSONObject) {
val playUrlInfo = info.optJSONObject("playurl_info") ?: return
val playUrlObj = playUrlInfo.getJSONObject("playurl")
debug { "printCodec >>>>>>>>>>>>>>" }
playUrlObj.getJSONArray("stream").iterator().forEach { stream ->
stream.getJSONArray("format").iterator().forEach { format ->
format.getJSONArray("codec").iterator().asSequence().run {
firstOrNull { codec -> fixCodec(codec, expectQuality, true) }
?: firstOrNull { codec -> fixCodec(codec, expectQuality, false) }
format.getJSONArray("codec").iterator().forEach {
debug { "codec: ${it.toString(2)}" }
}
}
}
return true
}

private fun fixCodec(codec: JSONObject, expectQuality: Int, strict: Boolean): Boolean {
val newQuality = findQuality(codec.getJSONArray("accept_qn"), expectQuality)
if (strict && newQuality != expectQuality) {
return false
}
val oldQn = codec.getInt("current_qn")
if (oldQn != newQuality) {
codec.put("current_qn", newQuality)
}
return true
debug { "printCodec <<<<<<<<<<<<<<" }
}
}
14 changes: 13 additions & 1 deletion app/src/main/proto/me/iacn/biliroaming/configs.proto
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ message ResponseBody {
optional Method string = 3;
}

message HttpUrl {
optional Class class = 1;
optional Method parse = 2;
}

message MediaType {
optional Class class = 1;
optional Method get = 2;
Expand All @@ -44,6 +49,7 @@ message OkHttp {
optional Response response = 2;
optional ResponseBody response_body = 3;
optional MediaType mediaType = 4;
optional HttpUrl httpUrl = 5;
}

message FastJson {
Expand Down Expand Up @@ -277,17 +283,23 @@ message VipQualityTrialService {

message LivePlayUrlSelectUtil {
optional Class class = 1;
optional Method parseUri = 2;
optional Method buildSelectorData = 2;
}

message LiveRTCSourceServiceImpl {
optional Class class = 1;
optional Method switchAuto = 2;
}

message DefaultRequestIntercept {
optional Class class = 1;
optional Method intercept = 2;
}

message LiveQuality {
optional LivePlayUrlSelectUtil selectUtil = 1;
optional LiveRTCSourceServiceImpl sourceService = 2;
optional DefaultRequestIntercept interceptor = 3;
}

message HookInfo {
Expand Down
Loading