feat(decode): 用 WebRTC stats 替代原生探针计算解码耗时
This commit is contained in:
@@ -44,3 +44,10 @@ kotlin {
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// flutter_webrtc 依赖了 io.github.webrtc-sdk(org.webrtc.*),但它声明为
|
||||
// implementation,app 模块默认看不到 org.webrtc.VideoTrack/VideoSink 等类型。
|
||||
// 这里显式引入同版本,使解码耗时探针能直接引用 libwebrtc 的 VideoTrack/VideoSink。
|
||||
implementation("io.github.webrtc-sdk:android:144.7559.09")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user