
webrtc本地服务器hub的搭建bool start(uint16_t port) { if (port 0) { return false; } stop(); try { rtc::WebSocketServerConfiguration scfg; scfg.port port; scfg.bindAddress 0.0.0.0; auto server std::make_sharedrtc::WebSocketServer(std::move(scfg)); server-onClient([](std::shared_ptrrtc::WebSocket client) { if (client) { bind_client(std::move(client)); } }); { std::lock_guardstd::mutex lk(g_mtx); g_server std::move(server); } return true; } catch (const std::exception e) { stop(); return false; } }hub:start函数里面做了什么1. 参数检查port 0 直接返回失败调 stop()避免重复 listen2. 起一个 WebSocket 服务端 并 listenbindAddress 0.0.0.0 port 传入的8888端口 → new rtc::WebSocketServer → 存到 g_server此后本机及局域网可以连 ws://设备IP:port/......3. 有连接时的注册回调server-onClient([](client) { bind_client(client); });start 不会自己去 open也不转发消息只是挂好钩子函数。真正有客户端连上并握手成功后bind_client 才会执行bind_client给每一个刚连上 Hub 的客户端挂回调握手成功onOpen后再登记、收消息。static void bind_client(std::shared_ptrrtc::WebSocket ws) { ws-onOpen([ws]() { const std::string path_str ws-path().value_or(); const std::string id peer_id_from_path(path_str); { std::lock_guardstd::mutex lk(g_mtx); g_clients[id] ws; } ws-onMessage([id, ws](rtc::message_variant data) { if (!std::holds_alternativertc::string(data)) { return; } on_client_message(id, std::getrtc::string(data)); }); ws-onClosed([id, ws]() { unregister_client(id, ws); }); ws-onError([id](std::string err) { }); }); }从 URL 路径取出 id如 /server → server放进 g_clients[id]在 onMessage 里做 JSON 按 id 转发在指定端口8888开 WS 信令服务器并等连接登记客户端、改 id、转发都在后续 onClient / bind_client 里发生。Hub相当于本机信令中转站只帮设备和浏览器互相递 WebRTC 信令 JSON不传视频。具体干什么listen 一个 WebSocket 端口默认 8888登记谁连上来路径 → id如 /server本机、/alice浏览器id转发消息看 JSON 里的 id 寄给谁再把 id 改成发送方名字设备和浏览器都连上 Hub 之后才能交换 request / offer / answer / candidate。-------------------------------------------------------static void on_client_message(const std::string from_id, const std::string utf8) { json msg; try { msg json::parse(utf8); } catch (const std::exception e) { return; } if (!msg.contains(id) || !msg[id].is_string()) { return; } const std::string dest_id msg[id].getstd::string(); msg[id] from_id; const std::string out msg.dump(); std::shared_ptrrtc::WebSocket dest; { std::lock_guardstd::mutex lk(g_mtx); auto it g_clients.find(dest_id); if (it g_clients.end() || !it-second) { return; } dest it-second; } try { if (!dest-isOpen()) { return; } dest-send(out); } catch (const std::exception e) { } } g_clients结构如下图 g_clients │ │ key (string) value (shared_ptr) │ ───────────────── ───────────────────────────── ├─ alice ───────────► shared_ptr ──► rtc::WebSocket (浏览器A) ├─ bob ───────────► shared_ptr ──► rtc::WebSocket (浏览器B) └─ server ───────────► shared_ptr ──► rtc::WebSocket (设备ldc) map 本体 节点1 节点2 节点3 ┌──────────┐ ┌──────────┐ ┌──────────┐ │ key:alice│ │ key:bob │ │key:server│ │ value:●──┼──┐ │ value:●──┼──┐ │ value:●──┼──┐ └──────────┘ │ └──────────┘ │ └──────────┘ │ ▼ ▼ ▼ WebSocket WebSocket WebSocket (alice那条) (bob那条) (server那条) 代码同样是: auto it g_clients.find(nobody); 第1步: it → [0] bob ! nobody 第2步: it → [1] alice ! nobody 第3步: it → [2] server ! nobody 第4步: it → [end] map结构看完了没有 [0] bob [1] alice [2] server [end] ◄── it 停在这里 代码: if (it g_clients.end() ...) → 条件真return 后面的 dest ...、send 都不会执行改完后发给设备侧登记名为 server 的那条连接流程是浏览器发出{ id: server, type: request }Hub 改成{ id: alice, type: request }Hub 按原来的收件人 dest_id server执行g_clients[server]-send(...)g_clients[server] 就是设备 ldc 连上的那条ws://127.0.0.1:8888/server。消息到 ldc 的 onMessage → handle_signaling_message用 alice 建这一路 PeerConnection后面实现浏览器和设备webrtc的信令交互。//**********************************************************************////**********************************************************************//浏览器和设备webrtc的信令交互流程handle_signaling_message这是线程里真正处理业务1.对端 → 设备 request{ type: request, id: peer_id }[16:30:30.862] [webrtc_ldc] signaling RX?xml version1.0 encodingUTF-8?signalingidB2nPJBgiG8/idtyperequest/type/signaling浏览器请求预览 →设备端收到后创建 PeerConnection→ 加视频/音频/DataChannel 轨→ 发 offer ICE candidate2.设备 → 对端 candidate{type: candidate, id: peer_id, candidate: candidate:…, mid: … }candidate 消息相当于告诉对方可以用来传媒体的地址供ICE候选试连通用不传视频只交换地址信息。浏览器本端收集到地址后同样 ws.send两边都要把自己的 IP:端口告诉对方ICE 才能试通。只有一边发 candidate往往对不上或很难连通。[16:30:30.862] [webrtc_ldc] request from peer idB2nPJBgiG8 (enable_videotrue enable_datachanneltrue accept_inbound_dctrue)[16:30:30.898] [webrtc_ldc] signaling TX typecandidate idB2nPJBgiG8 (no sdp field)[16:30:30.900] [webrtc_ldc] PC state1 peerB2nPJBgiG8[16:30:30.959] [webrtc_ldc] signaling TX typecandidate idB2nPJBgiG8 (no sdp field)3.设备 → 对端 offer设备发出的媒体协同告诉浏览器我要怎么推流、用什么编码、ICE/DTLS 参数是什么。浏览器收到后才能setRemoteDescription(offer)据此生成 answer 回给设备双方再交换 candidate走 ICE连接没有 offer后面的answer建联都不会开始。[16:30:30.964] [webrtc_ldc] signaling TX typeoffer idB2nPJBgiG8 sdp_chars1136[16:30:30.964] [webrtc_ldc] sent local SDP typeoffer4.对端 → 设备answer收到浏览器 answer → setRemoteDescription → 完成协商[16:30:30.979] [webrtc_ldc] signaling RX?xml version1.0 encodingUTF-8?signalingidB2nPJBgiG8/idtypeanswer/typesdp![CDATA[v0o- 1432640720720693603 2 IN IP4 127.0.0.1s-t0 0agroup:BUNDLE cam-video 0amsid-semantic: WMSmvideo 9 UDP/TLS/RTP/SAVPF 102cIN IP4 0.0.0.0artcp:9 IN IP4 0.0.0.0acandidate:2719503686 1 udp 2113937151 67ae8833-aee7-43b7-87c1-8bab0d36e5ff.local 63708 typ host generation 0 network-cost 999aice-ufrag:kGflaice-pwd:Zg0oVICjNPQvWJfZKQSJ4iFHaice-options:trickleafingerprint:sha-256 3D:BF:01:6E:3B:CB:4F:26:CE:52:C1:77:7B:58:27:3B:6E:72:A1:7B:2E:72:C0:11:F9:83:28:BC:7B:03:59:81asetup:activeamid:cam-videoaextmap:4 urn:ietf:params:rtp-hdrext:sdes:midarecvonlyartcp-muxartpmap:102 H264/90000artcp-fb:102 goog-rembartcp-fb:102 nackartcp-fb:102 nack pliafmtp:102 level-asymmetry-allowed1;packetization-mode1;profile-level-id42e01fmapplication 9 UDP/DTLS/SCTP webrtc-datachannelcIN IP4 0.0.0.0aice-ufrag:kGflaice-pwd:Zg0oVICjNPQvWJfZKQSJ4iFHaice-options:trickleafingerprint:sha-256 3D:BF:01:6E:3B:CB:4F:26:CE:52:C1:77:7B:58:27:3B:6E:72:A1:7B:2E:72:C0:11:F9:83:28:BC:7B:03:59:81asetup:activeamid:0asctp-port:5000amax-message-size:262144]]/sdp/signaling[16:30:30.979] [webrtc_ldc] answer sdp first line: v0[16:30:30.980] [webrtc_ldc] RTP MID header ext_id4 midcam-video uriurn:ietf:params:rtp-hdrext:sdes:mid[16:30:30.982] [webrtc_ldc] setRemoteDescription(answer)这是浏览器对设备 offer 的应答同意怎么收流、本端 ICE/DTLS 参数是什么。设备收到后会 setRemoteDescription(answer)协商才算双边对齐。candidate收到远端 ICE candidate → addRemoteCandidate浏览器(alice) Hub 设备 ldc │ │ │ │── request ──────────►│ 改 id→alice ───────────►│ 建 PC / 出 offer │ │ │ │◄─ offer ─────────────│◄─ offer (idalice) ─────│ │ │ │ │───────answer ───────►│ 改 id→alice ───────────►│ setRemoteDescription │ │ │ │◄─ candidate ─────────│◄─ candidate ────────────│ (交叉多次) │── candidate ────────►│────────────────────────►│ │ │ │ │ ICE/DTLS不经 Hub→ 出画 │ │ │ │ │── bye ──────────────►│────────────────────────►│ remove peer信令交互日志[16:29:16.588] [webrtc_cam_track] stopped [16:29:16.588] [webrtc_cam_track] venc callback registered ch0 [16:29:16.589] [webrtc] libdatachannel session started [16:29:16.589] [service] webrtc started [16:29:16.589] camera init done. [16:29:16.657] [webrtc_ldc] opening ws://192.168.6.233:8000/server [16:29:16.683] [webrtc_ldc] signaling WebSocket open /tmp/tmp # /tmp/tmp # /tmp/tmp # I-Frame insertion must occur in less than 100 ms to be NDI|HX compliant [16:30:30.862] [webrtc_ldc] signaling RX ?xml version1.0 encodingUTF-8? signaling idB2nPJBgiG8/id typerequest/type /signaling [16:30:30.862] [webrtc_ldc] request from peer idB2nPJBgiG8 (enable_videotrue enable_datachanneltrue accept_inbound_dctrue) [16:30:30.898] [webrtc_ldc] signaling TX typecandidate idB2nPJBgiG8 (no sdp field) [16:30:30.900] [webrtc_ldc] PC state1 peerB2nPJBgiG8 [16:30:30.959] [webrtc_ldc] signaling TX typecandidate idB2nPJBgiG8 (no sdp field) 2026-05-14 16:30:30.961 WARN [2135] [rtc::impl::IceTransport::LogCallback390] juice: Send failed, errno101 [16:30:30.964] [webrtc_ldc] signaling TX typeoffer idB2nPJBgiG8 sdp_chars1136 [16:30:30.964] [webrtc_ldc] sent local SDP typeoffer [16:30:30.979] [webrtc_ldc] signaling RX ?xml version1.0 encodingUTF-8? signaling idB2nPJBgiG8/id typeanswer/type sdp![CDATA[v0 o- 1432640720720693603 2 IN IP4 127.0.0.1 s- t0 0 agroup:BUNDLE cam-video 0 amsid-semantic: WMS mvideo 9 UDP/TLS/RTP/SAVPF 102 cIN IP4 0.0.0.0 artcp:9 IN IP4 0.0.0.0 acandidate:2719503686 1 udp 2113937151 67ae8833-aee7-43b7-87c1-8bab0d36e5ff.local 63708 typ host generation 0 network-cost 999 aice-ufrag:kGfl aice-pwd:Zg0oVICjNPQvWJfZKQSJ4iFH aice-options:trickle afingerprint:sha-256 3D:BF:01:6E:3B:CB:4F:26:CE:52:C1:77:7B:58:27:3B:6E:72:A1:7B:2E:72:C0:11:F9:83:28:BC:7B:03:59:81 asetup:active amid:cam-video aextmap:4 urn:ietf:params:rtp-hdrext:sdes:mid arecvonly artcp-mux artpmap:102 H264/90000 artcp-fb:102 goog-remb artcp-fb:102 nack artcp-fb:102 nack pli afmtp:102 level-asymmetry-allowed1;packetization-mode1;profile-level-id42e01f mapplication 9 UDP/DTLS/SCTP webrtc-datachannel cIN IP4 0.0.0.0 aice-ufrag:kGfl aice-pwd:Zg0oVICjNPQvWJfZKQSJ4iFH aice-options:trickle afingerprint:sha-256 3D:BF:01:6E:3B:CB:4F:26:CE:52:C1:77:7B:58:27:3B:6E:72:A1:7B:2E:72:C0:11:F9:83:28:BC:7B:03:59:81 asetup:active amid:0 asctp-port:5000 amax-message-size:262144 ]]/sdp /signaling [16:30:30.979] [webrtc_ldc] answer sdp first line: v0 [16:30:30.980] [webrtc_ldc] RTP MID header ext_id4 midcam-video uriurn:ietf:params:rtp-hdrext:sdes:mid [16:30:30.982] [webrtc_ldc] setRemoteDescription(answer) [16:30:31.030] [webrtc_ldc] video track open (requested IDR for decoder) [16:30:31.032] [webrtc_ldc] PC state2 peerB2nPJBgiG8 [16:30:31.034] [webrtc_ldc] DataChannel open taglocal labelapp I-Frame insertion must occur in less than 100 ms to be NDI|HX compliant