diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f9198d5..979132a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -31,6 +31,7 @@ = 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 设备 */ private fun isTelevisionDevice(): Boolean { val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as? android.app.UiModeManager diff --git a/app/src/main/java/com/tv/browser/TVBrowserApplication.kt b/app/src/main/java/com/tv/browser/TVBrowserApplication.kt index 3a5b7d6..93f8f82 100644 --- a/app/src/main/java/com/tv/browser/TVBrowserApplication.kt +++ b/app/src/main/java/com/tv/browser/TVBrowserApplication.kt @@ -38,7 +38,7 @@ class TVBrowserApplication : Application() { val dateFormat = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.US) val fileName = "crash_${dateFormat.format(Date())}.txt" val file = File(dir, fileName) - FileWriter(file, Charsets.UTF_8).use { writer -> + file.bufferedWriter(Charsets.UTF_8).use { writer -> val pw = PrintWriter(writer) pw.println("=== 崩溃日志 ${SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date())} ===") pw.println() diff --git a/app/src/main/java/com/tv/browser/VideoResolver.kt b/app/src/main/java/com/tv/browser/VideoResolver.kt index d3ad639..f71e777 100644 --- a/app/src/main/java/com/tv/browser/VideoResolver.kt +++ b/app/src/main/java/com/tv/browser/VideoResolver.kt @@ -10,6 +10,7 @@ import android.util.Log import android.view.ViewGroup import android.webkit.JavascriptInterface import android.webkit.SslErrorHandler +import android.webkit.WebChromeClient import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebSettings @@ -120,6 +121,7 @@ class VideoResolver( if (AdBlockHelper.isEnabled(context) && AdBlockHelper.shouldBlockUrl(url)) { return AdBlockHelper.createEmptyResponse() } + VideoSnifferHelper.createCompatScriptResponse(url)?.let { return it } if (VideoSnifferHelper.isVideoStreamUrl(url)) { handler.post { onCandidate(gen, url, "net", null, "") } } @@ -134,6 +136,10 @@ class VideoResolver( 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?) { if (!hasDocStartHook) view?.evaluateJavascript(VideoSnifferHelper.JS_DOCUMENT_START_HOOK, null) view?.evaluateJavascript(VideoSnifferHelper.JS_SITE_ADAPTER_MACCMS, null) @@ -144,6 +150,11 @@ class VideoResolver( 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 会暂停计时器,导致播放器脚本不执行 container.addView(wv, FrameLayout.LayoutParams(1, 1)) return wv diff --git a/app/src/main/java/com/tv/browser/VideoSnifferHelper.kt b/app/src/main/java/com/tv/browser/VideoSnifferHelper.kt index cc8f3c2..e75e6f0 100644 --- a/app/src/main/java/com/tv/browser/VideoSnifferHelper.kt +++ b/app/src/main/java/com/tv/browser/VideoSnifferHelper.kt @@ -78,6 +78,27 @@ object VideoSnifferHelper { fun createRejectedResponse(): WebResourceResponse = 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 = "央视网视频经过加密,原生播放器会花屏,请直接在网页中播放" @@ -212,6 +233,14 @@ object VideoSnifferHelper { */ val JS_DOCUMENT_START_HOOK = """ (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; window.__tvSniffInstalled = true; var reported = {};