v8.8.0 切台提速:每频道独立锁(预取不再阻塞)+TTL30min+只预取高亮频道+缓存失效自动刷新源
This commit is contained in:
1 parent
bf4c32badc
commit
f1efc5dc5d
3 files changed
+58
-21
No files matched your search
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId = "com.iptv807.tv"
|
applicationId = "com.iptv807.tv"
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 870
|
versionCode = 880
|
||||||
versionName = "8.7.0"
|
versionName = "8.8.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ class MainActivity : Activity() {
|
|||||||
override fun run() {
|
override fun run() {
|
||||||
if (loadingMask.visibility != View.VISIBLE) return
|
if (loadingMask.visibility != View.VISIBLE) return
|
||||||
val s = ((System.currentTimeMillis() - loadStart) / 1000 + 1).toInt()
|
val s = ((System.currentTimeMillis() - loadStart) / 1000 + 1).toInt()
|
||||||
loadingText.text = "正在加载 $s 秒,精彩继续…"
|
loadingText.text = if (loadChannelName.isEmpty()) "正在加载 $s 秒,精彩继续…"
|
||||||
|
else "正在加载「$loadChannelName」$s 秒…"
|
||||||
handler.postDelayed(this, 1000)
|
handler.postDelayed(this, 1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,6 +82,8 @@ class MainActivity : Activity() {
|
|||||||
private var isVip = false
|
private var isVip = false
|
||||||
private var lastPlayError = ""
|
private var lastPlayError = ""
|
||||||
private var lastSelfTest = ""
|
private var lastSelfTest = ""
|
||||||
|
private var loadChannelName = ""
|
||||||
|
private var triedFresh = false
|
||||||
private var failureReport = ""
|
private var failureReport = ""
|
||||||
private var wasStopped = false
|
private var wasStopped = false
|
||||||
private var resumeBtn: TextView? = null
|
private var resumeBtn: TextView? = null
|
||||||
@@ -228,10 +231,8 @@ class MainActivity : Activity() {
|
|||||||
if (savedInstanceState == null && channels.isNotEmpty()) {
|
if (savedInstanceState == null && channels.isNotEmpty()) {
|
||||||
val defIdx = channels.indexOfFirst { it.name.contains("TVB翡翠台") }.let { if (it >= 0) it else 0 }
|
val defIdx = channels.indexOfFirst { it.name.contains("TVB翡翠台") }.let { if (it >= 0) it else 0 }
|
||||||
playChannel(defIdx)
|
playChannel(defIdx)
|
||||||
// 启动后台预取默认频道所在分组,之后切台秒开
|
// 启动后预取默认频道的相邻频道(很轻量,1 个一个来)
|
||||||
val g = channels.getOrNull(defIdx)?.group
|
handler.postDelayed({ schedulePrefetch(channels.getOrNull(defIdx)) }, 2000)
|
||||||
val gi = ChannelData.groups().indexOfFirst { it.name == g }
|
|
||||||
if (gi >= 0) handler.postDelayed({ prefetchGroup(gi) }, 2500)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,6 +423,13 @@ class MainActivity : Activity() {
|
|||||||
if (ev.action == MotionEvent.ACTION_DOWN || ev.action == MotionEvent.ACTION_MOVE) resetHideTimer()
|
if (ev.action == MotionEvent.ACTION_DOWN || ev.action == MotionEvent.ACTION_MOVE) resetHideTimer()
|
||||||
v.onTouchEvent(ev)
|
v.onTouchEvent(ev)
|
||||||
}
|
}
|
||||||
|
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
|
override fun onItemSelected(parent: AdapterView<*>?, v: View?, pos: Int, id: Long) {
|
||||||
|
val list = channelsOfGroup(curGroupPos)
|
||||||
|
if (pos in list.indices) schedulePrefetch(list[pos])
|
||||||
|
}
|
||||||
|
override fun onNothingSelected(parent: AdapterView<*>?) { }
|
||||||
|
}
|
||||||
setOnScrollListener(object : AbsListView.OnScrollListener {
|
setOnScrollListener(object : AbsListView.OnScrollListener {
|
||||||
override fun onScrollStateChanged(v: AbsListView, s: Int) { resetHideTimer() }
|
override fun onScrollStateChanged(v: AbsListView, s: Int) { resetHideTimer() }
|
||||||
override fun onScroll(v: AbsListView, a: Int, b: Int, c: Int) { resetHideTimer() }
|
override fun onScroll(v: AbsListView, a: Int, b: Int, c: Int) { resetHideTimer() }
|
||||||
@@ -432,26 +440,27 @@ class MainActivity : Activity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateChannelListOfGroup(pos: Int) {
|
private fun updateChannelListOfGroup(pos: Int) {
|
||||||
prefetchGroup(pos)
|
|
||||||
val list = channelsOfGroup(pos)
|
val list = channelsOfGroup(pos)
|
||||||
channelList?.adapter = RowAdapter(list.map { "%3d %s".format(it.num, it.name) })
|
channelList?.adapter = RowAdapter(list.map { "%3d %s".format(it.num, it.name) })
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hidePanel() { handler.removeCallbacks(hidePanelTask); panelLayout?.visibility = View.GONE }
|
private fun hidePanel() { handler.removeCallbacks(hidePanelTask); panelLayout?.visibility = View.GONE }
|
||||||
|
|
||||||
/** 后台预取某分组所有频道的源,让切台变秒切(每次运行只做一轮) */
|
/** 只预取"当前高亮的那一个频道"(1.2 秒去抖动)——源站很脆,不做整组预取 */
|
||||||
private var prefetchedGroups = HashSet<Int>()
|
private val prefetchTask = Runnable {
|
||||||
private fun prefetchGroup(gpos: Int) {
|
val ch = prefetchTarget ?: return@Runnable
|
||||||
if (prefetchedGroups.contains(gpos)) return
|
if (SourceResolver.hasCache(ch.tid, ch.id)) return@Runnable
|
||||||
prefetchedGroups.add(gpos)
|
|
||||||
val list = channelsOfGroup(gpos)
|
|
||||||
Thread {
|
Thread {
|
||||||
for (ch in list) {
|
try { SourceResolver.resolveLines(ch.tid, ch.id) } catch (e: Exception) { }
|
||||||
try { SourceResolver.resolveLines(ch.tid, ch.id) } catch (e: Exception) { }
|
|
||||||
try { Thread.sleep(250) } catch (ie: InterruptedException) { }
|
|
||||||
}
|
|
||||||
}.start()
|
}.start()
|
||||||
}
|
}
|
||||||
|
private var prefetchTarget: ChannelData.Channel? = null
|
||||||
|
private fun schedulePrefetch(ch: ChannelData.Channel?) {
|
||||||
|
if (ch == null) return
|
||||||
|
prefetchTarget = ch
|
||||||
|
handler.removeCallbacks(prefetchTask)
|
||||||
|
handler.postDelayed(prefetchTask, 1200)
|
||||||
|
}
|
||||||
private fun resetHideTimer() {
|
private fun resetHideTimer() {
|
||||||
handler.removeCallbacks(hidePanelTask)
|
handler.removeCallbacks(hidePanelTask)
|
||||||
handler.postDelayed(hidePanelTask, 5000)
|
handler.postDelayed(hidePanelTask, 5000)
|
||||||
@@ -473,6 +482,8 @@ class MainActivity : Activity() {
|
|||||||
val isSameChannel = (idx == currentChannel)
|
val isSameChannel = (idx == currentChannel)
|
||||||
currentChannel = idx; currentLine = line
|
currentChannel = idx; currentLine = line
|
||||||
val ch = channels[idx]
|
val ch = channels[idx]
|
||||||
|
if (!isSameChannel) triedFresh = false
|
||||||
|
loadChannelName = ch.name
|
||||||
if (!isSameChannel || loadingMask.visibility != View.VISIBLE) {
|
if (!isSameChannel || loadingMask.visibility != View.VISIBLE) {
|
||||||
beginLoading() // 只有换台才重置计时;同台换线路时继续计时
|
beginLoading() // 只有换台才重置计时;同台换线路时继续计时
|
||||||
}
|
}
|
||||||
@@ -565,8 +576,25 @@ class MainActivity : Activity() {
|
|||||||
val ch = channels.getOrNull(currentChannel) ?: return
|
val ch = channels.getOrNull(currentChannel) ?: return
|
||||||
currentLine++
|
currentLine++
|
||||||
if (currentLine > 12) {
|
if (currentLine > 12) {
|
||||||
|
// 可能是缓存里的 token 过期了 → 强制刷新一次源再试一轮
|
||||||
|
if (!triedFresh) {
|
||||||
|
triedFresh = true
|
||||||
|
statusText.text = "线路失效,正在刷新源…"
|
||||||
|
Thread {
|
||||||
|
val fresh = try { SourceResolver.resolveFresh(ch.tid, ch.id) } catch (e: Exception) { null }
|
||||||
|
runOnUiThread {
|
||||||
|
if (!fresh.isNullOrEmpty()) { currentLine = 0; playChannel(currentChannel, 0) }
|
||||||
|
else {
|
||||||
|
val vn = try { packageManager.getPackageInfo(packageName, 0).versionName } catch (e: Exception) { "?" }
|
||||||
|
statusText.text = "⚠ ${ch.num} ${ch.name} 全部线路失败\n$lastPlayError\n解析: ${SourceResolver.lastError}\n版本: v$vn"
|
||||||
|
hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
return
|
||||||
|
}
|
||||||
val vn = try { packageManager.getPackageInfo(packageName, 0).versionName } catch (e: Exception) { "?" }
|
val vn = try { packageManager.getPackageInfo(packageName, 0).versionName } catch (e: Exception) { "?" }
|
||||||
statusText.text = "⚠ ${ch.num} ${ch.name} 全部线路失败\n$lastPlayError\n解析: ${SourceResolver.lastError}\n$failureReport\n版本: v$vn"
|
statusText.text = "⚠ ${ch.num} ${ch.name} 全部线路失败\n$lastPlayError\n解析: ${SourceResolver.lastError}\n版本: v$vn"
|
||||||
hideLoading()
|
hideLoading()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ object SourceResolver {
|
|||||||
|
|
||||||
@Volatile private var cache: MutableMap<String, Cached>? = null
|
@Volatile private var cache: MutableMap<String, Cached>? = null
|
||||||
private data class Cached(val lines: List<String>, val ts: Long)
|
private data class Cached(val lines: List<String>, val ts: Long)
|
||||||
private const val TTL = 8 * 60 * 1000L
|
/** 每频道独立锁:避免后台预取把用户切台的请求挡住 */
|
||||||
|
private val locks = ConcurrentHashMap<String, Any>()
|
||||||
|
private const val TTL = 30 * 60 * 1000L
|
||||||
|
|
||||||
private const val BASE = "https://m.iptv807.com/"
|
private const val BASE = "https://m.iptv807.com/"
|
||||||
private const val UA = "Mozilla/5.0 (Linux; Android 12; SM-S901B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36"
|
private const val UA = "Mozilla/5.0 (Linux; Android 12; SM-S901B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36"
|
||||||
@@ -56,6 +58,12 @@ object SourceResolver {
|
|||||||
/** 兼容之前的 method signature */
|
/** 兼容之前的 method signature */
|
||||||
fun resolve(tid: String, id: String): List<String>? = resolveLines(tid, id)
|
fun resolve(tid: String, id: String): List<String>? = resolveLines(tid, id)
|
||||||
|
|
||||||
|
/** 该频道是否已有可用缓存(不触发网络请求) */
|
||||||
|
fun hasCache(tid: String, id: String): Boolean {
|
||||||
|
val c = cache?.get("$tid-$id") ?: return false
|
||||||
|
return System.currentTimeMillis() - c.ts < TTL
|
||||||
|
}
|
||||||
|
|
||||||
/** 强制丢弃缓存重新抓源(用于"缓存线路全失效"时) */
|
/** 强制丢弃缓存重新抓源(用于"缓存线路全失效"时) */
|
||||||
fun resolveFresh(tid: String, id: String): List<String>? {
|
fun resolveFresh(tid: String, id: String): List<String>? {
|
||||||
cache?.remove("$tid-$id")
|
cache?.remove("$tid-$id")
|
||||||
@@ -66,7 +74,8 @@ object SourceResolver {
|
|||||||
val key = "$tid-$id"
|
val key = "$tid-$id"
|
||||||
cache?.get(key)?.let { if (System.currentTimeMillis() - it.ts < TTL) return it.lines }
|
cache?.get(key)?.let { if (System.currentTimeMillis() - it.ts < TTL) return it.lines }
|
||||||
|
|
||||||
synchronized(this) {
|
val lock = locks.getOrPut(key) { Any() }
|
||||||
|
synchronized(lock) {
|
||||||
// double-check
|
// double-check
|
||||||
cache?.get(key)?.let { if (System.currentTimeMillis() - it.ts < TTL) return it.lines }
|
cache?.get(key)?.let { if (System.currentTimeMillis() - it.ts < TTL) return it.lines }
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in new issue
Block a user