diff --git a/example/libs/sellycloudsdk-1.0.0.aar b/example/libs/sellycloudsdk-1.0.0.aar index 2cf5902..e59039d 100644 Binary files a/example/libs/sellycloudsdk-1.0.0.aar and b/example/libs/sellycloudsdk-1.0.0.aar differ diff --git a/example/src/main/java/com/demo/SellyCloudSDK/interactive/InteractiveLiveActivity.kt b/example/src/main/java/com/demo/SellyCloudSDK/interactive/InteractiveLiveActivity.kt index e1dfd11..10d8993 100644 --- a/example/src/main/java/com/demo/SellyCloudSDK/interactive/InteractiveLiveActivity.kt +++ b/example/src/main/java/com/demo/SellyCloudSDK/interactive/InteractiveLiveActivity.kt @@ -151,16 +151,27 @@ class InteractiveLiveActivity : AppCompatActivity() { override fun onDestroy() { super.onDestroy() rtcEngine?.setCaptureVideoFrameInterceptor(null) - leaveChannel() - InteractiveRtcEngine.destroy(rtcEngine) - rtcEngine = null - localRenderer?.let { releaseRenderer(it) } - remoteRendererMap.values.forEach { releaseRenderer(it) } - remoteRendererMap.clear() fuFrameInterceptor = null - try { beautyRenderer?.release() } catch (_: Exception) {} - beautyRenderer = null remoteMediaState.clear() + + // 捕获需要释放的引用,避免主线程阻塞导致 ANR + val engine = rtcEngine + val local = localRenderer + val remotes = remoteRendererMap.values.toList() + val beauty = beautyRenderer + rtcEngine = null + localRenderer = null + remoteRendererMap.clear() + beautyRenderer = null + + // 重量级资源释放移到后台线程 + Thread { + try { engine?.leaveChannel() } catch (_: Exception) {} + try { InteractiveRtcEngine.destroy(engine) } catch (_: Exception) {} + try { local?.release() } catch (_: Exception) {} + remotes.forEach { try { it.release() } catch (_: Exception) {} } + try { beauty?.release() } catch (_: Exception) {} + }.start() } override fun onSupportNavigateUp(): Boolean { @@ -685,19 +696,29 @@ class InteractiveLiveActivity : AppCompatActivity() { slot.layout.detachRenderer() updateSlotOverlay(slot) } - rtcEngine?.clearRemoteVideo(userId) - remoteRendererMap.remove(userId)?.let { releaseRenderer(it) } + val engine = rtcEngine + val renderer = remoteRendererMap.remove(userId) remoteStats.remove(userId) + // SurfaceViewRenderer.release() 会死锁主线程,移到后台 + Thread { + try { engine?.clearRemoteVideo(userId) } catch (_: Exception) {} + try { renderer?.release() } catch (_: Exception) {} + }.start() } private fun resetVideoSlots(releaseRemotes: Boolean = true) { if (releaseRemotes) { + val engine = rtcEngine val remoteIds = remoteRendererMap.keys.toList() - remoteIds.forEach { userId -> - rtcEngine?.clearRemoteVideo(userId) - remoteRendererMap.remove(userId)?.let { releaseRenderer(it) } - } + val renderersToRelease = remoteIds.mapNotNull { remoteRendererMap.remove(it) } remoteStats.clear() + // SurfaceViewRenderer.release() 会死锁主线程,移到后台 + Thread { + remoteIds.forEach { userId -> + try { engine?.clearRemoteVideo(userId) } catch (_: Exception) {} + } + renderersToRelease.forEach { try { it.release() } catch (_: Exception) {} } + }.start() } remoteSlots.forEach { slot -> slot.userId = null @@ -722,7 +743,9 @@ class InteractiveLiveActivity : AppCompatActivity() { private fun displayId(userId: String): String = userId private fun leaveChannel() { - rtcEngine?.leaveChannel() + // SDK 的 leaveChannel() 会同步停止 Whip/Whep 客户端,阻塞主线程 + val engine = rtcEngine + Thread { try { engine?.leaveChannel() } catch (_: Exception) {} }.start() resetUiAfterLeave() } diff --git a/example/src/main/java/com/demo/SellyCloudSDK/live/LivePlayActivity.kt b/example/src/main/java/com/demo/SellyCloudSDK/live/LivePlayActivity.kt index 79e2386..277137f 100644 --- a/example/src/main/java/com/demo/SellyCloudSDK/live/LivePlayActivity.kt +++ b/example/src/main/java/com/demo/SellyCloudSDK/live/LivePlayActivity.kt @@ -81,6 +81,7 @@ class LivePlayActivity : AppCompatActivity() { private var firstAudioFrameCostMs: Long? = null private var isLatencyChasingActive: Boolean = false private var lastLatencyChasingSpeed: Float? = null + private var lastLatencyChasingUpdate: SellyLatencyChasingUpdate? = null private var hasReleasedPlayer: Boolean = false private val logLines: ArrayDeque = ArrayDeque() @@ -157,29 +158,35 @@ class LivePlayActivity : AppCompatActivity() { override fun onLatencyChasingUpdate(update: SellyLatencyChasingUpdate) { runOnUiThread { val speedRounded = kotlin.math.round(update.speed * 10f) / 10f + val speedText = formatLatencyChasingSpeed(speedRounded) + val chasingDetail = buildLatencyChasingDetail(update) + lastLatencyChasingUpdate = update val isChasing = speedRounded > 1.0f if (isChasing && !isLatencyChasingActive) { isLatencyChasingActive = true - val speedText = String.format(Locale.US, "%.1f", speedRounded) - logEvent("追帧开始: 速度=${speedText}x") + logEvent("追帧开始: 速度=${speedText}x, $chasingDetail") lastLatencyChasingSpeed = speedRounded } else if (isChasing && isLatencyChasingActive) { if (lastLatencyChasingSpeed == null || lastLatencyChasingSpeed != speedRounded) { - val speedText = String.format(Locale.US, "%.1f", speedRounded) - logEvent("追帧速率变化: 速度=${speedText}x") + logEvent("追帧速率变化: 速度=${speedText}x, $chasingDetail") lastLatencyChasingSpeed = speedRounded } } else if (!isChasing && isLatencyChasingActive) { isLatencyChasingActive = false - logEvent("追帧结束: 速度=1.0x") + logEvent("追帧结束: 速度=1.0x, $chasingDetail") lastLatencyChasingSpeed = null + } else { + logEvent("追帧状态更新: 速度=${speedText}x, $chasingDetail") } } } override fun onLatencyChasingReloadRequired(latencyMs: Long) { runOnUiThread { - logEvent("追帧触发重载: 延迟=${latencyMs}ms") + val lastUpdateDetail = lastLatencyChasingUpdate + ?.let { ", 最近追帧: ${buildLatencyChasingDetail(it)}" } + .orEmpty() + logEvent("追帧触发重载: 延迟=${latencyMs}ms$lastUpdateDetail") } } @@ -709,9 +716,27 @@ class LivePlayActivity : AppCompatActivity() { firstAudioFrameCostMs = null isLatencyChasingActive = false lastLatencyChasingSpeed = null + lastLatencyChasingUpdate = null logEvent("播放尝试开始") } + private fun formatLatencyChasingSpeed(speed: Float): String { + return String.format(Locale.US, "%.1f", speed) + } + + private fun buildLatencyChasingDetail(update: SellyLatencyChasingUpdate): String { + val cacheDeltaMs = update.audioCachedMs - update.videoCachedMs + val deltaText = if (cacheDeltaMs > 0L) "+$cacheDeltaMs" else cacheDeltaMs.toString() + val cacheSkewTag = when { + cacheDeltaMs >= CACHE_SKEW_WARNING_MS -> "音频缓存领先" + cacheDeltaMs <= -CACHE_SKEW_WARNING_MS -> "视频缓存领先" + else -> "音视频缓存接近" + } + return "延迟=${update.latencyMs}ms, 档位=${update.tier}, 缓冲中=${if (update.buffering) "是" else "否"}, " + + "首帧已出=${if (update.firstFrameRendered) "是" else "否"}, 音频缓存=${update.audioCachedMs}ms, " + + "视频缓存=${update.videoCachedMs}ms, 缓存差(音-视)=${deltaText}ms($cacheSkewTag)" + } + private fun formatState(state: SellyPlayerState): String { return "${stateLabel(state)}(${state.name})" } @@ -744,6 +769,7 @@ class LivePlayActivity : AppCompatActivity() { companion object { private const val TAG = "LivePlayActivity" private const val MAX_LOG_LINES = 200 + private const val CACHE_SKEW_WARNING_MS = 300L @Volatile private var pipActivityRef: WeakReference? = null const val EXTRA_PLAY_PROTOCOL = "play_protocol"