chore: 清理订阅请求死代码 + RTSP 自动重连指数退避
- ControlCommands.kt: 删除 subscribeStatus() 及 SUB_* 常量 - ProtocolClient.kt: 删除 sendSubscriptionRequests() 方法及调用 - RtspVideoPlayer.kt: 播放失败后自动重试,指数退避(1s~30s,倍率2),覆盖层显示重试次数和倒计时 Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
@@ -130,9 +130,6 @@ class ProtocolClient(
|
||||
}
|
||||
}
|
||||
|
||||
// 发送订阅请求(触发服务端 UDP 推送)
|
||||
sendSubscriptionRequests()
|
||||
|
||||
// 启动断线检测(立即开始,覆盖 CONNECTING 和 CONNECTED 状态)
|
||||
timeoutJob = scope.launch {
|
||||
timeoutCheckLoop()
|
||||
@@ -195,25 +192,6 @@ class ProtocolClient(
|
||||
socket = null
|
||||
}
|
||||
|
||||
/** 发送订阅请求 */
|
||||
private fun sendSubscriptionRequests() {
|
||||
val subscriptions: List<Pair<Int, Int>> = listOf(
|
||||
ControlCommands.SUB_BASIC,
|
||||
ControlCommands.SUB_MOTION_CONTROL,
|
||||
ControlCommands.SUB_DEVICE,
|
||||
ControlCommands.SUB_ERROR
|
||||
)
|
||||
val sock = socket ?: return
|
||||
val addr = remoteAddress ?: return
|
||||
for ((type, cmd) in subscriptions) {
|
||||
try {
|
||||
val json = ControlCommands.subscribeStatus(type, cmd)
|
||||
val packet = encoder.encode(json)
|
||||
sock.send(DatagramPacket(packet, packet.size, addr, port))
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
|
||||
/** 接收循环 */
|
||||
private fun CoroutineScope.receiveLoop(sock: DatagramSocket) {
|
||||
val buffer = ByteArray(65535)
|
||||
|
||||
@@ -122,10 +122,6 @@ object ControlCommands {
|
||||
// ---- 2.10 休眠状态查询 ----
|
||||
fun querySleepStatus(time: String = now()): String = buildAsdu(type = 1101, command = 7, time = time)
|
||||
|
||||
// ---- 订阅状态上报 ----
|
||||
fun subscribeStatus(type: Int, command: Int, time: String = now()): String =
|
||||
buildAsdu(type = type, command = command, time = time)
|
||||
|
||||
// ========== 常量 ==========
|
||||
|
||||
// 运动状态常量
|
||||
@@ -147,9 +143,4 @@ object ControlCommands {
|
||||
const val MODE_NAVIGATION = 1
|
||||
const val MODE_ASSIST = 2
|
||||
|
||||
// 订阅 Type/Command — 用于 subscribeStatus()
|
||||
val SUB_ERROR = Pair(1002, 3)
|
||||
val SUB_MOTION_CONTROL = Pair(1002, 4)
|
||||
val SUB_DEVICE = Pair(1002, 5)
|
||||
val SUB_BASIC = Pair(1002, 6)
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
package com.example.m20_gamepad.video
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -14,6 +17,9 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.PlaybackException
|
||||
@@ -26,6 +32,7 @@ import androidx.media3.exoplayer.mediacodec.MediaCodecSelector
|
||||
import androidx.media3.ui.AspectRatioFrameLayout
|
||||
import androidx.media3.ui.PlayerView
|
||||
import com.example.m20_gamepad.data.VideoResizeMode
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/**
|
||||
* 视频解码器选择策略。
|
||||
@@ -44,13 +51,18 @@ enum class VideoCodec {
|
||||
/** RTSP 播放状态 */
|
||||
sealed class RtspState {
|
||||
/** 连接中 */
|
||||
object Connecting : RtspState()
|
||||
data class Connecting(val retryCount: Int = 0) : RtspState()
|
||||
/** 播放中 */
|
||||
object Playing : RtspState()
|
||||
/** 连接失败 */
|
||||
data class Error(val message: String) : RtspState()
|
||||
/** 连接失败(自动重试中) */
|
||||
data class Error(val message: String, val retryCount: Int = 0, val nextRetryMs: Long = 0) : RtspState()
|
||||
}
|
||||
|
||||
/** 指数退避参数 */
|
||||
private const val RETRY_DELAY_INITIAL_MS = 1000L
|
||||
private const val RETRY_DELAY_MAX_MS = 30_000L
|
||||
private const val RETRY_DELAY_MULTIPLIER = 2
|
||||
|
||||
/**
|
||||
* RTSP 视频流播放 Composable。
|
||||
*
|
||||
@@ -72,7 +84,10 @@ fun RtspVideoPlayer(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
var state by remember { mutableStateOf<RtspState>(RtspState.Connecting) }
|
||||
var state by remember { mutableStateOf<RtspState>(RtspState.Connecting()) }
|
||||
var retryCount by remember { mutableIntStateOf(0) }
|
||||
var retryDelayMs by remember { mutableStateOf(RETRY_DELAY_INITIAL_MS) }
|
||||
var retrySignal by remember { mutableIntStateOf(0) }
|
||||
|
||||
// codec 变化时重建 ExoPlayer,新的 selector 在 RenderersFactory 中注入,
|
||||
// 同时使用最小缓冲策略降低延迟
|
||||
@@ -99,24 +114,42 @@ fun RtspVideoPlayer(
|
||||
addListener(object : Player.Listener {
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
state = when (playbackState) {
|
||||
Player.STATE_BUFFERING -> RtspState.Connecting
|
||||
Player.STATE_BUFFERING -> RtspState.Connecting(retryCount)
|
||||
Player.STATE_READY -> RtspState.Playing
|
||||
Player.STATE_IDLE -> RtspState.Connecting
|
||||
Player.STATE_IDLE -> RtspState.Connecting(retryCount)
|
||||
else -> state
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPlayerError(error: PlaybackException) {
|
||||
state = RtspState.Error(error.message ?: "连接失败")
|
||||
state = RtspState.Error(
|
||||
message = error.message ?: "连接失败",
|
||||
retryCount = retryCount,
|
||||
nextRetryMs = retryDelayMs
|
||||
)
|
||||
// 递增 retrySignal 触发 LaunchedEffect 自动重试
|
||||
retrySignal++
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// URL 或 codec 变化时重新加载媒体源
|
||||
LaunchedEffect(url, codec) {
|
||||
// 初始连接 + 自动重试:key 包含 url/codec 变化时自动取消旧重试
|
||||
LaunchedEffect(url, codec, retrySignal) {
|
||||
if (url.isNotBlank()) {
|
||||
state = RtspState.Connecting
|
||||
if (retrySignal == 0) {
|
||||
// 首次连接 / URL 或 codec 变更:重置退避
|
||||
retryCount = 0
|
||||
retryDelayMs = RETRY_DELAY_INITIAL_MS
|
||||
} else {
|
||||
// 重试:指数退避等待
|
||||
delay(retryDelayMs)
|
||||
retryDelayMs = (retryDelayMs * RETRY_DELAY_MULTIPLIER).coerceAtMost(RETRY_DELAY_MAX_MS)
|
||||
retryCount++
|
||||
}
|
||||
state = RtspState.Connecting(retryCount)
|
||||
player.stop()
|
||||
player.clearMediaItems()
|
||||
val mediaItem = MediaItem.fromUri(url)
|
||||
player.setMediaItem(mediaItem)
|
||||
player.prepare()
|
||||
@@ -152,23 +185,50 @@ fun RtspVideoPlayer(
|
||||
|
||||
// 连接中 / 错误时的提示覆盖层
|
||||
when (val s = state) {
|
||||
is RtspState.Connecting -> StatusOverlay("连接 RTSP 流中...")
|
||||
is RtspState.Error -> StatusOverlay("RTSP 连接失败: ${s.message}")
|
||||
is RtspState.Connecting -> {
|
||||
val msg = if (s.retryCount > 0) {
|
||||
"RTSP 重连中... (第 ${s.retryCount} 次)"
|
||||
} else {
|
||||
"连接 RTSP 流中..."
|
||||
}
|
||||
StatusOverlay(msg)
|
||||
}
|
||||
is RtspState.Error -> {
|
||||
val retryHint = if (s.nextRetryMs > 0) {
|
||||
"${s.nextRetryMs / 1000}s 后自动重试"
|
||||
} else ""
|
||||
StatusOverlay(
|
||||
text = "RTSP 连接失败: ${s.message}",
|
||||
subtitle = retryHint
|
||||
)
|
||||
}
|
||||
is RtspState.Playing -> { /* 正常播放,不显示覆盖层 */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatusOverlay(text: String) {
|
||||
private fun StatusOverlay(text: String, subtitle: String = "") {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
color = Color.White
|
||||
)
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
text = text,
|
||||
color = Color.White,
|
||||
fontSize = 16.sp
|
||||
)
|
||||
if (subtitle.isNotBlank()) {
|
||||
Text(
|
||||
text = subtitle,
|
||||
color = Color.White.copy(alpha = 0.7f),
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user