Skip to content

Commit

Permalink
feat: live quality (yujincheng08#1615)
Browse files Browse the repository at this point in the history
- 添加设置直播默认清晰度
  • Loading branch information
TinyHai authored Jan 17, 2025
1 parent 5552175 commit e888312
Show file tree
Hide file tree
Showing 7 changed files with 256 additions and 0 deletions.
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

0 comments on commit e888312

Please sign in to comment.