feat(decode): 用 WebRTC stats 替代原生探针计算解码耗时

This commit is contained in:
TongTongStudio
2026-07-25 11:18:34 +08:00
parent 4a33f3db0d
commit e5dafb65ed
3 changed files with 101 additions and 46 deletions

View File

@@ -44,3 +44,10 @@ kotlin {
flutter {
source = "../.."
}
dependencies {
// flutter_webrtc 依赖了 io.github.webrtc-sdkorg.webrtc.*),但它声明为
// implementationapp 模块默认看不到 org.webrtc.VideoTrack/VideoSink 等类型。
// 这里显式引入同版本,使解码耗时探针能直接引用 libwebrtc 的 VideoTrack/VideoSink。
implementation("io.github.webrtc-sdk:android:144.7559.09")
}

View File

@@ -16,7 +16,6 @@ import io.flutter.view.TextureRegistry
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
import org.webrtc.VideoFrame
import org.webrtc.VideoSink
import org.webrtc.VideoTrack
import com.cloudwebrtc.webrtc.FlutterWebRTCPlugin
@@ -90,7 +89,7 @@ private class DecodeProbeSink {
val nowMs = System.currentTimeMillis()
framesSinceFps++
if (lastFpsStampMs != 0L && nowMs - lastFpsStampMs >= 1000) {
fpsNow = framesSinceFps * 1000 / (nowMs - lastFpsStampMs)
fpsNow = framesSinceFps * 1000 / (nowMs - lastFpsStampMs).toInt()
framesSinceFps = 0
lastFpsStampMs = nowMs
} else if (lastFpsStampMs == 0L) {
@@ -254,15 +253,22 @@ class SelfCodecDecoderPlugin :
try {
val plugin = FlutterWebRTCPlugin.sharedSingleton
val track = plugin?.getRemoteTrack(trackId)
if (track !is VideoTrack) {
if (track == null) {
result.error("PROBE", "remote video track not found: $trackId", null)
return
}
// getRemoteTrack 返回 org.webrtc.MediaStreamTrack超类addSink 只在
// 子类 org.webrtc.VideoTrack 上,用安全强转拿到 VideoTrack。
val videoTrack = track as? VideoTrack
if (videoTrack == null) {
result.error("PROBE", "track is not a video track: $trackId", null)
return
}
detachWebRtcDecodeProbeInternal()
val stats = DecodeProbeSink()
val sink = VideoSink { stats.onFrame() }
track.addSink(sink)
probeHolder = ProbeHolder(track, sink, stats)
videoTrack.addSink(sink)
probeHolder = ProbeHolder(videoTrack, sink, stats)
result.success(null)
} catch (e: Exception) {
Log.e(TAG, "attachWebRtcDecodeProbe failed", e)

View File

@@ -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,14 +627,14 @@ class WebRtcController {
? '已连接'
: (dcState == null ? '未连接' : dcState.name);
// —— 自编码(自建 MediaCodec 硬解)补充:真实分辨率 + 解码耗时 ——
// 自编码模式视频走 DataChannel 透传裸码流WebRTC 的视频统计(分辨率/帧率/
// 解码格式)均为空。这里只要解码器处于活动状态就读取原生解码统计,用解码器
// 上报的真实分辨率/解码帧率覆盖 WebRTC 的空值,并把解码耗时一并显示,
// 确保「分辨率」与「解码耗时」这些信息显示在一起。
// —— 分模式统计解码耗时 ——
String? decodeInfo;
// 基础行要展示的解码耗时WebRTC 模式用 inbound-rtp 估算,自编码模式用原生平均。
double nativeAvg = 0;
if (_currentStreamMode == streamModeSelfCodec) {
// 【自编码模式】:视频走 DataChannel 透传裸码流WebRTC 的视频统计为空。
// 使用原生解码器上报的真实分辨率、帧率与耗时。
if (_selfCodecDecoder != null) {
final ds = await _selfCodecDecoder!.getStats();
if (ds != null) {
@@ -637,23 +657,43 @@ class WebRtcController {
'解码帧率: ${decodeFps.toStringAsFixed(0)} fps 已解码: $cnt';
}
}
// 无活动解码器时清零计数,避免重连后帧率计算异常
if (_selfCodecDecoder == null) _prevDecodeCount = 0;
// 自编码模式下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;
}
}
// WebRTC 全托管模式flutter_webrtc 1.5.2 不报 totalDecodeTime / framesDecoded
// 改为由原生 VideoSink 探针逐帧实测真实解码耗时last/avg/peak与帧率。
// 探针在远端视频轨道额外挂一个 VideoSink记录每帧真实到达时间算出帧间
// 耗时(≈ 真实解码+渲染周期),是原生层实测值,不会恒为 0。
if (_selfCodecDecoder == null) {
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 =
'解码耗时: ${p.lastMs.toStringAsFixed(1)} ms 平均: ${p.avgMs.toStringAsFixed(1)} ms 峰值: ${p.peakMs.toStringAsFixed(1)} ms\n'
'解码耗时 (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 = '-';
if (decodeInfo != null && nativeAvg > 0) {
@@ -761,6 +801,8 @@ class WebRtcController {
_prevBytesSent = 0;
_prevStatsTimestamp = 0;
_prevDecodeCount = 0;
_prevTotalDecodeTime = 0.0;
_prevFramesDecoded = 0;
_webRtcDecodeProbe = null;
_selfCodecWidth = 0;
_selfCodecHeight = 0;