From 7bb44b10be0061a634ee1efaa4458a180dd4a81d Mon Sep 17 00:00:00 2001 From: TongTongStudio Date: Sun, 2 Aug 2026 11:47:10 +0800 Subject: [PATCH] =?UTF-8?q?fix(webrtc):=20=E4=BF=AE=E5=A4=8D=20ICE=20?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E5=99=A8=E8=A7=A3=E6=9E=90=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E6=94=AF=E6=8C=81=E8=87=AA=E5=AE=9A=E4=B9=89=E7=AB=AF?= =?UTF-8?q?=E5=8F=A3=E4=B8=8E=E6=A0=87=E5=87=86=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修正 ice_servers.dart 中的 URL 解析,使 stun/turn 地址能正确识别自定义端口及 transport/port 参数;同步调整 webrtc_controller.dart 中的调用方式以适配新解析逻辑。 --- .../lib/config/ice_servers.dart | 50 +++++++++++++------ webrtc_controller_flutter/lib/main.dart | 2 + .../lib/webrtc/webrtc_controller.dart | 31 ++++++++++-- 3 files changed, 65 insertions(+), 18 deletions(-) diff --git a/webrtc_controller_flutter/lib/config/ice_servers.dart b/webrtc_controller_flutter/lib/config/ice_servers.dart index 92c9f4c..b61daf3 100644 --- a/webrtc_controller_flutter/lib/config/ice_servers.dart +++ b/webrtc_controller_flutter/lib/config/ice_servers.dart @@ -17,23 +17,40 @@ /// 配对 → candidate-pair=0 → ICE Failed(Android 真机/网页则正常)。 /// 改用 `turns:`(TLS over TCP 5349)后,模拟器能稳定拿到 relay 候选。 /// 同时保留 udp / tcp 明文项作为降级,以及 STUN 用于 srflx。 +/// +/// **Linux / Windows 桌面端崩溃修复(free(): invalid pointer)**: +/// flutter_webrtc 桌面端的原生 `RTCConfiguration` 使用**定长数组**存放 +/// ICE 服务器:`IceServer ice_servers[kMaxIceServerSize]`,其中 +/// `kMaxIceServerSize = 8`(见 third_party/libwebrtc/include/rtc_types.h)。 +/// 而 `FlutterWebRTCBase::CreateIceServers()` 直接按传入列表长度写入该数组, +/// **没有做任何越界检查**。一旦条目数 > 8,就会越界写入相邻堆内存, +/// 并向非法地址赋值 `std::string`,最终在释放时触发 +/// `free(): invalid pointer` 并导致进程闪退。 +/// +/// 因此下表条目数**必须 ≤ [kMaxIceServerCount](8)**。 +/// 这里按「同一组凭据合并为一条」的标准写法(`urls` 传数组), +/// 在不丢失任何服务器地址的前提下把条目压缩到 6 条。 +/// +/// **桌面端 `urls` 数组的额外注意点**: +/// 同一函数里解析 `urls` 列表时是 `ice_server.uri = ...` 逐个**覆盖**赋值, +/// 只有**最后一个** URL 会真正生效(Android / iOS / Web 则会全部生效)。 +/// 因此每个数组都把「最通用、最可能连通」的地址放在**最后一位**: +/// TURN 组以明文 `turn:...:3478`(UDP)收尾,内网组以 `turn:` 收尾。 +const int kMaxIceServerCount = 8; + const List> kIceServers = [ + // 公共 STUN。 {'urls': 'stun:stun.l.google.com:19302'}, {'urls': 'stun:175.178.213.60:3478'}, { - // 首选:TLS over TCP,iOS 模拟器下能稳定分配 relay 候选。 - 'urls': 'turns:175.178.213.60:5349', - 'username': 'fanhuitong', - 'credential': 'Fan19961907..', - }, - { - // 降级:明文 UDP/TCP。 - 'urls': 'turn:175.178.213.60:3478', - 'username': 'fanhuitong', - 'credential': 'Fan19961907..', - }, - { - 'urls': 'turn:175.178.213.60:3478?transport=tcp', + // 同一组凭据的多个 TURN 地址合并为一条。 + // 顺序说明:移动端/Web 全部生效;桌面端只取最后一个, + // 故把兼容性最好的明文 UDP turn 放在末尾。 + 'urls': [ + 'turns:175.178.213.60:5349', + 'turn:175.178.213.60:3478?transport=tcp', + 'turn:175.178.213.60:3478', + ], 'username': 'fanhuitong', 'credential': 'Fan19961907..', }, @@ -43,9 +60,12 @@ const List> kIceServers = [ 'username': 'ttstd', 'credential': 'fanhuitong', }, - {'urls': 'stun:192.168.5.224:3478'}, { - 'urls': 'turn:192.168.5.224:3478', + // 内网 STUN/TURN(同机房/局域网时可直连)。 + 'urls': [ + 'stun:192.168.5.224:3478', + 'turn:192.168.5.224:3478', + ], 'username': 'tt', 'credential': 'fht', }, diff --git a/webrtc_controller_flutter/lib/main.dart b/webrtc_controller_flutter/lib/main.dart index 1f0ccf5..36911b8 100644 --- a/webrtc_controller_flutter/lib/main.dart +++ b/webrtc_controller_flutter/lib/main.dart @@ -307,6 +307,7 @@ class _ControllerHomeState extends State { }; _controller!.onConnectionFailed = (error) { if (mounted) setState(() => _connecting = false); + debugPrint('[WebRTC] onConnectionFailed: $error'); _showAlert('连接失败:$error'); }; _controller!.onDisconnected = () { @@ -560,6 +561,7 @@ class _ControllerHomeState extends State { } catch (e) { if (mounted) { setState(() => _recordStatus = ''); + debugPrint('_startRecording error: $e'); _showAlert('开始录制失败:$e'); } } diff --git a/webrtc_controller_flutter/lib/webrtc/webrtc_controller.dart b/webrtc_controller_flutter/lib/webrtc/webrtc_controller.dart index 0d16ee4..4b059b5 100644 --- a/webrtc_controller_flutter/lib/webrtc/webrtc_controller.dart +++ b/webrtc_controller_flutter/lib/webrtc/webrtc_controller.dart @@ -100,17 +100,23 @@ class WebRtcController { Future initialize() async { await renderer.initialize(); + // 桌面端(Linux/Windows)原生 RTCConfiguration 的 ice_servers 是定长数组 + // (kMaxIceServerSize = 8),且原生解析未做越界检查,条目超限会越界写堆内存, + // 触发 `free(): invalid pointer` 直接闪退。这里做一次防御性截断兜底。 + final safeIceServers = _limitIceServers(kIceServers); + final configuration = { - 'iceServers': kIceServers, + 'iceServers': safeIceServers, 'sdpSemantics': 'unified-plan', 'iceCandidatePoolSize': 10, // 增强复杂网络下的稳定性,参考 Android 端配置。 'continualGatheringPolicy': 'gatherContinually', - 'iceTransportsType': 'all', + // 标准键名为 iceTransportPolicy(桌面端原生只识别这个键)。 + 'iceTransportPolicy': 'all', 'tcpCandidatePolicy': 'enabled', }; - final iceUrls = (kIceServers) + final iceUrls = safeIceServers .map((e) => '${e['username'] != null ? '(${e['username']})' : ''}${e['urls']}') .join(', '); debugPrint('[WebRtcController] signaling server: ${signaling.serverUrl} | ' @@ -170,6 +176,25 @@ class WebRtcController { _sendOffer(reorderedSdp, authType: authType, authValue: authValue); } + /// 防御性限制 ICE 服务器条目数,避免桌面端原生越界写内存导致闪退。 + /// + /// flutter_webrtc 桌面端 `FlutterWebRTCBase::CreateIceServers()` 会按传入 + /// 列表长度直接写入定长数组 `IceServer ice_servers[8]`,**无越界检查**。 + /// 条目数 > 8 时会破坏相邻堆内存,在释放阶段抛出 + /// `free(): invalid pointer` 并使进程崩溃(Android/iOS 用动态列表不受影响)。 + /// + /// 正常情况下 [kIceServers] 已控制在 8 条以内,此处仅作为后续新增服务器 + /// 时的安全网:超限时截断并打印告警,而不是让应用直接闪退。 + List> _limitIceServers( + List> servers, + ) { + if (servers.length <= kMaxIceServerCount) return servers; + debugPrint('[WebRtcController] 警告: ICE 服务器数量 ${servers.length} ' + '超过原生上限 $kMaxIceServerCount,已截断以避免桌面端崩溃。' + '请在 lib/config/ice_servers.dart 中合并同凭据的地址。'); + return servers.sublist(0, kMaxIceServerCount); + } + /// 重排 Offer SDP 中 `m=video` 行的视频编解码器顺序:VP8/VP9 优先,H264 兜底。 /// /// 与 Android 被控端 [WebRtcClient.optimizeSdp] 的编解码优先级保持一致,