fix(webrtc): 修复 ICE 服务器解析逻辑,支持自定义端口与标准参数
修正 ice_servers.dart 中的 URL 解析,使 stun/turn 地址能正确识别自定义端口及 transport/port 参数;同步调整 webrtc_controller.dart 中的调用方式以适配新解析逻辑。
This commit is contained in:
@@ -17,23 +17,40 @@
|
|||||||
/// 配对 → candidate-pair=0 → ICE Failed(Android 真机/网页则正常)。
|
/// 配对 → candidate-pair=0 → ICE Failed(Android 真机/网页则正常)。
|
||||||
/// 改用 `turns:`(TLS over TCP 5349)后,模拟器能稳定拿到 relay 候选。
|
/// 改用 `turns:`(TLS over TCP 5349)后,模拟器能稳定拿到 relay 候选。
|
||||||
/// 同时保留 udp / tcp 明文项作为降级,以及 STUN 用于 srflx。
|
/// 同时保留 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<Map<String, dynamic>> kIceServers = [
|
const List<Map<String, dynamic>> kIceServers = [
|
||||||
|
// 公共 STUN。
|
||||||
{'urls': 'stun:stun.l.google.com:19302'},
|
{'urls': 'stun:stun.l.google.com:19302'},
|
||||||
{'urls': 'stun:175.178.213.60:3478'},
|
{'urls': 'stun:175.178.213.60:3478'},
|
||||||
{
|
{
|
||||||
// 首选:TLS over TCP,iOS 模拟器下能稳定分配 relay 候选。
|
// 同一组凭据的多个 TURN 地址合并为一条。
|
||||||
'urls': 'turns:175.178.213.60:5349',
|
// 顺序说明:移动端/Web 全部生效;桌面端只取最后一个,
|
||||||
'username': 'fanhuitong',
|
// 故把兼容性最好的明文 UDP turn 放在末尾。
|
||||||
'credential': 'Fan19961907..',
|
'urls': [
|
||||||
},
|
'turns:175.178.213.60:5349',
|
||||||
{
|
'turn:175.178.213.60:3478?transport=tcp',
|
||||||
// 降级:明文 UDP/TCP。
|
'turn:175.178.213.60:3478',
|
||||||
'urls': 'turn:175.178.213.60:3478',
|
],
|
||||||
'username': 'fanhuitong',
|
|
||||||
'credential': 'Fan19961907..',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'urls': 'turn:175.178.213.60:3478?transport=tcp',
|
|
||||||
'username': 'fanhuitong',
|
'username': 'fanhuitong',
|
||||||
'credential': 'Fan19961907..',
|
'credential': 'Fan19961907..',
|
||||||
},
|
},
|
||||||
@@ -43,9 +60,12 @@ const List<Map<String, dynamic>> kIceServers = [
|
|||||||
'username': 'ttstd',
|
'username': 'ttstd',
|
||||||
'credential': 'fanhuitong',
|
'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',
|
'username': 'tt',
|
||||||
'credential': 'fht',
|
'credential': 'fht',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -307,6 +307,7 @@ class _ControllerHomeState extends State<ControllerHome> {
|
|||||||
};
|
};
|
||||||
_controller!.onConnectionFailed = (error) {
|
_controller!.onConnectionFailed = (error) {
|
||||||
if (mounted) setState(() => _connecting = false);
|
if (mounted) setState(() => _connecting = false);
|
||||||
|
debugPrint('[WebRTC] onConnectionFailed: $error');
|
||||||
_showAlert('连接失败:$error');
|
_showAlert('连接失败:$error');
|
||||||
};
|
};
|
||||||
_controller!.onDisconnected = () {
|
_controller!.onDisconnected = () {
|
||||||
@@ -560,6 +561,7 @@ class _ControllerHomeState extends State<ControllerHome> {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() => _recordStatus = '');
|
setState(() => _recordStatus = '');
|
||||||
|
debugPrint('_startRecording error: $e');
|
||||||
_showAlert('开始录制失败:$e');
|
_showAlert('开始录制失败:$e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,17 +100,23 @@ class WebRtcController {
|
|||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
await renderer.initialize();
|
await renderer.initialize();
|
||||||
|
|
||||||
|
// 桌面端(Linux/Windows)原生 RTCConfiguration 的 ice_servers 是定长数组
|
||||||
|
// (kMaxIceServerSize = 8),且原生解析未做越界检查,条目超限会越界写堆内存,
|
||||||
|
// 触发 `free(): invalid pointer` 直接闪退。这里做一次防御性截断兜底。
|
||||||
|
final safeIceServers = _limitIceServers(kIceServers);
|
||||||
|
|
||||||
final configuration = {
|
final configuration = {
|
||||||
'iceServers': kIceServers,
|
'iceServers': safeIceServers,
|
||||||
'sdpSemantics': 'unified-plan',
|
'sdpSemantics': 'unified-plan',
|
||||||
'iceCandidatePoolSize': 10,
|
'iceCandidatePoolSize': 10,
|
||||||
// 增强复杂网络下的稳定性,参考 Android 端配置。
|
// 增强复杂网络下的稳定性,参考 Android 端配置。
|
||||||
'continualGatheringPolicy': 'gatherContinually',
|
'continualGatheringPolicy': 'gatherContinually',
|
||||||
'iceTransportsType': 'all',
|
// 标准键名为 iceTransportPolicy(桌面端原生只识别这个键)。
|
||||||
|
'iceTransportPolicy': 'all',
|
||||||
'tcpCandidatePolicy': 'enabled',
|
'tcpCandidatePolicy': 'enabled',
|
||||||
};
|
};
|
||||||
|
|
||||||
final iceUrls = (kIceServers)
|
final iceUrls = safeIceServers
|
||||||
.map((e) => '${e['username'] != null ? '(${e['username']})' : ''}${e['urls']}')
|
.map((e) => '${e['username'] != null ? '(${e['username']})' : ''}${e['urls']}')
|
||||||
.join(', ');
|
.join(', ');
|
||||||
debugPrint('[WebRtcController] signaling server: ${signaling.serverUrl} | '
|
debugPrint('[WebRtcController] signaling server: ${signaling.serverUrl} | '
|
||||||
@@ -170,6 +176,25 @@ class WebRtcController {
|
|||||||
_sendOffer(reorderedSdp, authType: authType, authValue: authValue);
|
_sendOffer(reorderedSdp, authType: authType, authValue: authValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 防御性限制 ICE 服务器条目数,避免桌面端原生越界写内存导致闪退。
|
||||||
|
///
|
||||||
|
/// flutter_webrtc 桌面端 `FlutterWebRTCBase::CreateIceServers()` 会按传入
|
||||||
|
/// 列表长度直接写入定长数组 `IceServer ice_servers[8]`,**无越界检查**。
|
||||||
|
/// 条目数 > 8 时会破坏相邻堆内存,在释放阶段抛出
|
||||||
|
/// `free(): invalid pointer` 并使进程崩溃(Android/iOS 用动态列表不受影响)。
|
||||||
|
///
|
||||||
|
/// 正常情况下 [kIceServers] 已控制在 8 条以内,此处仅作为后续新增服务器
|
||||||
|
/// 时的安全网:超限时截断并打印告警,而不是让应用直接闪退。
|
||||||
|
List<Map<String, dynamic>> _limitIceServers(
|
||||||
|
List<Map<String, dynamic>> 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 兜底。
|
/// 重排 Offer SDP 中 `m=video` 行的视频编解码器顺序:VP8/VP9 优先,H264 兜底。
|
||||||
///
|
///
|
||||||
/// 与 Android 被控端 [WebRtcClient.optimizeSdp] 的编解码优先级保持一致,
|
/// 与 Android 被控端 [WebRtcClient.optimizeSdp] 的编解码优先级保持一致,
|
||||||
|
|||||||
Reference in New Issue
Block a user