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
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ dependencies {
implementation(libs.kotlin.coroutines.jdk)
implementation(libs.androidx.documentfile)
implementation(libs.cxx)
implementation(libs.okhttp)
}

val adbExecutable: String = androidComponents.sdkComponents.adb.get().asFile.absolutePath
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 @@ -129,6 +129,7 @@ class XposedInit : IXposedHookLoadPackage, IXposedHookZygoteInit {
startHook { LiveQualityHook(lpparam.classLoader) }
startHook { StoryPlayerAdHook(lpparam.classLoader) }
startHook { LongPressSpeed(lpparam.classLoader) }
startHook { SkipVideoAd(lpparam.classLoader) }
}

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

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import me.iacn.biliroaming.BiliBiliPackage.Companion.instance
import me.iacn.biliroaming.utils.BilibiliSponsorBlock
import me.iacn.biliroaming.utils.Log
import me.iacn.biliroaming.utils.av2bv
import me.iacn.biliroaming.utils.callMethod
import me.iacn.biliroaming.utils.callMethodAs
import me.iacn.biliroaming.utils.hookAfterMethod
import me.iacn.biliroaming.utils.hookBeforeMethod
import me.iacn.biliroaming.utils.mossResponseHandlerReplaceProxy
import me.iacn.biliroaming.utils.sPrefs
import java.lang.ref.WeakReference

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

private var lastSeekTime = 0L
private var playerRef: WeakReference<Any>? = null
private val player get() = playerRef?.get()
private var duration: Int = -1
private var segments : List<BilibiliSponsorBlock.Segment>? = null
private var bvid: String = ""
private var cid: String = ""
private var waitTime = 1000

override fun startHook() {
if (!sPrefs.getBoolean("skip_video_ad", false)) return

Log.d("startHook: SkipVideoAd")

instance.playerMossClass?.apply {
hookBeforeMethod("executePlayViewUnite",
instance.playViewUniteReqClass
) { param ->
val req = param.args[0]
bvid = req.callMethodAs("getBvid")
val vod = req.callMethod("getVod")?:return@hookBeforeMethod
if (bvid.isEmpty()){
val aid = vod.callMethodAs<Long>("getAid")
if (aid==-1L){
return@hookBeforeMethod
}
bvid = av2bv(aid)
}
cid = vod.callMethodAs<Long>("getCid").toString()
}

hookBeforeMethod("playViewUnite",
instance.playViewUniteReqClass,
instance.mossResponseHandlerClass
){ param ->
param.args[1] = param.args[1].mossResponseHandlerReplaceProxy { reply ->
reply ?: return@mossResponseHandlerReplaceProxy null
val playArc = reply.callMethod("getPlayArc")?:return@mossResponseHandlerReplaceProxy null
cid = playArc.callMethodAs<Long>("getCid").toString()
val aid = playArc.callMethodAs<Long>("getAid")?:-1L
if (aid==-1L){
return@mossResponseHandlerReplaceProxy null
}
bvid = av2bv(aid)
null
}
}
}

instance.playerCoreServiceV2Class?.apply {
hookAfterMethod("G1", Int::class.java) { param ->
playerRef = WeakReference(param.thisObject)
val state = param.args[0] as Int
if (state in 3..5 && duration<=0) {
duration = (player?.callMethodAs<Int>("getDuration") ?: -1)
}
if(state == 2) {
duration = -1
segments = null
CoroutineScope(Dispatchers.IO).launch{
var retryCount = 0
val maxRetries = 3
while (retryCount < maxRetries) {
segments = BilibiliSponsorBlock(bvid, cid).getSegments()
if (segments.isNullOrEmpty()) {
retryCount++
delay(1000)
} else {
break
}
}
if (segments == null){
Log.toast("广告片段数据获取失败")
return@launch
}

}
}
}

hookAfterMethod("getCurrentPosition") { param ->
val now = System.currentTimeMillis()
if (now - lastSeekTime > waitTime) {
lastSeekTime = now
waitTime = if(seekTo(param.result as Int)) 3000 else 1000
}
}
}
}

private fun seekTo(position: Int?): Boolean {
if (position != null) {
if (position > duration) return false
}

if (segments != null) {
for (segment in segments) {
val start = (segment.segment[0]*1000).toInt()
val end = (segment.segment[1]*1000).toInt()
if (position in start..<end) {
Log.toast("已跳过广告片段")
player?.callMethod("seekTo", end)
return true
}
}
}
return false
}
}



110 changes: 110 additions & 0 deletions app/src/main/java/me/iacn/biliroaming/utils/BilibiliSponsorBlock.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package me.iacn.biliroaming.utils

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONArray
import org.json.JSONException
import java.util.concurrent.TimeUnit

