4k
This commit is contained in:
@@ -144,10 +144,12 @@ class BrowserActivity : AppCompatActivity(), RemoteControlCallback {
|
||||
@Volatile private var contentFilterActive: Boolean = false
|
||||
|
||||
/**
|
||||
* 已切到原生 / 外部播放器、网页视频被暂停:期间拒绝网页的子资源请求(shouldInterceptRequest 在后台线程读取)。
|
||||
* 已切到原生 / 外部播放器或退到后台、网页视频被暂停:期间拒绝网页的子资源请求(shouldInterceptRequest 在后台线程读取)。
|
||||
* 网页播放器暂停后仍会继续缓冲分片(部分站点还带 P2P 加速),会和原生播放器抢网速;回到本页或打开新页面时解除。
|
||||
*/
|
||||
@Volatile private var webMediaSuspended: Boolean = false
|
||||
/** 当前 Activity 是否处于前台可见状态(onResume ~ onPause),供后台网络拦截与页面完成回调判断是否立即暂停网页媒体 */
|
||||
@Volatile private var isActivityResumed: Boolean = false
|
||||
|
||||
/** 防跳转劫持:用户最近一次真实点击的链接与时间(JS 线程写入,UI 线程读取) */
|
||||
@Volatile private var lastUserLink: String? = null
|
||||
@@ -316,10 +318,10 @@ class BrowserActivity : AppCompatActivity(), RemoteControlCallback {
|
||||
request: WebResourceRequest?
|
||||
): WebResourceResponse? {
|
||||
val reqUrl = request?.url?.toString() ?: return null
|
||||
// 原生 / 外部播放器播放中:网页不再加载任何子资源(分片、清单刷新、P2P 回源、广告轮询),把网速留给播放器
|
||||
// 原生 / 外部播放器播放中或已退到后台:网页不再加载任何子资源(分片、清单刷新、P2P 回源、广告轮询),把网速留给播放器或避免后台偷跑
|
||||
if (request.isForMainFrame) {
|
||||
webMediaSuspended = false
|
||||
} else if (webMediaSuspended) {
|
||||
if (isActivityResumed) webMediaSuspended = false
|
||||
} else if (webMediaSuspended || !isActivityResumed) {
|
||||
return VideoSnifferHelper.createRejectedResponse()
|
||||
}
|
||||
// 0. 记录请求头,原生播放器回放时使用(Referer / UA 等)
|
||||
@@ -376,6 +378,10 @@ class BrowserActivity : AppCompatActivity(), RemoteControlCallback {
|
||||
// 强力过滤:隐藏可疑动图 / 色情博彩广告
|
||||
updateContentFilterState(url)
|
||||
if (contentFilterActive) view?.evaluateJavascript(ContentFilterHelper.JS_CONTENT_FILTER, null)
|
||||
if (!isActivityResumed) {
|
||||
suspendWebMedia()
|
||||
view?.onPause()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPageCommitVisible(view: WebView?, url: String?) {
|
||||
@@ -385,6 +391,10 @@ class BrowserActivity : AppCompatActivity(), RemoteControlCallback {
|
||||
injectDesktopModeFallback(view)
|
||||
// 新页面首帧可见时再注入一次,尽量在广告图显示前隐藏
|
||||
if (contentFilterActive) view?.evaluateJavascript(ContentFilterHelper.JS_CONTENT_FILTER, null)
|
||||
if (!isActivityResumed) {
|
||||
suspendWebMedia()
|
||||
view?.onPause()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
@@ -404,6 +414,10 @@ class BrowserActivity : AppCompatActivity(), RemoteControlCallback {
|
||||
view?.evaluateJavascript(VideoSnifferHelper.JS_SITE_ADAPTER_MACCMS, null)
|
||||
url?.let { injectUserScripts(it) }
|
||||
url?.let { historyRepository.add(it, currentPageTitle.ifEmpty { it }) }
|
||||
if (!isActivityResumed) {
|
||||
suspendWebMedia()
|
||||
view?.onPause()
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
@@ -2554,8 +2568,12 @@ class BrowserActivity : AppCompatActivity(), RemoteControlCallback {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// 从原生 / 外部播放器回来:恢复网页加载
|
||||
if (::webView.isInitialized) resumeWebMediaIfSuspended()
|
||||
isActivityResumed = true
|
||||
// 从后台或原生 / 外部播放器回来:恢复 WebView 与网页加载
|
||||
if (::webView.isInitialized) {
|
||||
webView.onResume()
|
||||
resumeWebMediaIfSuspended()
|
||||
}
|
||||
// 从播放器返回首页时刷新追剧列表(进度 / 下一集)
|
||||
if (isOnHomePage && ::binding.isInitialized) updateContinueWatchingBanner()
|
||||
// 自愈:回到前台时重新计算光标显示状态
|
||||
@@ -2563,10 +2581,25 @@ class BrowserActivity : AppCompatActivity(), RemoteControlCallback {
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
isActivityResumed = false
|
||||
cancelAutoPlay(markHandled = true)
|
||||
if (::webView.isInitialized) {
|
||||
suspendWebMedia()
|
||||
webView.onPause()
|
||||
}
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
isActivityResumed = false
|
||||
cancelAutoPlay(markHandled = true)
|
||||
if (::webView.isInitialized) {
|
||||
suspendWebMedia()
|
||||
webView.onPause()
|
||||
}
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onBackPressed() {
|
||||
// 自动接管播放倒计时中:返回键只取消倒计时
|
||||
|
||||
@@ -94,6 +94,8 @@ class PlayerActivity : AppCompatActivity() {
|
||||
/** 当前片源是否已做过片头检查 / 已触发片尾 */
|
||||
private var introChecked = false
|
||||
private var outroTriggered = false
|
||||
/** 是否已退到后台(按 Home 键等触发 onPause / onStop,非画中画状态):防止后台异步解析完成后自动出声播放 */
|
||||
private var isInBackground = false
|
||||
|
||||
/** 下一集预解析:独立的后台解析器(VideoResolver 一个实例同时只能解析一个页面) */
|
||||
private var preloadResolver: VideoResolver? = null
|
||||
@@ -250,6 +252,8 @@ class PlayerActivity : AppCompatActivity() {
|
||||
private fun initPlayer() {
|
||||
val p = ExoPlayer.Builder(this, createRenderersFactory())
|
||||
.setLoadControl(PlayerTuning.buildLoadControl())
|
||||
.setAudioAttributes(androidx.media3.common.AudioAttributes.DEFAULT, true)
|
||||
.setHandleAudioBecomingNoisy(true)
|
||||
.build()
|
||||
p.addAnalyticsListener(diagnostics)
|
||||
binding.playerView.player = p
|
||||
@@ -407,16 +411,20 @@ class PlayerActivity : AppCompatActivity() {
|
||||
p.setMediaSource(mediaSource, startPos)
|
||||
p.setPlaybackSpeed(speeds[currentSpeedIndex])
|
||||
p.prepare()
|
||||
p.playWhenReady = true
|
||||
val canPlayNow = !isInBackground
|
||||
p.playWhenReady = canPlayNow
|
||||
if (!canPlayNow) p.pause()
|
||||
if (carried <= 0 && record != null && startPos > 0) showFeedback("已自动续播至 ${record.formatPosition()}")
|
||||
|
||||
Log.d(TAG, "play: ${source.url} hls=${source.isHls} headers=${source.headers.keys}")
|
||||
Log.d(TAG, "play: ${source.url} hls=${source.isHls} canPlayNow=$canPlayNow headers=${source.headers.keys}")
|
||||
lastSavedSec = 0
|
||||
suppressSave = false
|
||||
introChecked = false
|
||||
outroTriggered = false
|
||||
mainHandler.removeCallbacks(updateProgressRunnable)
|
||||
mainHandler.post(updateProgressRunnable)
|
||||
if (canPlayNow) mainHandler.post(updateProgressRunnable)
|
||||
updatePlayPauseButtonText()
|
||||
updatePauseOverlay()
|
||||
updateEpisodeUi()
|
||||
showOsdTemporarily()
|
||||
}
|
||||
@@ -456,7 +464,11 @@ class PlayerActivity : AppCompatActivity() {
|
||||
private fun handlePlayerError(error: PlaybackException) {
|
||||
binding.playerLoading.visibility = View.GONE
|
||||
val cause = PlayerSsl.rootCauseText(error)
|
||||
Log.e(TAG, "playback error: ${error.errorCodeName} cause=$cause", error)
|
||||
Log.e(TAG, "playback error: ${error.errorCodeName} cause=$cause inBg=$isInBackground", error)
|
||||
if (isInBackground) {
|
||||
pauseForBackground()
|
||||
return
|
||||
}
|
||||
val page = currentPageUrl
|
||||
// 证书 / SSL 握手失败:网页会忽略证书错误继续播,原生播放器这里同样放宽后重试一次
|
||||
if (PlayerSsl.isSslError(error) && PlayerSsl.enableLenient()) {
|
||||
@@ -711,6 +723,7 @@ class PlayerActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun maybeSkipOutro(current: Long, duration: Long) {
|
||||
if (isInBackground || player?.isPlaying != true) return
|
||||
if (outroTriggered || duration <= 0 || !hasNextEpisode()) return
|
||||
val outro = AppSettings.getSkip(this, skipKey()).outroMs
|
||||
if (outro <= 0 || outro >= duration) return
|
||||
@@ -1209,17 +1222,19 @@ class PlayerActivity : AppCompatActivity() {
|
||||
override fun onUserLeaveHint() {
|
||||
super.onUserLeaveHint()
|
||||
val inPip = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInPictureInPictureMode
|
||||
if (!inPip) pauseForBackground()
|
||||
if (!inPip) {
|
||||
isInBackground = true
|
||||
pauseForBackground()
|
||||
}
|
||||
}
|
||||
|
||||
/** 切到后台时暂停播放,并同步按钮文字 / 暂停浮层,回到前台时保持暂停状态 */
|
||||
private fun pauseForBackground() {
|
||||
val p = player ?: return
|
||||
if (p.playWhenReady) {
|
||||
p.playWhenReady = false
|
||||
p.pause()
|
||||
updatePlayPauseButtonText()
|
||||
updatePauseOverlay()
|
||||
}
|
||||
saveCurrentProgress()
|
||||
}
|
||||
|
||||
@@ -1431,17 +1446,30 @@ class PlayerActivity : AppCompatActivity() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
isInBackground = false
|
||||
hideSystemUI()
|
||||
if (currentSource != null) {
|
||||
mainHandler.removeCallbacks(updateProgressRunnable)
|
||||
mainHandler.post(updateProgressRunnable)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
val inPip = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) isInPictureInPictureMode else false
|
||||
if (!inPip) pauseForBackground() else saveCurrentProgress()
|
||||
if (!inPip) {
|
||||
isInBackground = true
|
||||
pauseForBackground()
|
||||
} else {
|
||||
saveCurrentProgress()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
isInBackground = true
|
||||
mainHandler.removeCallbacks(updateProgressRunnable)
|
||||
cancelPreload()
|
||||
// 兜底:任何情况下界面完全不可见(含关闭画中画小窗)都暂停,避免后台继续出声
|
||||
pauseForBackground()
|
||||
}
|
||||
|
||||
@@ -325,13 +325,20 @@ object VideoSnifferHelper {
|
||||
};
|
||||
}
|
||||
} catch (e) {}
|
||||
// 记录页面里的 hls.js 实例:切到原生播放器时 stopLoad(),否则网页播放器暂停后仍会在后台继续缓冲分片,和原生播放器抢网速
|
||||
// 记录页面里的 hls.js 实例:切到原生播放器或退到后台时 stopLoad(),否则网页播放器暂停后仍会在后台继续缓冲分片,和原生播放器抢网速
|
||||
var hlsList = [];
|
||||
var mediaSuspended = false;
|
||||
function trackHls(h) {
|
||||
try {
|
||||
if (!h || hlsList.indexOf(h) >= 0) return;
|
||||
if (!h) return;
|
||||
if (hlsList.indexOf(h) < 0) {
|
||||
hlsList.push(h);
|
||||
if (hlsList.length > 20) hlsList.shift();
|
||||
}
|
||||
if (mediaSuspended && !h.__tvSuspended && typeof h.stopLoad === 'function') {
|
||||
h.stopLoad();
|
||||
h.__tvSuspended = true;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
function wrapHls(H) {
|
||||
@@ -372,6 +379,27 @@ object VideoSnifferHelper {
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
try {
|
||||
var origPlay = HTMLMediaElement.prototype.play;
|
||||
if (typeof origPlay === 'function') {
|
||||
HTMLMediaElement.prototype.play = function() {
|
||||
if (mediaSuspended) {
|
||||
try { this.pause(); } catch (_) {}
|
||||
return window.Promise ? Promise.resolve() : undefined;
|
||||
}
|
||||
return origPlay.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
function onMediaPlay(ev) {
|
||||
if (!mediaSuspended) return;
|
||||
var t = ev && ev.target;
|
||||
if (t && typeof t.pause === 'function') {
|
||||
try { t.pause(); } catch (_) {}
|
||||
}
|
||||
}
|
||||
document.addEventListener('play', onMediaPlay, true);
|
||||
document.addEventListener('playing', onMediaPlay, true);
|
||||
} catch (e) {}
|
||||
try {
|
||||
window.addEventListener('message', function(e) {
|
||||
try {
|
||||
@@ -379,6 +407,7 @@ object VideoSnifferHelper {
|
||||
if (!data || (data.__tvcmd !== 'pause' && data.__tvcmd !== 'resume')) return;
|
||||
var i;
|
||||
if (data.__tvcmd === 'pause') {
|
||||
mediaSuspended = true;
|
||||
var vs = document.querySelectorAll('video,audio');
|
||||
for (i = 0; i < vs.length; i++) { try { vs[i].pause(); } catch (_) {} }
|
||||
// 暂停后 hls.js 仍会缓冲到上限(不少站点设到几分钟),停止加载,回到网页时再恢复
|
||||
@@ -386,6 +415,7 @@ object VideoSnifferHelper {
|
||||
try { if (!hlsList[i].__tvSuspended) { hlsList[i].stopLoad(); hlsList[i].__tvSuspended = true; } } catch (_) {}
|
||||
}
|
||||
} else {
|
||||
mediaSuspended = false;
|
||||
for (i = 0; i < hlsList.length; i++) {
|
||||
try { if (hlsList[i].__tvSuspended) { hlsList[i].__tvSuspended = false; hlsList[i].startLoad(); } } catch (_) {}
|
||||
}
|
||||
@@ -408,7 +438,13 @@ object VideoSnifferHelper {
|
||||
for (var k = 0; k < fs.length; k++) { try { pauseIn(fs[k].contentDocument); } catch (e) {} }
|
||||
}
|
||||
try { pauseIn(document); } catch (e) {}
|
||||
try { window.postMessage({ __tvcmd: 'pause' }, '*'); } catch (e) {}
|
||||
var cmd = { __tvcmd: 'pause' };
|
||||
try { window.postMessage(cmd, '*'); } catch (e) {}
|
||||
try {
|
||||
for (var j = 0; j < window.frames.length; j++) {
|
||||
try { window.frames[j].postMessage(cmd, '*'); } catch (e) {}
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
""".trimIndent()
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package com.tv.browser
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.webkit.WebSettings
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.WebView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.tv.browser.databinding.ActivityVideoWebBinding
|
||||
@@ -13,10 +12,12 @@ import com.tv.browser.databinding.ActivityVideoWebBinding
|
||||
*/
|
||||
class VideoWebActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityVideoWebBinding
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val binding = ActivityVideoWebBinding.inflate(layoutInflater)
|
||||
binding = ActivityVideoWebBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
val url = intent.getStringExtra(EXTRA_VIDEO_URL) ?: ""
|
||||
@@ -59,6 +60,44 @@ class VideoWebActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (::binding.isInitialized) {
|
||||
binding.webView.onResume()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
if (::binding.isInitialized) {
|
||||
try {
|
||||
binding.webView.evaluateJavascript(VideoSnifferHelper.JS_PAUSE_ALL_MEDIA, null)
|
||||
binding.webView.onPause()
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
if (::binding.isInitialized) {
|
||||
try {
|
||||
binding.webView.evaluateJavascript(VideoSnifferHelper.JS_PAUSE_ALL_MEDIA, null)
|
||||
binding.webView.onPause()
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (::binding.isInitialized) {
|
||||
try {
|
||||
binding.webView.stopLoading()
|
||||
(binding.webView.parent as? ViewGroup)?.removeView(binding.webView)
|
||||
binding.webView.destroy()
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun escapeHtmlAttr(s: String): String {
|
||||
return s.replace("&", "&")
|
||||
.replace("\"", """)
|
||||
|
||||
Reference in New Issue
Block a user