feat(decode): 用 WebRTC stats 替代原生探针计算解码耗时
This commit is contained in:
@@ -235,6 +235,7 @@ class WebRtcController {
|
||||
MethodChannel('webrtc_decode_probe');
|
||||
|
||||
Future<void> _attachWebRtcDecodeProbe(String trackId) async {
|
||||
debugPrint('[WebRtcController] Attaching decode probe for track: $trackId');
|
||||
try {
|
||||
await _decodeProbeChannel.invokeMethod<void>(
|
||||
'attachWebRtcDecodeProbe', {'trackId': trackId});
|
||||
@@ -471,6 +472,10 @@ class WebRtcController {
|
||||
/// 上一次采样时的累计解码帧数,用于计算自编码模式下的解码帧率。
|
||||
int _prevDecodeCount = 0;
|
||||
|
||||
/// 上一次采样时的累计解码时间(秒)与解码帧数,用于计算 WebRTC 模式下的解码耗时。
|
||||
double _prevTotalDecodeTime = 0.0;
|
||||
int _prevFramesDecoded = 0;
|
||||
|
||||
/// WebRTC 全托管模式下,由原生 VideoSink 探针实测的解码耗时统计缓存。
|
||||
_DecodeProbeStats? _webRtcDecodeProbe;
|
||||
|
||||
@@ -509,6 +514,9 @@ class WebRtcController {
|
||||
int dcMessagesSent = 0;
|
||||
int dcMessagesReceived = 0;
|
||||
|
||||
double currentTotalDecodeTime = 0.0;
|
||||
int currentFramesDecoded = 0;
|
||||
|
||||
// 与上次采样的时间差(秒),供循环内估算解码帧率,也供下方计算每秒网速。
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
final double dtSec =
|
||||
@@ -518,6 +526,12 @@ class WebRtcController {
|
||||
final values = report.values;
|
||||
final type = report.type;
|
||||
if (type == 'inbound-rtp' && values['kind'] == 'video') {
|
||||
// 关键数据打印,用于排查 totalDecodeTime 是否存在于当前平台的统计中
|
||||
debugPrint('[WebRtcController] inbound-rtp: totalDecodeTime=${values['totalDecodeTime']}, '
|
||||
'googDecodeMs=${values['googDecodeMs']}, '
|
||||
'framesDecoded=${values['framesDecoded']}, '
|
||||
'fps=${values['framesPerSecond']}');
|
||||
|
||||
width = values['frameWidth'] ?? '-';
|
||||
height = values['frameHeight'] ?? '-';
|
||||
fps = values['framesPerSecond'] ?? '-';
|
||||
@@ -535,6 +549,12 @@ class WebRtcController {
|
||||
final fd = values['framesDropped'];
|
||||
if (fd is num) framesDropped = fd;
|
||||
|
||||
// 核心:解析解码统计(兼容标准字段与旧版 Google 私有字段)
|
||||
currentTotalDecodeTime = (values['totalDecodeTime'] as num?)?.toDouble() ??
|
||||
((values['googDecodeMs'] as num?)?.toDouble() ?? 0.0) / 1000.0;
|
||||
currentFramesDecoded = (values['framesDecoded'] as num?)?.toInt() ??
|
||||
(values['googFramesDecoded'] as num?)?.toInt() ?? 0;
|
||||
|
||||
final codecId = values['codecId'];
|
||||
if (codecId != null) {
|
||||
final codecReport = reports.where((r) => r.id == codecId).firstOrNull;
|
||||
@@ -607,52 +627,72 @@ class WebRtcController {
|
||||
? '已连接'
|
||||
: (dcState == null ? '未连接' : dcState.name);
|
||||
|
||||
// —— 自编码(自建 MediaCodec 硬解)补充:真实分辨率 + 解码耗时 ——
|
||||
// 自编码模式视频走 DataChannel 透传裸码流,WebRTC 的视频统计(分辨率/帧率/
|
||||
// 解码格式)均为空。这里只要解码器处于活动状态就读取原生解码统计,用解码器
|
||||
// 上报的真实分辨率/解码帧率覆盖 WebRTC 的空值,并把解码耗时一并显示,
|
||||
// 确保「分辨率」与「解码耗时」这些信息显示在一起。
|
||||
// —— 分模式统计解码耗时 ——
|
||||
String? decodeInfo;
|
||||
// 基础行要展示的解码耗时:WebRTC 模式用 inbound-rtp 估算,自编码模式用原生平均。
|
||||
double nativeAvg = 0;
|
||||
if (_selfCodecDecoder != null) {
|
||||
final ds = await _selfCodecDecoder!.getStats();
|
||||
if (ds != null) {
|
||||
final last = (ds['lastMs'] as num?)?.toDouble() ?? 0;
|
||||
final avg = (ds['avgMs'] as num?)?.toDouble() ?? 0;
|
||||
final peak = (ds['maxMs'] as num?)?.toDouble() ?? 0;
|
||||
final cnt = (ds['count'] as num?)?.toInt() ?? 0;
|
||||
nativeAvg = avg;
|
||||
if (_selfCodecWidth > 0 && _selfCodecHeight > 0) {
|
||||
width = _selfCodecWidth;
|
||||
height = _selfCodecHeight;
|
||||
}
|
||||
double decodeFps = 0;
|
||||
if (dtSec > 0 && cnt > 0) {
|
||||
decodeFps = (cnt - _prevDecodeCount) / dtSec;
|
||||
}
|
||||
_prevDecodeCount = cnt;
|
||||
if (decodeFps > 0) fps = decodeFps.round();
|
||||
decodeInfo = '解码耗时: ${last.toStringAsFixed(1)} ms 平均: ${avg.toStringAsFixed(1)} ms 峰值: ${peak.toStringAsFixed(1)} ms\n'
|
||||
'解码帧率: ${decodeFps.toStringAsFixed(0)} fps 已解码: $cnt 帧';
|
||||
}
|
||||
}
|
||||
// 无活动解码器时清零计数,避免重连后帧率计算异常。
|
||||
if (_selfCodecDecoder == null) _prevDecodeCount = 0;
|
||||
|
||||
// WebRTC 全托管模式:flutter_webrtc 1.5.2 不报 totalDecodeTime / framesDecoded,
|
||||
// 改为由原生 VideoSink 探针逐帧实测真实解码耗时(last/avg/peak)与帧率。
|
||||
// 探针在远端视频轨道额外挂一个 VideoSink,记录每帧真实到达时间,算出帧间
|
||||
// 耗时(≈ 真实解码+渲染周期),是原生层实测值,不会恒为 0。
|
||||
if (_selfCodecDecoder == null) {
|
||||
_webRtcDecodeProbe = await _getWebRtcDecodeStats();
|
||||
final p = _webRtcDecodeProbe;
|
||||
if (p != null && p.totalFrames > 0) {
|
||||
nativeAvg = p.avgMs;
|
||||
decodeInfo =
|
||||
'解码耗时: ${p.lastMs.toStringAsFixed(1)} ms 平均: ${p.avgMs.toStringAsFixed(1)} ms 峰值: ${p.peakMs.toStringAsFixed(1)} ms\n'
|
||||
'解码帧率: ${p.fpsNow} fps (平均 ${p.fpsAvg} fps) 已解码: ${p.totalFrames} 帧';
|
||||
if (_currentStreamMode == streamModeSelfCodec) {
|
||||
// 【自编码模式】:视频走 DataChannel 透传裸码流,WebRTC 的视频统计为空。
|
||||
// 使用原生解码器上报的真实分辨率、帧率与耗时。
|
||||
if (_selfCodecDecoder != null) {
|
||||
final ds = await _selfCodecDecoder!.getStats();
|
||||
if (ds != null) {
|
||||
final last = (ds['lastMs'] as num?)?.toDouble() ?? 0;
|
||||
final avg = (ds['avgMs'] as num?)?.toDouble() ?? 0;
|
||||
final peak = (ds['maxMs'] as num?)?.toDouble() ?? 0;
|
||||
final cnt = (ds['count'] as num?)?.toInt() ?? 0;
|
||||
nativeAvg = avg;
|
||||
if (_selfCodecWidth > 0 && _selfCodecHeight > 0) {
|
||||
width = _selfCodecWidth;
|
||||
height = _selfCodecHeight;
|
||||
}
|
||||
double decodeFps = 0;
|
||||
if (dtSec > 0 && cnt > 0) {
|
||||
decodeFps = (cnt - _prevDecodeCount) / dtSec;
|
||||
}
|
||||
_prevDecodeCount = cnt;
|
||||
if (decodeFps > 0) fps = decodeFps.round();
|
||||
decodeInfo = '解码耗时: ${last.toStringAsFixed(1)} ms 平均: ${avg.toStringAsFixed(1)} ms 峰值: ${peak.toStringAsFixed(1)} ms\n'
|
||||
'解码帧率: ${decodeFps.toStringAsFixed(0)} fps 已解码: $cnt 帧';
|
||||
}
|
||||
}
|
||||
// 自编码模式下,WebRTC 的相关计数建议重置,避免切回时出现跳变。
|
||||
_prevTotalDecodeTime = currentTotalDecodeTime;
|
||||
_prevFramesDecoded = currentFramesDecoded;
|
||||
} else {
|
||||
// 【WebRTC 模式】:优先使用标准的 getStats() 增量计算耗时。
|
||||
double avg = 0;
|
||||
if (currentFramesDecoded > 0) {
|
||||
if (_prevFramesDecoded > 0 && currentFramesDecoded > _prevFramesDecoded) {
|
||||
final deltaMs = (currentTotalDecodeTime - _prevTotalDecodeTime) * 1000.0;
|
||||
final deltaFrames = currentFramesDecoded - _prevFramesDecoded;
|
||||
if (deltaFrames > 0) avg = deltaMs / deltaFrames;
|
||||
} else if (currentTotalDecodeTime > 0) {
|
||||
// 兜底:第一次采样或帧数无增量时,用全量平均作为初始参考
|
||||
avg = (currentTotalDecodeTime * 1000.0) / currentFramesDecoded;
|
||||
}
|
||||
}
|
||||
|
||||
if (avg > 0) {
|
||||
nativeAvg = avg;
|
||||
decodeInfo = '解码耗时 (stats): ${avg.toStringAsFixed(1)} ms\n'
|
||||
'解码帧率: $fps fps 已解码: $currentFramesDecoded 帧';
|
||||
} else {
|
||||
// 备选方案:如果 getStats 没报数据(部分平台/环境限制),则回退到原生 VideoSink 探针实测模式。
|
||||
_webRtcDecodeProbe = await _getWebRtcDecodeStats();
|
||||
final p = _webRtcDecodeProbe;
|
||||
if (p != null && p.totalFrames > 0) {
|
||||
nativeAvg = p.avgMs;
|
||||
decodeInfo =
|
||||
'解码耗时 (probe): ${p.lastMs.toStringAsFixed(1)} ms 平均: ${p.avgMs.toStringAsFixed(1)} ms 峰值: ${p.peakMs.toStringAsFixed(1)} ms\n'
|
||||
'解码帧率: ${p.fpsNow} fps (平均 ${p.fpsAvg} fps) 已解码: ${p.totalFrames} 帧';
|
||||
}
|
||||
}
|
||||
// WebRTC 模式下,自编码的计数重置。
|
||||
_prevDecodeCount = 0;
|
||||
_prevTotalDecodeTime = currentTotalDecodeTime;
|
||||
_prevFramesDecoded = currentFramesDecoded;
|
||||
}
|
||||
|
||||
String decodeTimeText = '-';
|
||||
@@ -761,6 +801,8 @@ class WebRtcController {
|
||||
_prevBytesSent = 0;
|
||||
_prevStatsTimestamp = 0;
|
||||
_prevDecodeCount = 0;
|
||||
_prevTotalDecodeTime = 0.0;
|
||||
_prevFramesDecoded = 0;
|
||||
_webRtcDecodeProbe = null;
|
||||
_selfCodecWidth = 0;
|
||||
_selfCodecHeight = 0;
|
||||
|
||||
Reference in New Issue
Block a user