This commit is contained in:
haomingming
2026-11-09 09:50:20 +08:00
parent a7224d19f0
commit 38d2d7de77
5 changed files with 55 additions and 1 deletions
+1
View File
@@ -31,6 +31,7 @@
<activity <activity
android:name=".BrowserActivity" android:name=".BrowserActivity"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:launchMode="singleTask"
android:configChanges="orientation|screenSize|keyboardHidden" android:configChanges="orientation|screenSize|keyboardHidden"
android:exported="true" android:exported="true"
android:label="@string/app_name" android:label="@string/app_name"
@@ -339,6 +339,8 @@ class BrowserActivity : AppCompatActivity(), RemoteControlCallback {
Log.d("TVBrowser.Filter", "Blocked script: $reqUrl") Log.d("TVBrowser.Filter", "Blocked script: $reqUrl")
return ContentFilterHelper.createEmptyScriptResponse() return ContentFilterHelper.createEmptyScriptResponse()
} }
// 1.3 兼容旧版 WebView:拦截并替换含高版本语法(如 ES2021 `??=`)而导致播放器崩溃的插件脚本
VideoSnifferHelper.createCompatScriptResponse(reqUrl)?.let { return it }
// 2. 视频流嗅探拦截(.m3u8, .mp4 等) // 2. 视频流嗅探拦截(.m3u8, .mp4 等)
if (VideoSnifferHelper.isVideoStreamUrl(reqUrl)) { if (VideoSnifferHelper.isVideoStreamUrl(reqUrl)) {
onVideoSniffed(reqUrl, currentPageTitle, "net") onVideoSniffed(reqUrl, currentPageTitle, "net")
@@ -378,6 +380,8 @@ class BrowserActivity : AppCompatActivity(), RemoteControlCallback {
override fun onPageCommitVisible(view: WebView?, url: String?) { override fun onPageCommitVisible(view: WebView?, url: String?) {
super.onPageCommitVisible(view, url) super.onPageCommitVisible(view, url)
// 新文档 window 刚提交时立即注入嗅探 Hook(onPageStarted 时新 window 可能尚未提交)
if (!hasDocStartHook) view?.evaluateJavascript(VideoSnifferHelper.JS_DOCUMENT_START_HOOK, null)
injectDesktopModeFallback(view) injectDesktopModeFallback(view)
// 新页面首帧可见时再注入一次,尽量在广告图显示前隐藏 // 新页面首帧可见时再注入一次,尽量在广告图显示前隐藏
if (contentFilterActive) view?.evaluateJavascript(ContentFilterHelper.JS_CONTENT_FILTER, null) if (contentFilterActive) view?.evaluateJavascript(ContentFilterHelper.JS_CONTENT_FILTER, null)
@@ -448,6 +452,7 @@ class BrowserActivity : AppCompatActivity(), RemoteControlCallback {
webView.webChromeClient = object : WebChromeClient() { webView.webChromeClient = object : WebChromeClient() {
override fun onProgressChanged(view: WebView?, newProgress: Int) { override fun onProgressChanged(view: WebView?, newProgress: Int) {
progressBar.progress = newProgress progressBar.progress = newProgress
if (!hasDocStartHook) view?.evaluateJavascript(VideoSnifferHelper.JS_DOCUMENT_START_HOOK, null)
if (newProgress >= 100) progressBar.visibility = View.GONE if (newProgress >= 100) progressBar.visibility = View.GONE
} }
@@ -586,6 +591,14 @@ class BrowserActivity : AppCompatActivity(), RemoteControlCallback {
} }
} }
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
setIntent(intent)
if (intent?.action == Intent.ACTION_VIEW && intent.data != null && ::webView.isInitialized) {
loadUrl(intent.data!!.toString())
}
}
/** 是否为 Android TV 设备 */ /** 是否为 Android TV 设备 */
private fun isTelevisionDevice(): Boolean { private fun isTelevisionDevice(): Boolean {
val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as? android.app.UiModeManager val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as? android.app.UiModeManager
@@ -38,7 +38,7 @@ class TVBrowserApplication : Application() {
val dateFormat = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.US) val dateFormat = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.US)
val fileName = "crash_${dateFormat.format(Date())}.txt" val fileName = "crash_${dateFormat.format(Date())}.txt"
val file = File(dir, fileName) val file = File(dir, fileName)
FileWriter(file, Charsets.UTF_8).use { writer -> file.bufferedWriter(Charsets.UTF_8).use { writer ->
val pw = PrintWriter(writer) val pw = PrintWriter(writer)
pw.println("=== 崩溃日志 ${SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date())} ===") pw.println("=== 崩溃日志 ${SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date())} ===")
pw.println() pw.println()
@@ -10,6 +10,7 @@ import android.util.Log
import android.view.ViewGroup import android.view.ViewGroup
import android.webkit.JavascriptInterface import android.webkit.JavascriptInterface
import android.webkit.SslErrorHandler import android.webkit.SslErrorHandler
import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse import android.webkit.WebResourceResponse
import android.webkit.WebSettings import android.webkit.WebSettings
@@ -120,6 +121,7 @@ class VideoResolver(
if (AdBlockHelper.isEnabled(context) && AdBlockHelper.shouldBlockUrl(url)) { if (AdBlockHelper.isEnabled(context) && AdBlockHelper.shouldBlockUrl(url)) {
return AdBlockHelper.createEmptyResponse() return AdBlockHelper.createEmptyResponse()
} }
VideoSnifferHelper.createCompatScriptResponse(url)?.let { return it }
if (VideoSnifferHelper.isVideoStreamUrl(url)) { if (VideoSnifferHelper.isVideoStreamUrl(url)) {
handler.post { onCandidate(gen, url, "net", null, "") } handler.post { onCandidate(gen, url, "net", null, "") }
} }
@@ -134,6 +136,10 @@ class VideoResolver(
if (!hasDocStartHook) view?.evaluateJavascript(VideoSnifferHelper.JS_DOCUMENT_START_HOOK, null) if (!hasDocStartHook) view?.evaluateJavascript(VideoSnifferHelper.JS_DOCUMENT_START_HOOK, null)
} }
override fun onPageCommitVisible(view: WebView?, url: String?) {
if (!hasDocStartHook) view?.evaluateJavascript(VideoSnifferHelper.JS_DOCUMENT_START_HOOK, null)
}
override fun onPageFinished(view: WebView?, url: String?) { override fun onPageFinished(view: WebView?, url: String?) {
if (!hasDocStartHook) view?.evaluateJavascript(VideoSnifferHelper.JS_DOCUMENT_START_HOOK, null) if (!hasDocStartHook) view?.evaluateJavascript(VideoSnifferHelper.JS_DOCUMENT_START_HOOK, null)
view?.evaluateJavascript(VideoSnifferHelper.JS_SITE_ADAPTER_MACCMS, null) view?.evaluateJavascript(VideoSnifferHelper.JS_SITE_ADAPTER_MACCMS, null)
@@ -144,6 +150,11 @@ class VideoResolver(
handler?.proceed() // 与浏览器主界面一致:电视系统时间不准时放行 handler?.proceed() // 与浏览器主界面一致:电视系统时间不准时放行
} }
} }
wv.webChromeClient = object : WebChromeClient() {
override fun onProgressChanged(view: WebView?, newProgress: Int) {
if (!hasDocStartHook) view?.evaluateJavascript(VideoSnifferHelper.JS_DOCUMENT_START_HOOK, null)
}
}
// 1x1 挂到窗口上:部分内核对未挂载的 WebView 会暂停计时器,导致播放器脚本不执行 // 1x1 挂到窗口上:部分内核对未挂载的 WebView 会暂停计时器,导致播放器脚本不执行
container.addView(wv, FrameLayout.LayoutParams(1, 1)) container.addView(wv, FrameLayout.LayoutParams(1, 1))
return wv return wv
@@ -78,6 +78,27 @@ object VideoSnifferHelper {
fun createRejectedResponse(): WebResourceResponse = fun createRejectedResponse(): WebResourceResponse =
WebResourceResponse("text/plain", "utf-8", 403, "Blocked", emptyMap(), ByteArrayInputStream(ByteArray(0))) WebResourceResponse("text/plain", "utf-8", 403, "Blocked", emptyMap(), ByteArrayInputStream(ByteArray(0)))
/**
* 针对旧版系统 WebView(Chromium < 85,不支持 `??=` / `||=` 等 ES2021 语法)的播放器插件兼容替换:
* 例如 4K影视(4kvm.tv)引入的 `artplayer-plugin-danmaku*.js` 含有 `??=`,在电视旧版 WebView 中会报
* `Uncaught SyntaxError: Unexpected token '='`,进而导致主脚本调用 `artplayerPluginDanmaku(...)` 时抛
* `ReferenceError: artplayerPluginDanmaku is not defined`,在创建 Artplayer / hls.js 前直接中断。
* 返回无语法兼容问题且免除弹幕 DOM 开销的同名空插件,确保播放器与 m3u8 加载正常执行。
*/
private const val JS_DANMAKU_COMPAT_STUB =
"window.artplayerPluginDanmaku=function(){return function(){var n=function(){};return{name:'artplayerPluginDanmaku',send:n,load:n,clear:n,toggle:n,setOpacity:n,destroy:n,push:n,manager:null,container:null}}};"
fun createCompatScriptResponse(url: String): WebResourceResponse? {
if (url.contains("artplayer-plugin-danmaku", ignoreCase = true)) {
return WebResourceResponse(
"application/javascript",
"utf-8",
ByteArrayInputStream(JS_DANMAKU_COMPAT_STUB.toByteArray(Charsets.UTF_8))
)
}
return null
}
/** 原生播放器无法播放时给用户的提示 */ /** 原生播放器无法播放时给用户的提示 */
const val WEB_ONLY_MESSAGE = "央视网视频经过加密,原生播放器会花屏,请直接在网页中播放" const val WEB_ONLY_MESSAGE = "央视网视频经过加密,原生播放器会花屏,请直接在网页中播放"
@@ -212,6 +233,14 @@ object VideoSnifferHelper {
*/ */
val JS_DOCUMENT_START_HOOK = """ val JS_DOCUMENT_START_HOOK = """
(function() { (function() {
if (typeof window.artplayerPluginDanmaku !== 'function') {
window.artplayerPluginDanmaku = function() {
return function() {
var n = function() {};
return { name: 'artplayerPluginDanmaku', send: n, load: n, clear: n, toggle: n, setOpacity: n, destroy: n, push: n, manager: null, container: null };
};
};
}
if (window.__tvSniffInstalled) return; if (window.__tvSniffInstalled) return;
window.__tvSniffInstalled = true; window.__tvSniffInstalled = true;
var reported = {}; var reported = {};