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
62 changes: 62 additions & 0 deletions app/src/main/java/me/iacn/biliroaming/BiliBiliPackage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package me.iacn.biliroaming
import android.app.AndroidAppHelper
import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
import android.text.style.ClickableSpan
import android.text.style.LineBackgroundSpan
import android.util.SparseArray
Expand Down Expand Up @@ -168,6 +169,8 @@ class BiliBiliPackage constructor(private val mClassLoader: ClassLoader, mContex
val playSpeedManager by Weak { mHookInfo.playSpeedManager from mClassLoader }
val continuationClass by Weak { mHookInfo.continuation.class_ from mClassLoader }
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 }

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

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

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

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

private fun readHookInfo(context: Context): Configs.HookInfo {
try {
val hookInfoFile = File(context.cacheDir, Constant.HOOK_INFO_FILE_NAME)
Expand Down Expand Up @@ -2125,6 +2132,61 @@ class BiliBiliPackage constructor(private val mClassLoader: ClassLoader, mContex
class_ = class_ { name = serviceClass.name }
canTrial = method { name = canTrialMethod.name }
}
liveQuality = liveQuality {
val utilClass = dexHelper.findMethodUsingString(
"select 秒开 play url --codec:",
false,
-1,
-1,
null,
-1,
null,
null,
null,
true
).map {
dexHelper.decodeMethodIndex(it)?.declaringClass
}.firstOrNull() ?: return@liveQuality
val selectorDataClass = dexHelper.findMethodUsingString(
"LiveUrlSelectorData(playUrl=",
false,
-1,
-1,
null,
-1,
null,
null,
null,
true
).map {
dexHelper.decodeMethodIndex(it)?.declaringClass
}.firstOrNull() ?: return@liveQuality
val parseUriMethod = utilClass.declaredMethods.firstOrNull {
it.returnType == selectorDataClass && it.parameterCount == 1 && it.parameterTypes[0] == Uri::class.java
} ?: return@liveQuality
val switchAutoMethod = dexHelper.findMethodUsingString(
"switchAuto ",
false,
-1,
-1,
null,
-1,
null,
null,
null,
true
).map {
dexHelper.decodeMethodIndex(it)
}.firstOrNull() ?: return@liveQuality
selectUtil = livePlayUrlSelectUtil {
class_ = class_ { name = utilClass.name }
parseUri = method { name = parseUriMethod.name }
}
sourceService = liveRTCSourceServiceImpl {
class_ = class_ { name = switchAutoMethod.declaringClass.name }
switchAuto = method { name = switchAutoMethod.name }
}
}

dexHelper.close()
}
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 @@ -122,6 +122,7 @@ class XposedInit : IXposedHookLoadPackage, IXposedHookZygoteInit {
startHook(UposReplaceHook(lpparam.classLoader))
startHook(SpeedHook(lpparam.classLoader))
startHook(MultiWindowHook(lpparam.classLoader))
startHook(LiveQualityHook(lpparam.classLoader))
}

lpparam.processName.endsWith(":web") -> {
Expand Down
147 changes: 147 additions & 0 deletions app/src/main/java/me/iacn/biliroaming/hook/LiveQualityHook.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package me.iacn.biliroaming.hook

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

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

override fun startHook() {
val liveQuality = sPrefs.getString("live_quality", "0")?.toIntOrNull() ?: 0
if (liveQuality <= 0) {
return
}

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

instance.retrofitResponseClass?.hookBeforeAllConstructors { param ->
val url = getRetrofitUrl(param.args[0]) ?: return@hookBeforeAllConstructors
val body = param.args[1] ?: return@hookBeforeAllConstructors

when {
instance.generalResponseClass?.isInstance(body) != true -> Unit
// 处理上下滑动切换直播间
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())
if (fixLiveRoomFeedInfo(info, liveQuality)) {
body.setObjectField(
"data",
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
}
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)
)
}
}
}
}

instance.liveRTCSourceServiceImplClass?.hookBeforeAllMethods(instance.switchAutoMethod()) { param ->
val mode = param.args[0] as? Enum<*> ?: return@hookBeforeAllMethods
if (mode.ordinal == 2) { // AUTO
param.result = null
}
}

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

val newQuality = findQuality(
JSONArray(originalUri.getQueryParameter("accept_quality")),
liveQuality
).toString()

