feat(webrtc): 新增解码耗时统计并修复黑屏问题
1. 新增自编码与全托管模式的解码耗时探针(VideoSink),统计帧率与解码耗时。 2. 修复 SPS 与 PPS 作为独立 CONFIG 单元发送时被误丢弃导致 H.264 永久黑屏的问题。 3. 优化分片重组缓冲,丢弃过期单元并限制缓冲堆积以防内存泄漏。
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
|
||||
import 'self_codec_decoder.dart';
|
||||
@@ -9,6 +10,24 @@ import '../proto/control_message.pb.dart';
|
||||
import '../signaling/signaling_client.dart';
|
||||
import '../utils/control_commands.dart';
|
||||
|
||||
/// WebRTC 全托管模式下,由原生 VideoSink 探针逐帧实测得到的解码耗时统计。
|
||||
class _DecodeProbeStats {
|
||||
final double lastMs;
|
||||
final double avgMs;
|
||||
final double peakMs;
|
||||
final int totalFrames;
|
||||
final int fpsNow;
|
||||
final int fpsAvg;
|
||||
const _DecodeProbeStats({
|
||||
required this.lastMs,
|
||||
required this.avgMs,
|
||||
required this.peakMs,
|
||||
required this.totalFrames,
|
||||
required this.fpsNow,
|
||||
required this.fpsAvg,
|
||||
});
|
||||
}
|
||||
|
||||
/// 封装 WebRTC 连接逻辑(对应 Android 端 WebRtcClient)。
|
||||
///
|
||||
/// 职责:
|
||||
@@ -204,6 +223,53 @@ class WebRtcController {
|
||||
}
|
||||
renderer.srcObject = stream;
|
||||
onRemoteStream?.call(renderer);
|
||||
// 仅在 WebRTC 全托管模式 attach 解码耗时探针(自编码模式用自己的解码器统计)。
|
||||
if (_selfCodecDecoder == null) {
|
||||
final trackId = event.track.id;
|
||||
if (trackId != null) _attachWebRtcDecodeProbe(trackId);
|
||||
}
|
||||
}
|
||||
|
||||
/// 解码耗时探针通道(原生侧给远端视频轨道 attach 一个测量 VideoSink)。
|
||||
static const MethodChannel _decodeProbeChannel =
|
||||
MethodChannel('webrtc_decode_probe');
|
||||
|
||||
Future<void> _attachWebRtcDecodeProbe(String trackId) async {
|
||||
try {
|
||||
await _decodeProbeChannel.invokeMethod<void>(
|
||||
'attachWebRtcDecodeProbe', {'trackId': trackId});
|
||||
} catch (e) {
|
||||
debugPrint('[decodeProbe] attach failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _detachWebRtcDecodeProbe() async {
|
||||
try {
|
||||
await _decodeProbeChannel.invokeMethod<void>('detachWebRtcDecodeProbe');
|
||||
} catch (e) {
|
||||
debugPrint('[decodeProbe] detach failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<_DecodeProbeStats?> _getWebRtcDecodeStats() async {
|
||||
try {
|
||||
final res =
|
||||
await _decodeProbeChannel.invokeMethod<dynamic>('getWebRtcDecodeStats');
|
||||
if (res is Map) {
|
||||
final m = res.cast<String, dynamic>();
|
||||
return _DecodeProbeStats(
|
||||
lastMs: (m['lastMs'] as num? ?? 0).toDouble(),
|
||||
avgMs: (m['avgMs'] as num? ?? 0).toDouble(),
|
||||
peakMs: (m['peakMs'] as num? ?? 0).toDouble(),
|
||||
totalFrames: (m['totalFrames'] as num? ?? 0).toInt(),
|
||||
fpsNow: (m['fpsNow'] as num? ?? 0).toInt(),
|
||||
fpsAvg: (m['fpsAvg'] as num? ?? 0).toInt(),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[decodeProbe] getStats failed: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void _onDataChannel(RTCDataChannel channel) {
|
||||
@@ -264,7 +330,11 @@ class WebRtcController {
|
||||
_creatingDecoder = true;
|
||||
try {
|
||||
final decoder = SelfCodecDecoder(
|
||||
onSize: (w, h) => onResolutionReported?.call(w, h),
|
||||
onSize: (w, h) {
|
||||
_selfCodecWidth = w;
|
||||
_selfCodecHeight = h;
|
||||
onResolutionReported?.call(w, h);
|
||||
},
|
||||
onError: (err) => debugPrint('自编码解码器: $err'),
|
||||
);
|
||||
final textureId = await decoder.create();
|
||||
@@ -294,6 +364,10 @@ class WebRtcController {
|
||||
onStreamModeReport?.call(msg.streamMode);
|
||||
break;
|
||||
case Action.REPORT_RESOLUTION:
|
||||
if (msg.width > 0 && msg.height > 0) {
|
||||
_selfCodecWidth = msg.width;
|
||||
_selfCodecHeight = msg.height;
|
||||
}
|
||||
onResolutionReported?.call(msg.width, msg.height);
|
||||
break;
|
||||
default:
|
||||
@@ -394,6 +468,16 @@ class WebRtcController {
|
||||
int _prevBytesSent = 0;
|
||||
int _prevStatsTimestamp = 0;
|
||||
|
||||
/// 上一次采样时的累计解码帧数,用于计算自编码模式下的解码帧率。
|
||||
int _prevDecodeCount = 0;
|
||||
|
||||
/// WebRTC 全托管模式下,由原生 VideoSink 探针实测的解码耗时统计缓存。
|
||||
_DecodeProbeStats? _webRtcDecodeProbe;
|
||||
|
||||
/// 解码器上报的真实分辨率(自编码模式下 WebRTC 视频统计为空,需用此覆盖)。
|
||||
int _selfCodecWidth = 0;
|
||||
int _selfCodecHeight = 0;
|
||||
|
||||
/// 连接建立时的时间戳(毫秒),用于计算连接时长。
|
||||
int? _connectedAt;
|
||||
|
||||
@@ -425,6 +509,11 @@ class WebRtcController {
|
||||
int dcMessagesSent = 0;
|
||||
int dcMessagesReceived = 0;
|
||||
|
||||
// 与上次采样的时间差(秒),供循环内估算解码帧率,也供下方计算每秒网速。
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
final double dtSec =
|
||||
_prevStatsTimestamp != 0 ? (now - _prevStatsTimestamp) / 1000.0 : 0;
|
||||
|
||||
for (final report in reports) {
|
||||
final values = report.values;
|
||||
final type = report.type;
|
||||
@@ -482,17 +571,13 @@ class WebRtcController {
|
||||
}
|
||||
|
||||
// 计算每秒网速(与上次采样差值)。
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
String downSpeed = '-';
|
||||
String upSpeed = '-';
|
||||
if (_prevStatsTimestamp != 0) {
|
||||
final dt = (now - _prevStatsTimestamp) / 1000.0;
|
||||
if (dt > 0) {
|
||||
final downBps = (bytesReceived - _prevBytesReceived) / dt;
|
||||
final upBps = (bytesSent - _prevBytesSent) / dt;
|
||||
downSpeed = _formatSpeed(downBps);
|
||||
upSpeed = _formatSpeed(upBps);
|
||||
}
|
||||
if (dtSec > 0) {
|
||||
final downBps = (bytesReceived - _prevBytesReceived) / dtSec;
|
||||
final upBps = (bytesSent - _prevBytesSent) / dtSec;
|
||||
downSpeed = _formatSpeed(downBps);
|
||||
upSpeed = _formatSpeed(upBps);
|
||||
}
|
||||
_prevBytesReceived = bytesReceived;
|
||||
_prevBytesSent = bytesSent;
|
||||
@@ -522,11 +607,66 @@ class WebRtcController {
|
||||
? '已连接'
|
||||
: (dcState == null ? '未连接' : dcState.name);
|
||||
|
||||
return '分辨率: ${width}x$height 帧率: $fps 延迟: $delay ms\n'
|
||||
// —— 自编码(自建 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} 帧';
|
||||
}
|
||||
}
|
||||
|
||||
String decodeTimeText = '-';
|
||||
if (decodeInfo != null && nativeAvg > 0) {
|
||||
decodeTimeText = '${nativeAvg.toStringAsFixed(1)} ms';
|
||||
}
|
||||
|
||||
final base = '分辨率: ${width}x$height 帧率: $fps 延迟: $delay ms 解码耗时: $decodeTimeText\n'
|
||||
'解码格式: $codec 抖动: $jitter ms 丢包: $lossRate%\n'
|
||||
'丢帧: $framesDropped ↓下载: $downSpeed ↑上传: $upSpeed\n'
|
||||
'连接类型: $connType ($protocol) 时长: $duration\n'
|
||||
'控制通道: $dcStatus 收发: $dcMessagesSent/$dcMessagesReceived';
|
||||
|
||||
return decodeInfo != null ? '$base\n$decodeInfo' : base;
|
||||
}
|
||||
|
||||
/// 依据选定候选对的本地/远端候选类型,返回 P2P / TURN 中继描述。
|
||||
@@ -591,6 +731,9 @@ class WebRtcController {
|
||||
try {
|
||||
await _dataChannel?.close();
|
||||
} catch (_) {}
|
||||
try {
|
||||
await _detachWebRtcDecodeProbe();
|
||||
} catch (_) {}
|
||||
try {
|
||||
await _pc?.close();
|
||||
} catch (_) {}
|
||||
@@ -617,6 +760,10 @@ class WebRtcController {
|
||||
_prevBytesReceived = 0;
|
||||
_prevBytesSent = 0;
|
||||
_prevStatsTimestamp = 0;
|
||||
_prevDecodeCount = 0;
|
||||
_webRtcDecodeProbe = null;
|
||||
_selfCodecWidth = 0;
|
||||
_selfCodecHeight = 0;
|
||||
_connectedAt = null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user