视频对讲---排查卡顿
This commit is contained in:
parent
113a6a345e
commit
47e4b86086
@ -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<void> _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<void> _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<void> _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<String, dynamic>? frameMap = state.h264FrameBuffer.removeAt(targetIndex);
|
||||
if (frameMap == null) {
|
||||
state.isProcessingFrame = false;
|
||||
return;
|
||||
}
|
||||
final List<int>? 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<String, dynamic>? 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<String, dynamic>? frameMap = state.h264FrameBuffer.removeAt(targetIndex);
|
||||
if (frameMap == null) {
|
||||
state.isProcessingFrame = false;
|
||||
return;
|
||||
}
|
||||
final List<int>? 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();
|
||||
|
||||
@ -109,12 +109,12 @@ class TalkViewNativeDecodeState {
|
||||
|
||||
// H264帧缓冲区相关
|
||||
final List<Map<String, dynamic>> h264FrameBuffer = <Map<String, dynamic>>[]; // 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; // 上次处理帧的时间戳
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user