param.args[0] = originalUri.replaceQuery { name, oldVal ->
when {
"current_qn" == name -> newQuality
"current_quality" == name -> newQuality
name.endsWith("current_qn") -> "0"
else -> oldVal
}
}
}
}

private fun Uri.isLive(): Boolean {
return scheme in arrayOf("http", "https")
&& host == "live.bilibili.com"
&& pathSegments.firstOrNull()?.all { it.isDigit() } == true
}

private fun findQuality(acceptQuality: JSONArray, expectQuality: Int): Int {
val acceptQnList = acceptQuality.asSequence<Int>().sorted().toList()
val max = acceptQnList.max()
val min = acceptQnList.min()
return when {
expectQuality > max -> max
expectQuality < min -> min
else -> acceptQnList.first { it >= expectQuality }
}
}

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

private fun fixLiveRoomFeedInfo(info: JSONObject, expectQuality: Int): Boolean {
val feedList = info.optJSONArray("list") ?: return false
feedList.iterator().forEach { feedData ->
val newQuality = findQuality(feedData.getJSONArray("accept_quality"), expectQuality)
feedData.put("current_qn", newQuality)
feedData.put("current_quality", newQuality)
}
return true
}

private fun fixRoomPlayInfo(info: JSONObject, expectQuality: Int): Boolean {
val playUrlInfo = info.optJSONObject("playurl_info") ?: return false
val playUrlObj = playUrlInfo.getJSONObject("playurl")
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) }
}
}
}
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
}
}
16 changes: 16 additions & 0 deletions app/src/main/proto/me/iacn/biliroaming/configs.proto
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,21 @@ message VipQualityTrialService {
optional Method canTrial = 2;
}

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

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

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

message HookInfo {
int64 last_update_time = 1;
optional MapIds map_ids = 2;
Expand Down Expand Up @@ -340,4 +355,5 @@ message HookInfo {
optional QualityStrategyProvider qualityStrategyProvider = 95;
optional Continuation continuation = 96;
optional VipQualityTrialService vipQualityTrialService = 97;
optional LiveQuality liveQuality = 98;
}
20 changes: 20 additions & 0 deletions app/src/main/res/values/arrays.xml
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,26 @@
<item>120</item>
<item>127</item>
</string-array>
<string-array name="live_quality_entries">
<item>默认</item>
<item>流畅</item>
<item>高清</item>
<item>超清</item>
<item>蓝光</item>
<item>原画</item>
<item>4K</item>
<item>杜比</item>
</string-array>
<string-array name="live_quality_values">
<item>0</item>
<item>80</item>
<item>150</item>
<item>250</item>
<item>400</item>
<item>10000</item>
<item>20000</item>
<item>30000</item>
</string-array>
<string-array name="full_screen_quality_entries">
<item>默认</item>
<item>240P</item>
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,8 @@
<string name="half_screen_quality_summary">设置视频半屏播放时的清晰度,设置后会大幅降低视频的首次播放加载速度,旧版播放器(半屏时进度条在框内)仅支持跟随全屏清晰度,需开启解锁番剧限制选项</string>
<string name="full_screen_quality_title">视频全屏清晰度</string>
<string name="full_screen_quality_summary">设置视频全屏播放时默认的清晰度,当播放器画质选项为自动时此选项不生效,需开启解锁番剧限制选项</string>
<string name="live_quality_title">直播清晰度</string>
<string name="live_quality_summary">设置进入直播间的默认清晰度,若不存在指定清晰度,则会取最接近的清晰度</string>
<string name="block_comment_guide_title">屏蔽评论引导</string>
<string name="block_comment_guide_summary">屏蔽视频详情页及评论页的评论引导提示</string>
<string name="disable_auto_refresh_title">禁止首页自动刷新</string>
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/res/xml/prefs_setting.xml
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,14 @@
android:summary="@string/full_screen_quality_summary"
android:title="@string/full_screen_quality_title" />

<ListPreference
android:defaultValue="0"
android:entries="@array/live_quality_entries"
android:entryValues="@array/live_quality_values"
android:key="live_quality"
android:summary="@string/live_quality_summary"
android:title="@string/live_quality_title"/>

<SwitchPreference
android:key="block_comment_guide"
android:summary="@string/block_comment_guide_summary"
Expand Down
Loading