V1.4.7: 新增原生取址兜底 —— 修复 WebView 不可用的老盒子无法点歌

证据(用户导出的 1.4.6 远程日志):
- 两条失败记录只有 Kotlin 侧那一行
  「点歌编号=... 详情=无法生成下载地址」,
  而 JS ...(本版新加的 console 桥)、RAW-ERR、NET-ERR、点歌失败 一条都没有。
- 即 JS 侧从未执行 —— 隐藏 WebView 在这台盒子上从未就绪
  (每条失败前正好有约 15s 的「等桥就绪」超时,时间吻合)。
- 于是 SongApiClient 只能回退 DB 的 cloud_url,取不到就报「无法生成下载地址」。

根因: 取址完全依赖 WebView 里的 ktv_api.js;GMUI2.0/Android 5.1.1 上
WebView 不可用(无法导出 window.KtvApi),桥永远 not ready。

修复:
- 新增 SongUrlResolver: 用 Kotlin 复刻 ktv_api.js 的协议
  (GET <host>/i.php 取 token → GET <host>/music/do.php 取地址),
  镜像多源排序(失败源沉底)与 isDemoUrl 校验(wb66/demo/纯IP/r2.dev/ts编号不一致)。
- 走与 JS 完全相同的裸 Socket「干净请求」通道(KtvJsBridge.rawCleanGet),
  请求特征一致故风控表现一致,且不依赖 WebView 是否就绪。
- SongApiClient.getSongDownloadUrl 三处回退点改为:
  桥未就绪 / JS 未取到地址 / 主线程调用 → 先走原生解析,再回退 DB。
- 补齐桥的生命周期可观测性: WebView 初始化失败、KtvApi 未导出、
  15s 未就绪 均写入远程日志(以前只在 logcat,这就是本轮一开始没有线索的原因)。

已用 App 的真实身份( mac=800b510bf4fd / sn=2d7bb4a7d7f7 / VER=2.0 )
从本机复现协议: i.php 200 → do.php code=200 且返回
  http://txog.ktvsky.com/.../480p/7303235.ts (与点歌编号一致),
证明参数顺序与签名算法正确。

版本 1.4.6(26) 到 1.4.7(27);签名证书不变。ktv_api.js 未改动,KTV_BRIDGE_API 仍为 8。
This commit is contained in:
zijunchiang
2026-09-26 17:08:38 +08:00
parent 691e56f617
commit 1687f87eca
5 changed files with 262 additions and 13 deletions
@@ -24,9 +24,13 @@ object SongApiClient {
fun init(context: Context) {
if (bridge != null) return
bridge = KtvJsBridge(context)
// 原生兜底取址通道:复用桥的「干净请求」裸 Socket(与 JS 同一条路,风控表现一致),
// 但**不依赖 WebView 是否可用** —— 老盒子上 WebView 不可用时唯一能取到地址的路径。
SongUrlResolver.install { url -> bridge?.rawCleanGet(url) }
bridge?.init { ok ->
ready = ok
Log.i(TAG, "JS Bridge init: $ok")
if (!ok) MiliLog.error("JS-BRIDGE 初始化返回 false,取址将走原生兜底")
}
}
@@ -47,10 +51,10 @@ object SongApiClient {
fun getSongDownloadUrl(musicno: String, resolution: String, h265: Boolean): String? {
// 主线程同步等待 WebView 回调会死锁;当前播放/下载流程会在 IO 线程调用这里。
if (Looper.myLooper() == Looper.getMainLooper()) {
Log.w(TAG, "Synchronous JS request on main thread; using DB fallback")
return fetchFromDb(musicno)
Log.w(TAG, "Synchronous JS request on main thread; using native/DB fallback")
return resolveNatively(musicno, resolution, h265) ?: fetchFromDb(musicno)
}
val bridge = bridge ?: return fetchFromDb(musicno)
val bridge = bridge ?: return resolveNatively(musicno, resolution, h265) ?: fetchFromDb(musicno)
// 等待 JS 桥"当前"真正就绪。首次冷启动时隐藏 WebView 的初始化在低性能设备
// (极米投影等)上可能超过 3 秒;桥热更新重载期间 isReady() 也会短暂为 false。
// 这里放宽到 15 秒,避免第一首歌因撞上就绪窗口而回退到不可靠的 DB cloud_url。
@@ -62,8 +66,11 @@ object SongApiClient {
}
}
if (!bridge.isReady()) {
Log.w(TAG, "Bridge not ready after wait, DB fallback")
return fetchFromDb(musicno)
// ⚠️ 老盒子(WebView 不可用)会一直走到这里。以前直接回退 DB,结果就是
// 「无法生成下载地址」且远程日志里连一条 JS 日志都没有。现在改走原生解析器。
Log.w(TAG, "Bridge not ready after wait, native resolver")
MiliLog.warn("JS-BRIDGE 15s 未就绪,改用原生取址通道")
return resolveNatively(musicno, resolution, h265) ?: fetchFromDb(musicno)
}
val result = AtomicReference<String?>()
val latch = CountDownLatch(1)
@@ -74,7 +81,9 @@ object SongApiClient {
} catch (_: InterruptedException) {
Thread.currentThread().interrupt()
}
val url = result.get() ?: fetchFromDb(musicno)
// 桥可用但 JS 没取到地址(或超时)时,同样再给原生解析器一次机会,
// 避免 JS 侧偶发失败直接把第一首歌判死。
val url = result.get() ?: resolveNatively(musicno, resolution, h265) ?: fetchFromDb(musicno)
if (url != null) {
// 成功取到歌曲 URL → 歌曲源窗口已恢复/未到期, 复位到期提示状态。
Eac19WindowGuard.noteUrlOk()
@@ -82,9 +91,17 @@ object SongApiClient {
return url
}
/**
* 原生兜底取址:WebView 不可用(老盒子)或 JS 没取到地址时使用。
* 走的是与 JS 完全相同的裸 Socket 干净请求通道,见 [SongUrlResolver]。
*/
private fun resolveNatively(musicno: String, resolution: String, h265: Boolean): String? =
runCatching { SongUrlResolver.resolve(musicno, resolution, h265) }
.onFailure { Log.w(TAG, "native resolver failed", it) }
.getOrNull()
/** DB cloud_url fallback — 必须通过 demo 检测才能使用 */
private fun fetchFromDb(musicno: String): String? {
try {
private fun fetchFromDb(musicno: String): String? { try {
val db = MuseDatabase()
if (db.open()) {
val url = db.getCloudUrl(musicno)