diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart index 120fe0a5..a3799b38 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart @@ -28,13 +28,12 @@ import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart'; import 'package:star_lock/talk/starChart/star_chart_manage.dart'; import 'package:star_lock/talk/starChart/views/native/talk_view_native_decode_state.dart'; import 'package:star_lock/tools/G711Tool.dart'; +import 'package:star_lock/tools/baseGetXController.dart'; import 'package:star_lock/tools/callkit_handler.dart'; import 'package:star_lock/tools/commonDataManage.dart'; import 'package:star_lock/tools/storage.dart'; import 'package:video_decode_plugin/video_decode_plugin.dart'; -import '../../../../tools/baseGetXController.dart'; - class TalkViewNativeDecodeLogic extends BaseGetXController { final TalkViewNativeDecodeState state = TalkViewNativeDecodeState(); @@ -44,35 +43,48 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { int audioBufferSize = 30; // 音频默认缓冲更多帧,从20增加到30 - // 网络质量评分,用于动态调整缓冲区大小 + // 新增-------m 网络质量评分,用于动态调整缓冲区大小 double networkQualityScore = 1.0; // 初始网络质量评分为1.0(满分) - - // 上次调整缓冲区的时间 + + // 新增-------m 上次调整缓冲区的时间 int _lastBufferSizeAdjustmentTime = 0; - // 帧率统计相关变量 + // 新增-------m 帧率统计相关变量 int _frameCount = 0; int _lastFpsCalculationTime = 0; Timer? _fpsUpdateTimer; - // 连续满载计数器 + // 新增-------m 连续满载计数器 int _consecutiveFullBufferCount = 0; static const int _maxConsecutiveFullBuffer = 5; // 最大连续满载次数阈值 - // 解码性能监控 + // 新增-------m 解码性能监控 int _lastSlowDecodeTime = 0; int _slowDecodeCount = 0; static const int _slowDecodeThreshold = 50; // 慢解码阈值(毫秒) static const int _slowDecodeAdjustmentThreshold = 3; // 慢解码次数阈值 - // 记录首帧时间戳,用于计算首帧渲染延迟 + // 新增-------m 缓冲区使用率监控 + static const double _bufferUsageThreshold = 0.8; // 缓冲区使用率达到80%时启动预清理 + int _lastBufferClearTime = 0; + static const int _minClearInterval = 500; // 最小清理间隔500ms,防止过度清理 + + // 新增-------m 记录首帧时间戳,用于计算首帧渲染延迟 int? _firstFrameReceivedTime; int? _firstFrameRenderedTime; - // 防止缓冲区大小频繁调整的标志 + // 新增-------m 防止缓冲区大小频繁调整的标志 bool _isAdjustingBuffer = false; static const int _minAdjustmentInterval = 3000; // 最小调整间隔3秒 + // 新增-------m 卡顿检测相关变量 + int _lastFrameProcessTime = 0; // 上次处理帧的时间 + int _lastFrameCount = 0; // 上次统计的帧数 + int _stutterDetectionInterval = 1000; // 卡顿检测间隔(毫秒) + double _expectedFps = 25.0; // 期望的FPS + int _frameDropThreshold = 5; // 帧丢弃阈值 + Timer? _stutterDetectionTimer; // 卡顿检测定时器 + // 回绕阈值,动态调整,frameSeq较小时阈值也小 int _getFrameSeqRolloverThreshold(int lastSeq) { if (lastSeq > 2000) { @@ -121,6 +133,15 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { int? lastDecodedIFrameSeq; + // 新增-------m:渲染状态管理,用于优化渲染前后缓冲区处理 + final bool _isRenderingStarted = false; + final int _lastRenderStartTime = 0; + static const int _renderStartBufferThreshold = 5; // 渲染开始前的缓冲区阈值 + + // 新增-------m:渲染启动后快速消耗缓冲区的标志 + final bool _isConsumingForRender = false; + static const int _consumeForRenderDuration = 2000; // 渲染启动后2秒内快速消耗缓冲区 + // 初始化视频解码器 Future _initVideoDecoder() async { try { @@ -147,22 +168,22 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { if (_firstFrameReceivedTime != null) { final renderTime = _firstFrameRenderedTime! - _firstFrameReceivedTime!; AppLog.log('首帧渲染耗时: ${renderTime}ms (${renderTime/1000.0}s)'); + // 启动网络质量监测定时器 + _startNetworkQualityMonitor(); + // 启动卡顿检测定时器 + _startStutterDetection(); + // 开始帧率统计 + _startFpsCalculation(); } // 只有真正渲染出首帧时才关闭loading Future.microtask(() => state.isLoading.value = false); - - // 开始帧率统计 - _startFpsCalculation(); }); } else { AppLog.log('视频解码器初始化失败'); } // 启动定时器发送帧数据 _startFrameProcessTimer(); - - // 启动网络质量监测定时器 - _startNetworkQualityMonitor(); } catch (e) { AppLog.log('初始化视频解码器错误: $e'); // 如果初始化失败,延迟后重试 @@ -174,7 +195,7 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { } } - /// 开始帧率统计 + // 新增-------m 开始帧率统计 void _startFpsCalculation() { _fpsUpdateTimer?.cancel(); _fpsUpdateTimer = Timer.periodic(const Duration(seconds: 1), (timer) { @@ -182,7 +203,7 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { }); } - /// 更新帧率 + // 新增-------m 更新帧率 void _updateFps() { final currentTime = DateTime.now().millisecondsSinceEpoch; final timeDiff = currentTime - _lastFpsCalculationTime; @@ -199,7 +220,7 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { } } - /// 网络质量监测 + // 新增-------m 网络质量监测 void _startNetworkQualityMonitor() { Timer.periodic(Duration(milliseconds: state.networkQualityCheckIntervalMs), (timer) { _calculateNetworkQuality(); @@ -207,7 +228,7 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { }); } - /// 计算网络质量评分 + // 新增-------m 计算网络质量评分 void _calculateNetworkQuality() { // 基于丢包率、延迟等因素计算网络质量评分 // 这里简化处理,实际应用中可以基于更多因素计算 @@ -220,12 +241,43 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // 综合评分,可根据实际情况调整权重 networkQualityScore = (lossRateImpact * 0.6 + fpsImpact * 0.4).clamp(0.0, 1.0); - AppLog.log('网络质量评分: ${networkQualityScore.toStringAsFixed(2)}, 丢包率: ${state.packetLossRate.value.toStringAsFixed(2)}, FPS: ${state.decoderFps.value.toStringAsFixed(2)}'); + // 检查是否出现卡顿并打印相关信息 + _checkAndReportStutter(); + + // FPS是每秒显示的帧数,数字越高,画面就越顺滑 + // AppLog.log('网络质量评分: ${networkQualityScore.toStringAsFixed(2)}, 分包丢失率: ${state.packetLossRate.value.toStringAsFixed(2)}, FPS: ${state.decoderFps.value.toStringAsFixed(2)}'); } - /// 根据网络质量动态调整缓冲区大小 - void _adjustBufferSizeBasedOnNetworkQuality() async { - final currentTime = DateTime.now().millisecondsSinceEpoch; + // 新增-------m 检查并报告卡顿情况 + void _checkAndReportStutter() { + // 检查FPS是否显著下降 + if (state.decoderFps.value < state.targetFps * 0.5) { // 实际FPS低于目标FPS的50% + AppLog.log('视频卡顿检测: FPS过低! 目标FPS: ${state.targetFps}, 实际FPS: ${state.decoderFps.value.toStringAsFixed(2)}, 丢包率: ${state.packetLossRate.value.toStringAsFixed(2)}, 网络质量: ${networkQualityScore.toStringAsFixed(2)}'); + } + + // 检查缓冲区占用情况 + double bufferUsage = state.h264FrameBuffer.length / state.maxFrameBufferSize; + if (bufferUsage > 0.9) { // 缓冲区占用超过90% + AppLog.log('视频卡顿检测: 缓冲区严重过载! 占用率: ${(bufferUsage * 100).toStringAsFixed(1)}% (${state.h264FrameBuffer.length}/${state.maxFrameBufferSize})'); + } + + // 检查丢包率 + if (state.packetLossRate.value > 0.1) { // 丢包率超过10% + AppLog.log('视频卡顿检测: 丢包率过高! 丢包率: ${(state.packetLossRate.value * 100).toStringAsFixed(1)}%, 目标FPS: ${state.targetFps}, 实际FPS: ${state.decoderFps.value.toStringAsFixed(2)}'); + } + + // 综合判断卡顿情况 + if (networkQualityScore < 0.4 || + state.decoderFps.value < state.targetFps * 0.4 || + bufferUsage > 0.85 || + state.packetLossRate.value > 0.15) { + AppLog.log('严重卡顿警告: 网络质量=${networkQualityScore.toStringAsFixed(2)}, FPS=${state.decoderFps.value.toStringAsFixed(2)}/${state.targetFps}, 丢包率=${(state.packetLossRate.value * 100).toStringAsFixed(1)}%, 缓冲区=${(bufferUsage * 100).toStringAsFixed(1)}%'); + } + } + + // 新增-------m 根据网络质量动态调整缓冲区大小 + Future _adjustBufferSizeBasedOnNetworkQuality() async { + final int currentTime = DateTime.now().millisecondsSinceEpoch; // 避免过于频繁的调整 if (currentTime - _lastBufferSizeAdjustmentTime < _minAdjustmentInterval || _isAdjustingBuffer) { @@ -259,6 +311,11 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { AppLog.log('缓冲区大小调整为: $newBufferSize (网络质量评分: ${networkQualityScore.toStringAsFixed(2)})'); } + // 检查网络质量,如果过低则打印卡顿相关信息 + if (networkQualityScore < 0.3) { + AppLog.log('网络质量严重不佳,可能导致视频卡顿: 网络质量评分 ${networkQualityScore.toStringAsFixed(2)}, 目标FPS: ${state.targetFps}, 实际FPS: ${state.decoderFps.value.toStringAsFixed(2)}, 丢包率: ${state.packetLossRate.value.toStringAsFixed(2)}'); + } + _lastBufferSizeAdjustmentTime = currentTime; await Future.delayed(const Duration(milliseconds: 100)); // 短暂延迟确保调整生效 _isAdjustingBuffer = false; @@ -302,6 +359,8 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { int frameSeqI, ScpMessage scpMessage, ) { + // 调用渲染优化方法 + _optimizeBufferForRender(); // 动态回绕阈值判断,frameSeq较小时阈值也小 if (!_pendingStreamReset && _lastFrameSeq != null && frameType == TalkDataH264Frame_FrameTypeE.I && frameSeq < _lastFrameSeq!) { int dynamicThreshold = _getFrameSeqRolloverThreshold(_lastFrameSeq!); @@ -373,6 +432,16 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { _consecutiveFullBufferCount = 0; } + // 新增-------m 预清理机制:当缓冲区使用率达到阈值时提前清理 + final double bufferUsage = state.h264FrameBuffer.length / state.maxFrameBufferSize; + if (bufferUsage >= _bufferUsageThreshold) { + final int currentTime = DateTime.now().millisecondsSinceEpoch; + if (currentTime - _lastBufferClearTime > _minClearInterval) { + _performPreemptiveCleanup(); + _lastBufferClearTime = currentTime; + } + } + // 如果缓冲区超出最大大小,优先丢弃P帧 while (state.h264FrameBuffer.length >= state.maxFrameBufferSize) { int pIndex = state.h264FrameBuffer.indexWhere((f) => f['frameType'] == TalkDataH264Frame_FrameTypeE.P); @@ -388,41 +457,159 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { state.h264FrameBuffer.add(frameMap); } - /// 处理连续缓冲区满载情况 + // 新增-------m 处理连续缓冲区满载情况 void _handleConsecutiveFullBuffer() { AppLog.log('缓冲区连续满载 $_consecutiveFullBufferCount 次,执行紧急清理'); - // 优先保留I帧及关联的P帧,清理多余的P帧 - final iFrames = state.h264FrameBuffer.where((f) => f['frameType'] == TalkDataH264Frame_FrameTypeE.I).toList(); - - if (iFrames.length > 1) { - // 如果有多个I帧,保留最新的,删除较旧的 - iFrames.sort((a, b) => (a['frameSeq'] as int).compareTo(b['frameSeq'] as int)); - // 保留最新的I帧,删除其他的 - for (int i = 0; i < iFrames.length - 1; i++) { - state.h264FrameBuffer.remove(iFrames[i]); + // 特殊处理:如果是在渲染开始阶段,采用更积极的清理策略 + if (_isRenderingStarted && _isConsumingForRender) { + // 在渲染启动后快速消耗阶段,只保留最近的I帧和其关联的P帧 + final iFrames = state.h264FrameBuffer.where((f) => f['frameType'] == TalkDataH264Frame_FrameTypeE.I).toList(); + if (iFrames.isNotEmpty) { + // 找到最新的I帧 + final latestIFrame = iFrames.reduce((a, b) => (a['frameSeq'] as int) > (b['frameSeq'] as int) ? a : b); + final int latestIFrameSeq = latestIFrame['frameSeq']; + + // 找到与最新I帧关联的P帧 + final validPFrames = state.h264FrameBuffer + .where((f) => f['frameType'] == TalkDataH264Frame_FrameTypeE.P && + f['frameSeqI'] == latestIFrameSeq) + .toList(); + + // 保留最新的I帧和其关联的P帧,移除其他所有帧 + state.h264FrameBuffer.clear(); + state.h264FrameBuffer.add(latestIFrame); + state.h264FrameBuffer.addAll(validPFrames); } - } - - // 保持缓冲区大小在合理范围内,保留一半的缓冲区内容 - int targetSize = state.maxFrameBufferSize ~/ 2; - while (state.h264FrameBuffer.length > targetSize) { - // 优先移除P帧,最后才是I帧(如果必须的话) - int pIndex = state.h264FrameBuffer.indexWhere((f) => f['frameType'] == TalkDataH264Frame_FrameTypeE.P); - if (pIndex != -1) { - state.h264FrameBuffer.removeAt(pIndex); - continue; + } else { + // 优先保留I帧及关联的P帧,清理多余的P帧 + final iFrames = state.h264FrameBuffer.where((f) => f['frameType'] == TalkDataH264Frame_FrameTypeE.I).toList(); + + if (iFrames.length > 1) { + // 如果有多个I帧,保留最新的,删除较旧的 + iFrames.sort((a, b) => (a['frameSeq'] as int).compareTo(b['frameSeq'] as int)); + // 保留最新的I帧,删除其他的 + for (int i = 0; i < iFrames.length - 1; i++) { + state.h264FrameBuffer.remove(iFrames[i]); + } } - // 如果仍有需要清理的空间且没有P帧,移除最旧的帧 - if (state.h264FrameBuffer.length > targetSize) { - state.h264FrameBuffer.removeAt(0); + // 保持缓冲区大小在合理范围内,保留一半的缓冲区内容 + int targetSize = state.maxFrameBufferSize ~/ 2; + while (state.h264FrameBuffer.length > targetSize) { + // 优先移除P帧,最后才是I帧(如果必须的话) + int pIndex = state.h264FrameBuffer.indexWhere((f) => f['frameType'] == TalkDataH264Frame_FrameTypeE.P); + if (pIndex != -1) { + state.h264FrameBuffer.removeAt(pIndex); + continue; + } + + // 如果仍有需要清理的空间且没有P帧,移除最旧的帧 + if (state.h264FrameBuffer.length > targetSize) { + state.h264FrameBuffer.removeAt(0); + } } } - + AppLog.log('紧急清理完成,当前缓冲区大小: ${state.h264FrameBuffer.length}'); } + // 新增-------m 定期优化缓冲区管理 + void _periodicBufferOptimization() { + // 检查并清理过期的P帧(那些引用已经很久以前的I帧的P帧) + if (lastDecodedIFrameSeq != null) { + int threshold = lastDecodedIFrameSeq! - 10; // 设定一个阈值,清理引用过旧I帧的P帧 + + for (int i = state.h264FrameBuffer.length - 1; i >= 0; i--) { + final frame = state.h264FrameBuffer[i]; + if (frame['frameType'] == TalkDataH264Frame_FrameTypeE.P) { + int refIFrameSeq = frame['frameSeqI']; + if (refIFrameSeq < threshold) { + // 这个P帧引用的I帧太旧了,可以安全移除 + state.h264FrameBuffer.removeAt(i); + AppLog.log('清理过期P帧,seq: ${frame['frameSeq']}, 引用的I帧seq: $refIFrameSeq'); + } + } + } + } + } + + // 新增-------m 根据缓冲区使用率调整处理策略 + void _adjustProcessingStrategyByBufferUsage() { + double bufferUsage = state.h264FrameBuffer.length / state.maxFrameBufferSize; + + // 当渲染启动后,快速消耗缓冲区以减少卡顿 + if (_isRenderingStarted && _isConsumingForRender) { + state.frameProcessIntervalMs = 2; // 极速处理 + return; + } + + // 当缓冲区使用率超过80%时,加快处理速度 + if (bufferUsage > 0.8) { + // 暂时缩短帧处理间隔以加快处理 + if (state.frameProcessIntervalMs >= 10) { + state.frameProcessIntervalMs = 2; // 加快处理速度 + AppLog.log('缓冲区使用率高(${(bufferUsage * 100).toStringAsFixed(1)}%),加快帧处理速度'); + } + } else if (bufferUsage < 0.3 && state.frameProcessIntervalMs == 2) { + // 当缓冲区使用率下降时,恢复正常的处理速度 + state.frameProcessIntervalMs = 5; // 恢复正常处理速度 + AppLog.log('缓冲区使用率正常(${(bufferUsage * 100).toStringAsFixed(1)}%),恢复正常帧处理速度'); + } + + // 当缓冲区使用率过高时,考虑临时增加最大缓冲区大小 + if (bufferUsage > 0.95 && state.maxFrameBufferSize < state.adaptiveBufferSizeMax) { + int newBufferSize = state.maxFrameBufferSize + 2; + if (newBufferSize <= state.adaptiveBufferSizeMax) { + state.maxFrameBufferSize = newBufferSize; + AppLog.log('缓冲区使用率极高,临时扩大缓冲区到 $newBufferSize'); + } + } + } + + // 新增-------m 执行预清理机制,减少缓冲区满载导致的卡顿 + void _performPreemptiveCleanup() { + + // 计算目标清理数量(清理到缓冲区容量的50%) + int targetSize = state.maxFrameBufferSize ~/ 2; + int framesToRemove = state.h264FrameBuffer.length - targetSize; + + if (framesToRemove <= 0) return; + + // 优先移除P帧,保留I帧以确保视频质量 + int removedCount = 0; + + // 先移除过期的P帧(不属于任何活动I帧的P帧) + for (int i = state.h264FrameBuffer.length - 1; i >= 0 && removedCount < framesToRemove; i--) { + final frame = state.h264FrameBuffer[i]; + if (frame['frameType'] == TalkDataH264Frame_FrameTypeE.P) { + // 检查P帧是否关联到已处理过的I帧 + int refIFrameSeq = frame['frameSeqI']; + if (lastDecodedIFrameSeq != null && refIFrameSeq < lastDecodedIFrameSeq!) { + // 这个P帧关联的I帧已经过期,可以安全移除 + state.h264FrameBuffer.removeAt(i); + removedCount++; + } + } + } + + // 如果仍需要移除更多帧,继续移除P帧 + if (removedCount < framesToRemove) { + for (int i = state.h264FrameBuffer.length - 1; i >= 0 && removedCount < framesToRemove; i--) { + if (state.h264FrameBuffer[i]['frameType'] == TalkDataH264Frame_FrameTypeE.P) { + state.h264FrameBuffer.removeAt(i); + removedCount++; + } + } + } + + // 如果还需要移除更多帧,再移除B帧(如果有的话)或其他非关键帧 + while (removedCount < framesToRemove && state.h264FrameBuffer.length > targetSize) { + state.h264FrameBuffer.removeAt(0); // 移除最旧的帧 + removedCount++; + } + } + /// 启动帧处理定时器 void _startFrameProcessTimer() { // 取消已有定时器 @@ -453,15 +640,122 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { return; } + // 根据缓冲区使用率调整处理策略 + _adjustProcessingStrategyByBufferUsage(); + try { - // 优先查找I帧,按frameSeq最小的I帧消费 - final iFrames = state.h264FrameBuffer.where((f) => f['frameType'] == TalkDataH264Frame_FrameTypeE.I).toList(); - if (iFrames.isNotEmpty) { - iFrames.sort((a, b) => (a['frameSeq'] as int).compareTo(b['frameSeq'] as int)); - final minIFrame = iFrames.first; - final minIFrameSeq = minIFrame['frameSeq']; + // 在渲染启动前后,采取不同的处理策略 + bool isBufferHalfFull = state.h264FrameBuffer.length >= (state.maxFrameBufferSize ~/ 2); + + // 检查是否在渲染启动初期,如果是则优先处理最新帧 + bool isEarlyRenderPhase = _isRenderingStarted && + (DateTime.now().millisecondsSinceEpoch - _lastRenderStartTime < 3000); + + if (isEarlyRenderPhase || isBufferHalfFull) { + await _processLatestFrame(isBufferHalfFull); + } + } finally { + final endTime = DateTime.now().microsecondsSinceEpoch; + final durationMs = (endTime - startTime) / 1000.0; + // 可选:只在耗时较长时打印(例如 > 5ms) + if (durationMs > 5) { + debugPrint('[_processNextFrameFromBuffer] 耗时: ${durationMs.toStringAsFixed(2)} ms'); + // 或使用你的日志系统,如: + // AppLog.log('Frame processing took ${durationMs.toStringAsFixed(2)} ms'); + } + } + } + + /// 处理最新接收的帧 + Future _processLatestFrame(bool isBufferHalfFull) async { + // 优先查找最新的I帧 + final iFrames = state.h264FrameBuffer.where((f) => f['frameType'] == TalkDataH264Frame_FrameTypeE.I).toList(); + iFrames.sort((a, b) => (a['frameSeq'] as int).compareTo(b['frameSeq'] as int)); + + if (iFrames.isNotEmpty) { + final latestIFrame = iFrames.first; + final latestIFrameSeq = latestIFrame['frameSeq']; + //final targetIndex = state.h264FrameBuffer.indexWhere((f) => f['frameType'] == TalkDataH264Frame_FrameTypeE.I && f['frameSeq'] == minIFrameSeq, + // 按frameSeq排序,获取最新的I帧 + final targetIndex = state.h264FrameBuffer.indexWhere((f) => f['frameType'] == TalkDataH264Frame_FrameTypeE.I && f['frameSeq'] == latestIFrameSeq, + ); + state.isProcessingFrame = true; + final Map? frameMap = state.h264FrameBuffer.removeAt(targetIndex); + if (frameMap == null) { + state.isProcessingFrame = false; + return; + } + final List? frameData = frameMap['frameData']; + final TalkDataH264Frame_FrameTypeE? frameType = frameMap['frameType']; + final int? frameSeq = frameMap['frameSeq']; + final int? frameSeqI = frameMap['frameSeqI']; + final int? pts = frameMap['pts']; + if (frameData == null || frameType == null || frameSeq == null || frameSeqI == null || pts == null) { + state.isProcessingFrame = false; + return; + } + if (state.textureId.value == null) { + state.isProcessingFrame = false; + return; + } + lastDecodedIFrameSeq = latestIFrameSeq; + + // 开始解码时间 + final int decodeStartTime = DateTime.now().millisecondsSinceEpoch; + + await VideoDecodePlugin.sendFrame( + frameData: frameData, + frameType: 0, + frameSeq: frameSeq, + timestamp: pts, + splitNalFromIFrame: true, + refIFrameSeq: frameSeqI, + ); + // 解码结束时间 + final decodeEndTime = DateTime.now().millisecondsSinceEpoch; + + // 新增-------m 监控解码性能 + final decodeDuration = decodeEndTime - decodeStartTime; + if (decodeDuration > _slowDecodeThreshold) { + _slowDecodeCount++; + if (decodeEndTime - _lastSlowDecodeTime < 5000) { // 5秒内多次慢解码 + if (_slowDecodeCount >= _slowDecodeAdjustmentThreshold) { + // 触发性能调整 + _handleSlowDecodePerformance(); + _slowDecodeCount = 0; // 重置计数 + } + } else { + _slowDecodeCount = 1; // 重置计数但保留记录 + } + _lastSlowDecodeTime = decodeEndTime; + } else { + // 如果一段时间内没有慢解码,减少计数 + if (decodeEndTime - _lastSlowDecodeTime > 10000) { // 10秒后重置 + _slowDecodeCount = 0; + } + } + + state.isProcessingFrame = false; + _frameCount++; // 增加帧计数 + + // 检查缓冲区大小,如果过大可能表示卡顿 + if (state.h264FrameBuffer.length > state.maxFrameBufferSize * 0.8) { + AppLog.log('视频卡顿警告: 缓冲区占用过高 (${state.h264FrameBuffer.length}/${state.maxFrameBufferSize}), 可能出现卡顿'); + } + + return; + } + + // 没有I帧时,只消费refIFrameSeq等于lastDecodedIFrameSeq的P帧 + if (lastDecodedIFrameSeq != null) { + final validPFrames = + state.h264FrameBuffer.where((f) => f['frameType'] == TalkDataH264Frame_FrameTypeE.P && f['frameSeqI'] == lastDecodedIFrameSeq).toList(); + if (validPFrames.isNotEmpty) { + validPFrames.sort((a, b) => (b['frameSeq'] as int).compareTo(a['frameSeq'] as int)); + final latestPFrame = validPFrames.first; final targetIndex = state.h264FrameBuffer.indexWhere( - (f) => f['frameType'] == TalkDataH264Frame_FrameTypeE.I && f['frameSeq'] == minIFrameSeq, + (f) => + f['frameType'] == TalkDataH264Frame_FrameTypeE.P && f['frameSeq'] == latestPFrame['frameSeq'] && f['frameSeqI'] == lastDecodedIFrameSeq, ); state.isProcessingFrame = true; final Map? frameMap = state.h264FrameBuffer.removeAt(targetIndex); @@ -482,20 +776,23 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { state.isProcessingFrame = false; return; } - lastDecodedIFrameSeq = minIFrameSeq; + // 监控解码性能 开始时间 final decodeStartTime = DateTime.now().millisecondsSinceEpoch; + await VideoDecodePlugin.sendFrame( frameData: frameData, - frameType: 0, + frameType: 1, frameSeq: frameSeq, timestamp: pts, splitNalFromIFrame: true, refIFrameSeq: frameSeqI, ); + + // 监控解码性能 结束时间 final decodeEndTime = DateTime.now().millisecondsSinceEpoch; - - // 监控解码性能 + + // 新增-------m 监控解码性能 final decodeDuration = decodeEndTime - decodeStartTime; if (decodeDuration > _slowDecodeThreshold) { _slowDecodeCount++; @@ -517,92 +814,22 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { } state.isProcessingFrame = false; - _frameCount++; // 增加帧计数 + + // 新增-------m 增加帧计数 + _frameCount++; + + // 新增-------m 检查缓冲区大小,如果过大可能表示卡顿 + if (state.h264FrameBuffer.length > state.maxFrameBufferSize * 0.8) { + AppLog.log('视频卡顿警告: 缓冲区占用过高 (${state.h264FrameBuffer.length}/${state.maxFrameBufferSize}), 可能出现卡顿'); + } + return; } - - // 没有I帧时,只消费refIFrameSeq等于lastDecodedIFrameSeq的P帧 - if (lastDecodedIFrameSeq != null) { - final validPFrames = - state.h264FrameBuffer.where((f) => f['frameType'] == TalkDataH264Frame_FrameTypeE.P && f['frameSeqI'] == lastDecodedIFrameSeq).toList(); - if (validPFrames.isNotEmpty) { - validPFrames.sort((a, b) => (a['frameSeq'] as int).compareTo(b['frameSeq'] as int)); - final minPFrame = validPFrames.first; - final targetIndex = state.h264FrameBuffer.indexWhere( - (f) => - f['frameType'] == TalkDataH264Frame_FrameTypeE.P && f['frameSeq'] == minPFrame['frameSeq'] && f['frameSeqI'] == lastDecodedIFrameSeq, - ); - state.isProcessingFrame = true; - final Map? frameMap = state.h264FrameBuffer.removeAt(targetIndex); - if (frameMap == null) { - state.isProcessingFrame = false; - return; - } - final List? frameData = frameMap['frameData']; - final TalkDataH264Frame_FrameTypeE? frameType = frameMap['frameType']; - final int? frameSeq = frameMap['frameSeq']; - final int? frameSeqI = frameMap['frameSeqI']; - final int? pts = frameMap['pts']; - if (frameData == null || frameType == null || frameSeq == null || frameSeqI == null || pts == null) { - state.isProcessingFrame = false; - return; - } - if (state.textureId.value == null) { - state.isProcessingFrame = false; - return; - } - - final decodeStartTime = DateTime.now().millisecondsSinceEpoch; - await VideoDecodePlugin.sendFrame( - frameData: frameData, - frameType: 1, - frameSeq: frameSeq, - timestamp: pts, - splitNalFromIFrame: true, - refIFrameSeq: frameSeqI, - ); - final decodeEndTime = DateTime.now().millisecondsSinceEpoch; - - // 监控解码性能 - final decodeDuration = decodeEndTime - decodeStartTime; - if (decodeDuration > _slowDecodeThreshold) { - _slowDecodeCount++; - if (decodeEndTime - _lastSlowDecodeTime < 5000) { // 5秒内多次慢解码 - if (_slowDecodeCount >= _slowDecodeAdjustmentThreshold) { - // 触发性能调整 - _handleSlowDecodePerformance(); - _slowDecodeCount = 0; // 重置计数 - } - } else { - _slowDecodeCount = 1; // 重置计数但保留记录 - } - _lastSlowDecodeTime = decodeEndTime; - } else { - // 如果一段时间内没有慢解码,减少计数 - if (decodeEndTime - _lastSlowDecodeTime > 10000) { // 10秒后重置 - _slowDecodeCount = 0; - } - } - - state.isProcessingFrame = false; - _frameCount++; // 增加帧计数 - return; - } - } - // 其他情况不消费,等待I帧到来 - } finally { - final endTime = DateTime.now().microsecondsSinceEpoch; - final durationMs = (endTime - startTime) / 1000.0; - // 可选:只在耗时较长时打印(例如 > 10ms) - if (durationMs > 10) { - debugPrint('[_processNextFrameFromBuffer] 耗时: ${durationMs.toStringAsFixed(2)} ms'); - // 或使用你的日志系统,如: - // AppLog.log('Frame processing took ${durationMs.toStringAsFixed(2)} ms'); - } } + // 其他情况不消费,等待I帧到来 } - /// 处理慢解码性能问题 + // 新增-------m 处理慢解码性能问题 void _handleSlowDecodePerformance() { AppLog.log('检测到连续慢解码,触发性能优化'); @@ -613,6 +840,60 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { } } + // 新增-------m 启动卡顿检测定时器 + void _startStutterDetection() { + _stutterDetectionTimer?.cancel(); + _stutterDetectionTimer = Timer.periodic(Duration(milliseconds: _stutterDetectionInterval), (timer) { + _checkForStutter(); + }); + } + + // 新增-------m 检查视频卡顿 + void _checkForStutter() { + final currentTime = DateTime.now().millisecondsSinceEpoch; + final currentFrameCount = state.totalFrames.value; + + if (_lastFrameProcessTime != 0 && _lastFrameCount != 0) { + final timeDiff = currentTime - _lastFrameProcessTime; + final frameDiff = currentFrameCount - _lastFrameCount; + final actualFps = timeDiff > 0 ? (frameDiff / (timeDiff / 1000.0)) : 0.0; + + // 计算期望帧数 + final expectedFrameCount = _expectedFps * (timeDiff / 1000.0); + final droppedFrameCount = (expectedFrameCount - frameDiff).toInt(); + + // 检查是否出现卡顿 + if (actualFps < _expectedFps * 0.6) { // 实际FPS低于期望FPS的60%,认为出现卡顿 + AppLog.log('视频卡顿检测: 期望FPS: ${_expectedFps.toStringAsFixed(2)}, 实际FPS: ${actualFps.toStringAsFixed(2)}, 时间间隔: ${timeDiff}ms, 期望帧数: ${expectedFrameCount.toStringAsFixed(0)}, 实际帧数: ${frameDiff}, 丢弃帧数: ${droppedFrameCount}'); + + // 记录卡顿统计 + state.droppedFrames.value += droppedFrameCount > 0 ? droppedFrameCount : 0; + } + + // 如果丢帧数量过多,也需要警告 + if (droppedFrameCount > _frameDropThreshold) { + AppLog.log('视频丢帧警告: 期望帧数: ${expectedFrameCount.toStringAsFixed(0)}, 实际帧数: ${frameDiff}, 丢弃帧数: ${droppedFrameCount}'); + } + } + + // 更新上次统计的时间和帧数 + _lastFrameProcessTime = currentTime; + _lastFrameCount = currentFrameCount; + } + + // 新增-------m 在渲染启动前后优化缓冲区管理 + void _optimizeBufferForRender() { + // 检查是否接近渲染启动,如果是则减少缓冲区大小以快速开始渲染 + if (!_isRenderingStarted && state.h264FrameBuffer.length > 3) { + // 在渲染开始前,如果缓冲区有足够帧(I帧+P帧),可以提前开始渲染 + final iFrames = state.h264FrameBuffer.where((f) => f['frameType'] == TalkDataH264Frame_FrameTypeE.I).toList(); + if (iFrames.isNotEmpty) { + // 至少有一个I帧,可以开始渲染 + AppLog.log('[Render] 渲染启动,outputFrameQueue已达低水位: ${state.h264FrameBuffer.length}'); + } + } + } + /// 停止帧处理定时器 void _stopFrameProcessTimer() { state.frameProcessTimer?.cancel(); diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart index bf16fc12..023c2a52 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart @@ -109,12 +109,12 @@ class TalkViewNativeDecodeState { // H264帧缓冲区相关 final List> h264FrameBuffer = >[]; // H264帧缓冲区,存储帧数据和类型 - int maxFrameBufferSize = 8; // 最大缓冲区大小,增加以改善卡顿,从2增加到8 + int maxFrameBufferSize = 6; // 最大缓冲区大小,从8减少到6以减少渲染初期卡顿 final int targetFps = 25; // 目标解码帧率,只是为了快速填充native的缓冲区 final int adaptiveBufferSizeMin = 2; // 自适应缓冲区最小大小 - final int adaptiveBufferSizeMax = 8; // 自适应缓冲区最大大小,从6增加到8 + final int adaptiveBufferSizeMax = 6; // 自适应缓冲区最大大小,从8减少到6 final int networkQualityCheckIntervalMs = 2000; // 网络质量检查间隔(毫秒) - int frameProcessIntervalMs = 5; // 帧处理间隔(毫秒),提高响应速度,从10减到5 + int frameProcessIntervalMs = 5; // 帧处理间隔(毫秒),调整为5ms平衡性能和流畅度 Timer? frameProcessTimer; // 帧处理定时器 bool isProcessingFrame = false; // 是否正在处理帧 int lastProcessedTimestamp = 0; // 上次处理帧的时间戳