diff --git a/app/src/main/java/me/iacn/biliroaming/BiliBiliPackage.kt b/app/src/main/java/me/iacn/biliroaming/BiliBiliPackage.kt index 38aa5d4cef..1edcb5542d 100644 --- a/app/src/main/java/me/iacn/biliroaming/BiliBiliPackage.kt +++ b/app/src/main/java/me/iacn/biliroaming/BiliBiliPackage.kt @@ -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 { @@ -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) @@ -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:", @@ -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( @@ -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( @@ -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() diff --git a/app/src/main/java/me/iacn/biliroaming/hook/LiveQualityHook.kt b/app/src/main/java/me/iacn/biliroaming/hook/LiveQualityHook.kt index 1e418b9f81..5fed4a0550 100644 --- a/app/src/main/java/me/iacn/biliroaming/hook/LiveQualityHook.kt +++ b/app/src/main/java/me/iacn/biliroaming/hook/LiveQualityHook.kt @@ -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) { @@ -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 @@ -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) } } } @@ -59,7 +102,7 @@ class LiveQualityHook(classLoader: ClassLoader) : BaseHook(classLoader) { } instance.livePlayUrlSelectUtilClass?.hookBeforeMethod( - instance.parseUriMethod(), + instance.buildSelectorDataMethod(), Uri::class.java ) { param -> val originalUri = param.args[0] as Uri @@ -67,18 +110,18 @@ class LiveQualityHook(classLoader: ClassLoader) : BaseHook(classLoader) { 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" } } } } @@ -100,11 +143,13 @@ 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() } @@ -112,36 +157,31 @@ class LiveQualityHook(classLoader: ClassLoader) : BaseHook(classLoader) { 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 <<<<<<<<<<<<<<" } } } \ No newline at end of file diff --git a/app/src/main/proto/me/iacn/biliroaming/configs.proto b/app/src/main/proto/me/iacn/biliroaming/configs.proto index 9e698225fe..88e3c0acec 100644 --- a/app/src/main/proto/me/iacn/biliroaming/configs.proto +++ b/app/src/main/proto/me/iacn/biliroaming/configs.proto @@ -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; @@ -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 { @@ -277,7 +283,7 @@ message VipQualityTrialService { message LivePlayUrlSelectUtil { optional Class class = 1; - optional Method parseUri = 2; + optional Method buildSelectorData = 2; } message LiveRTCSourceServiceImpl { @@ -285,9 +291,15 @@ message LiveRTCSourceServiceImpl { 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 {