4k
This commit is contained in:
@@ -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 = {};
|
||||||
|
|||||||
Reference in New Issue
Block a user