Skip to content

Commit

Permalink
✨ 超时换源、断线重连
Browse files Browse the repository at this point in the history
  • Loading branch information
yaoxieyoulei committed Jul 2, 2024
1 parent bc734c9 commit 7acbc74
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ class LeanbackMainContentState(
// 从记忆中删除不可播放的域名
SP.iptvPlayableHostList -= getUrlHost(_currentIptv.urlList[_currentIptvUrlIdx])
}

videoPlayerState.onCutoff {
changeCurrentIptv(_currentIptv, _currentIptvUrlIdx)
}
}

private fun getPrevIptv(): Iptv {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,4 +225,12 @@ class LeanbackSettingsViewModel : ViewModel() {
_videoPlayerUserAgent = value
SP.videoPlayerUserAgent = value
}

private var _videoPlayerLoadTimeout by mutableLongStateOf(SP.videoPlayerLoadTimeout)
var videoPlayerLoadTimeout: Long
get() = _videoPlayerLoadTimeout
set(value) {
_videoPlayerLoadTimeout = value
SP.videoPlayerLoadTimeout = value
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.tv.foundation.lazy.list.TvLazyColumn
import top.yogiczy.mytv.data.utils.Constants
import top.yogiczy.mytv.ui.screens.leanback.settings.LeanbackSettingsViewModel
import top.yogiczy.mytv.ui.theme.LeanbackTheme
import top.yogiczy.mytv.ui.utils.SP
import top.yogiczy.mytv.utils.humanizeMs
import kotlin.math.max

@Composable
fun LeanbackSettingsCategoryVideoPlayer(
Expand All @@ -27,10 +27,18 @@ fun LeanbackSettingsCategoryVideoPlayer(
contentPadding = PaddingValues(vertical = 10.dp),
) {
item {
val min = 1000 * 5L
val max = 1000 * 30L
val step = 1000 * 5L

LeanbackSettingsCategoryListItem(
headlineContent = "播放器加载超时",
trailingContent = Constants.VIDEO_PLAYER_LOAD_TIMEOUT.humanizeMs(),
locK = true,
supportingContent = "影响超时换源、断线重连",
trailingContent = settingsViewModel.videoPlayerLoadTimeout.humanizeMs(),
onSelected = {
settingsViewModel.videoPlayerLoadTimeout =
max(min, (settingsViewModel.videoPlayerLoadTimeout + step) % (max + step))
},
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
Expand Down Expand Up @@ -49,6 +50,7 @@ class LeanbackVideoPlayerState(private val instance: LeanbackVideoPlayer) {

private val onReadyListeners = mutableListOf<() -> Unit>()
private val onErrorListeners = mutableListOf<() -> Unit>()
private val onCutoffListeners = mutableListOf<() -> Unit>()

fun onReady(listener: () -> Unit) {
onReadyListeners.add(listener)
Expand All @@ -58,6 +60,10 @@ class LeanbackVideoPlayerState(private val instance: LeanbackVideoPlayer) {
onErrorListeners.add(listener)
}

fun onCutoff(listener: () -> Unit) {
onCutoffListeners.add(listener)
}

fun initialize() {
instance.initialize()
instance.onResolution { width, height ->
Expand All @@ -69,20 +75,14 @@ class LeanbackVideoPlayerState(private val instance: LeanbackVideoPlayer) {
error = if (ex != null) "${ex.errorCodeName}(${ex.errorCode})"
else null

if (error != null) {
onErrorListeners.forEach { it.invoke() }
}
}
instance.onReady {
onReadyListeners.forEach { it.invoke() }
}
instance.onBuffering {
if (it) {
error = null
}
if (error != null) onErrorListeners.forEach { it.invoke() }

}
instance.onReady { onReadyListeners.forEach { it.invoke() } }
instance.onBuffering { if (it) error = null }
instance.onPrepared { }
instance.onMetadata { metadata = it }
instance.onCutoff { onCutoffListeners.forEach { it.invoke() } }
}

fun release() {
Expand All @@ -96,8 +96,9 @@ class LeanbackVideoPlayerState(private val instance: LeanbackVideoPlayer) {
fun rememberLeanbackVideoPlayerState(): LeanbackVideoPlayerState {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val coroutineScope = rememberCoroutineScope()
val state = remember {
LeanbackVideoPlayerState(LeanbackMedia3VideoPlayer(context))
LeanbackVideoPlayerState(LeanbackMedia3VideoPlayer(context, coroutineScope))
}

DisposableEffect(Unit) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,18 @@ import androidx.media3.exoplayer.analytics.AnalyticsListener
import androidx.media3.exoplayer.hls.HlsMediaSource
import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.exoplayer.util.EventLogger
import top.yogiczy.mytv.data.utils.Constants
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import top.yogiczy.mytv.ui.utils.SP
import androidx.media3.common.PlaybackException as Media3PlaybackException

@OptIn(UnstableApi::class)
class LeanbackMedia3VideoPlayer(
private val context: Context,
) : LeanbackVideoPlayer() {
private val coroutineScope: CoroutineScope,
) : LeanbackVideoPlayer(coroutineScope) {
private val videoPlayer = ExoPlayer.Builder(
context,
DefaultRenderersFactory(context).setExtensionRendererMode(EXTENSION_RENDERER_MODE_ON)
Expand All @@ -37,14 +41,15 @@ class LeanbackMedia3VideoPlayer(
}

private val contentTypeAttempts = mutableMapOf<Int, Boolean>()
private var updatePositionJob: Job? = null

@OptIn(UnstableApi::class)
private fun prepare(uri: Uri, contentType: Int? = null) {
val dataSourceFactory =
DefaultDataSource.Factory(context, DefaultHttpDataSource.Factory().apply {
setUserAgent(SP.videoPlayerUserAgent)
setConnectTimeoutMs(Constants.VIDEO_PLAYER_LOAD_TIMEOUT.toInt())
setReadTimeoutMs(Constants.VIDEO_PLAYER_LOAD_TIMEOUT.toInt())
setConnectTimeoutMs(SP.videoPlayerLoadTimeout.toInt())
setReadTimeoutMs(SP.videoPlayerLoadTimeout.toInt())
setKeepPostFor302Redirects(true)
setAllowCrossProtocolRedirects(true)
})
Expand Down Expand Up @@ -76,6 +81,8 @@ class LeanbackMedia3VideoPlayer(
videoPlayer.prepare()
triggerPrepared()
}
updatePositionJob?.cancel()
updatePositionJob = null
}

private val playerListener = object : Player.Listener {
Expand Down Expand Up @@ -103,10 +110,7 @@ class LeanbackMedia3VideoPlayer(
}
} else {
triggerError(
PlaybackException(
ex.errorCodeName.replace("ERROR_CODE_", ""),
ex.errorCode,
)
PlaybackException(ex.errorCodeName, ex.errorCode)
)
}
}
Expand All @@ -117,6 +121,15 @@ class LeanbackMedia3VideoPlayer(
triggerBuffering(true)
} else if (playbackState == Player.STATE_READY) {
triggerReady()

updatePositionJob?.cancel()
updatePositionJob = coroutineScope.launch {
triggerCurrentPosition(-1)
while (true) {
triggerCurrentPosition(videoPlayer.currentPosition)
delay(1000)
}
}
}

if (playbackState != Player.STATE_BUFFERING) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
package top.yogiczy.mytv.ui.screens.leanback.video.player

import android.view.SurfaceView
import androidx.media3.common.PlaybackException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import top.yogiczy.mytv.ui.utils.SP
import top.yogiczy.mytv.utils.Loggable

abstract class LeanbackVideoPlayer(
private val coroutineScope: CoroutineScope,
) : Loggable() {
private var loadTimeoutJob: Job? = null
private var cutoffTimeoutJob: Job? = null
private var currentPosition = -1L

abstract class LeanbackVideoPlayer {
protected var metadata = Metadata()

open fun initialize() {
Expand All @@ -26,8 +39,8 @@ abstract class LeanbackVideoPlayer {
private val onReadyListeners = mutableListOf<() -> Unit>()
private val onBufferingListeners = mutableListOf<(buffering: Boolean) -> Unit>()
private val onPreparedListeners = mutableListOf<() -> Unit>()
private val onMetadataListeners =
mutableListOf<(metadata: Metadata) -> Unit>()
private val onMetadataListeners = mutableListOf<(metadata: Metadata) -> Unit>()
private val onCutoffListeners = mutableListOf<() -> Unit>()

private fun clearAllListeners() {
onResolutionListeners.clear()
Expand All @@ -36,6 +49,7 @@ abstract class LeanbackVideoPlayer {
onBufferingListeners.clear()
onPreparedListeners.clear()
onMetadataListeners.clear()
onCutoffListeners.clear()
}

protected fun triggerResolution(width: Int, height: Int) {
Expand All @@ -44,10 +58,15 @@ abstract class LeanbackVideoPlayer {

protected fun triggerError(error: PlaybackException?) {
onErrorListeners.forEach { it(error) }
if(error != PlaybackException.LOAD_TIMEOUT) {
loadTimeoutJob?.cancel()
loadTimeoutJob = null
}
}

protected fun triggerReady() {
onReadyListeners.forEach { it() }
loadTimeoutJob?.cancel()
}

protected fun triggerBuffering(buffering: Boolean) {
Expand All @@ -56,12 +75,30 @@ abstract class LeanbackVideoPlayer {

protected fun triggerPrepared() {
onPreparedListeners.forEach { it() }
loadTimeoutJob?.cancel()
loadTimeoutJob = coroutineScope.launch {
delay(SP.videoPlayerLoadTimeout)
triggerError(PlaybackException.LOAD_TIMEOUT)
}
cutoffTimeoutJob?.cancel()
cutoffTimeoutJob = null
}

protected fun triggerMetadata(metadata: Metadata) {
onMetadataListeners.forEach { it(metadata) }
}

protected fun triggerCurrentPosition(newPosition: Long) {
if (currentPosition != newPosition) {
cutoffTimeoutJob?.cancel()
cutoffTimeoutJob = coroutineScope.launch {
delay(SP.videoPlayerLoadTimeout)
onCutoffListeners.forEach { it() }
}
}
currentPosition = newPosition
}

fun onResolution(listener: (width: Int, height: Int) -> Unit) {
onResolutionListeners.add(listener)
}
Expand All @@ -86,13 +123,19 @@ abstract class LeanbackVideoPlayer {
onMetadataListeners.add(listener)
}

fun onCutoff(listener: () -> Unit) {
onCutoffListeners.add(listener)
}

data class PlaybackException(
val errorCodeName: String,
val errorCode: Int,
) : Exception(errorCodeName) {
companion object {
val UNSUPPORTED_TYPE =
PlaybackException("UNSUPPORTED_TYPE", 10002)
val LOAD_TIMEOUT =
PlaybackException("LOAD_TIMEOUT", 10003)
}
}

Expand Down
8 changes: 8 additions & 0 deletions app/src/main/java/top/yogiczy/mytv/ui/utils/SP.kt
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ object SP {
/** ==================== 播放器 ==================== */
/** 播放器 自定义ua */
VIDEO_PLAYER_USER_AGENT,

/** 播放器 加载超时 */
VIDEO_PLAYER_LOAD_TIMEOUT,
}

/** ==================== 应用 ==================== */
Expand Down Expand Up @@ -255,6 +258,11 @@ object SP {
}
set(value) = sp.edit().putString(KEY.VIDEO_PLAYER_USER_AGENT.name, value).apply()

/** 播放器 加载超时 */
var videoPlayerLoadTimeout: Long
get() = sp.getLong(KEY.VIDEO_PLAYER_LOAD_TIMEOUT.name, Constants.VIDEO_PLAYER_LOAD_TIMEOUT)
set(value) = sp.edit().putLong(KEY.VIDEO_PLAYER_LOAD_TIMEOUT.name, value).apply()

enum class UiTimeShowMode(val value: Int) {
/** 隐藏 */
HIDDEN(0),
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/raw/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
</van-cell>
</van-cell-group>

<van-cell-group inset title="网络">
<van-cell-group inset title="播放器">
<van-cell title="自定义UA">
<template #label>
<van-space class="w-full" direction="vertical" size="small">
Expand Down

0 comments on commit 7acbc74

Please sign in to comment.