diff --git a/webrtc_controller_flutter/android/app/build.gradle.kts b/webrtc_controller_flutter/android/app/build.gradle.kts index 51dd372..22c4eec 100644 --- a/webrtc_controller_flutter/android/app/build.gradle.kts +++ b/webrtc_controller_flutter/android/app/build.gradle.kts @@ -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") +} diff --git a/webrtc_controller_flutter/android/app/src/main/kotlin/com/ttstd/fluttercontroller/SelfCodecDecoderPlugin.kt b/webrtc_controller_flutter/android/app/src/main/kotlin/com/ttstd/fluttercontroller/SelfCodecDecoderPlugin.kt index 19b2ffb..23c2015 100644 --- a/webrtc_controller_flutter/android/app/src/main/kotlin/com/ttstd/fluttercontroller/SelfCodecDecoderPlugin.kt +++ b/webrtc_controller_flutter/android/app/src/main/kotlin/com/ttstd/fluttercontroller/SelfCodecDecoderPlugin.kt @@ -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) diff --git a/webrtc_controller_flutter/lib/webrtc/webrtc_controller.dart b/webrtc_controller_flutter/lib/webrtc/webrtc_controller.dart index 04ae9f9..39b844a 100644 --- a/webrtc_controller_flutter/lib/webrtc/webrtc_controller.dart +++ b/webrtc_controller_flutter/lib/webrtc/webrtc_controller.dart @@ -235,6 +235,7 @@ class WebRtcController { MethodChannel('webrtc_decode_probe'); Future _attachWebRtcDecodeProbe(String trackId) async { + debugPrint('[WebRtcController] Attaching decode probe for track: $trackId'); try { await _decodeProbeChannel.invokeMethod( '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;