class BilibiliSponsorBlock(
private val bvid: String,
private val cid: String,
) {
companion object {
private const val BASE_URL = "https://bsbsb.top/api/skipSegments/"
internal val httpClient by lazy {
OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.build()
}
private val sha256Digest by lazy {
java.security.MessageDigest.getInstance("SHA-256")
}
}

suspend fun getSegments(): List<Segment>? = withContext(Dispatchers.IO) {
try {
val prefix = bvid.trim().sha256().take(4)
val url = "$BASE_URL$prefix?category=sponsor"

val request = Request.Builder()
.url(url)
.header("origin", "BiliRoaming")
.header("x-ext-version", "1.7.0")
.build()

return@withContext httpClient.newCall(request).execute().use { resp ->
if (!resp.isSuccessful) {
Log.e("HTTP request failed with code: ${resp.code}")
return@use null
}

val body = resp.body.string()
if (body.isEmpty()) {
Log.e("HTTP request failed with empty body")
return@use null
}

parseSegments(body)
}

} catch (e: Exception) {
Log.e(e)
null
}
}

private fun parseSegments(json: String): List<Segment>? {
try {
val jsonArray = JSONArray(json)
val segments = mutableListOf<Segment>()
if (jsonArray.length() == 0) return null

for (i in 0 until jsonArray.length()) {
val obj = jsonArray.getJSONObject(i)
if (obj.optString("videoID") != bvid) continue

val segmentsArray = obj.getJSONArray("segments")
for (j in 0 until segmentsArray.length()) {
val item = segmentsArray.getJSONObject(j)
val segmentArray = item.getJSONArray("segment")
segments.add(
Segment(
segment = floatArrayOf(
segmentArray.getDouble(0).toFloat(),
segmentArray.getDouble(1).toFloat()
),
cid = item.optString("cid"),
UUID = item.optString("UUID"),
category = item.optString("category"),
actionType = item.optString("actionType"),
videoDuration = item.optInt("videoDuration")
)
)
}
break
}
return segments
.filter { it.cid == this.cid }
.sortedBy { it.segment[0] }
} catch (_: JSONException) {
return null
}
}

private fun String.sha256(): String = sha256Digest
.digest(toByteArray())
.joinToString("") { "%02x".format(it) }

data class Segment(
val segment: FloatArray,
val cid: String,
val UUID: String,
val category: String,
val actionType: String,
val videoDuration: Int
)
}
12 changes: 12 additions & 0 deletions app/src/main/java/me/iacn/biliroaming/utils/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ fun bv2av(bv: String): Long {
return r.and(2251799813685247L).xor(23442827791579L)
}

fun av2bv(aid: Long): String {
val s = CharArray(12) { if (it < 3) "BV1"[it] else '0' }
var t = ((1L shl 51) or aid) xor 23442827791579L
var i = 11
while (t > 0) {
s[i--] = "FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf"[(t % 58).toInt()]
t /= 58
}
s[3] = s[9].also { s[9] = s[3] }
s[4] = s[7].also { s[7] = s[4] }
return String(s)
}
fun getPackageVersion(packageName: String) = try {
@Suppress("DEPRECATION")
systemContext.packageManager.getPackageInfo(packageName, 0).run {
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 @@ -315,4 +315,6 @@
<string name="purify_story_video_ad_title">净化竖屏模式广告/推广</string>
<string name="purify_story_video_ad_summary">有了这个,连第一关都看不到了😭</string>
<string name="long_press_speed">长按播放速度设置</string>
<string name="skip_video_ad_title">跳过视频广告</string>
<string name="skip_video_ad_summary">一个简单的跳过视频广告功能,参考BilibiliSponsorBlock实现</string>
</resources>
6 changes: 6 additions & 0 deletions app/src/main/res/xml/prefs_setting.xml
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,12 @@
android:key="block_video_comment"
android:title="@string/block_video_comment_title" />

<SwitchPreference
android:dependency="hidden"
android:key="skip_video_ad"
android:summary="@string/skip_video_ad_summary"
android:title="@string/skip_video_ad_title" />

<Preference
android:key="copy_access_key"
android:summary="@string/copy_access_key_summary"
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
protobuf = "4.30.2"
coroutine = "1.10.2"
kotlin = "2.1.20"
okhttp = "5.3.2"

[plugins]
kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
Expand All @@ -23,3 +24,4 @@ kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "
kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutine" }
kotlin-coroutines-jdk = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8", version.ref = "coroutine" }
androidx-documentfile = { module = "androidx.documentfile:documentfile", version = "1.0.1" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
Loading