☰
Webrtc本地服务器hub实现细节和信令交互
2026/10/22 7:21:41 网站建设 项目流程

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_shared<rtc::WebSocketServer>(std::move(scfg)); server->onClient([](std::shared_ptr<rtc::WebSocket> client) { if (client) { bind_client(std::move(client)); } }); { std::lock_guard<std::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(),避免重复 listen


2. 起一个 WebSocket 服务端 并 listen

bindAddress = 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_ptr<rtc::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_guard<std::mutex> lk(g_mtx); g_clients[id] = ws; } ws->onMessage([id, ws](rtc::message_variant data) { if (!std::holds_alternative<rtc::string>(data)) { return; } on_client_message(id, std::get<rtc::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"].get<std::string>(); msg["id"] = from_id; const std::string out = msg.dump(); std::shared_ptr<rtc::WebSocket> dest; { std::lock_guard<std::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 version="1.0" encoding="UTF-8"?>
<signaling>
<id>B2nPJBgiG8</id>
<type>request</type>
</signaling>

浏览器请求预览 →设备端收到后创建 PeerConnection
→ 加视频/音频/DataChannel 轨
→ 发 offer + ICE candidate

2.设备 → 对端 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 id=B2nPJBgiG8 (enable_video=true enable_datachannel=true accept_inbound_dc=true)
[16:30:30.898] [webrtc_ldc] signaling TX type=candidate id=B2nPJBgiG8 (no sdp field)
[16:30:30.900] [webrtc_ldc] PC state=1 peer=B2nPJBgiG8
[16:30:30.959] [webrtc_ldc] signaling TX type=candidate id=B2nPJBgiG8 (no sdp field)

3.设备 → 对端 offer

设备发出的媒体协同:告诉浏览器,我要怎么推流、用什么编码、ICE/DTLS 参数是什么。
浏览器收到后才能:
setRemoteDescription(offer)
据此生成 answer 回给设备
双方再交换 candidate,走 ICE连接
没有 offer,后面的answer建联都不会开始。

[16:30:30.964] [webrtc_ldc] signaling TX type=offer id=B2nPJBgiG8 sdp_chars=1136
[16:30:30.964] [webrtc_ldc] sent local SDP type=offer


4.对端 → 设备answer
收到浏览器 answer → setRemoteDescription → 完成协商

[16:30:30.979] [webrtc_ldc] signaling RX
<?xml version="1.0" encoding="UTF-8"?>
<signaling>
<id>B2nPJBgiG8</id>
<type>answer</type>
<sdp><![CDATA[v=0
o=- 1432640720720693603 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE cam-video 0
a=msid-semantic: WMS
m=video 9 UDP/TLS/RTP/SAVPF 102
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=candidate:2719503686 1 udp 2113937151 67ae8833-aee7-43b7-87c1-8bab0d36e5ff.local 63708 typ host generation 0 network-cost 999
a=ice-ufrag:kGfl
a=ice-pwd:Zg0oVICjNPQvWJfZKQSJ4iFH
a=ice-options:trickle
a=fingerprint: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
a=setup:active
a=mid:cam-video
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
a=recvonly
a=rtcp-mux
a=rtpmap:102 H264/90000
a=rtcp-fb:102 goog-remb
a=rtcp-fb:102 nack
a=rtcp-fb:102 nack pli
a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
m=application 9 UDP/DTLS/SCTP webrtc-datachannel
c=IN IP4 0.0.0.0
a=ice-ufrag:kGfl
a=ice-pwd:Zg0oVICjNPQvWJfZKQSJ4iFH
a=ice-options:trickle
a=fingerprint: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
a=setup:active
a=mid:0
a=sctp-port:5000
a=max-message-size:262144
]]></sdp>
</signaling>
[16:30:30.979] [webrtc_ldc] answer sdp first line: v=0
[16:30:30.980] [webrtc_ldc] RTP MID header ext_id=4 mid=cam-video uri=urn: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 (id=alice) ─────│ │ │ │ │───────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 ch=0 [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 version="1.0" encoding="UTF-8"?> <signaling> <id>B2nPJBgiG8</id> <type>request</type> </signaling> [16:30:30.862] [webrtc_ldc] request from peer id=B2nPJBgiG8 (enable_video=true enable_datachannel=true accept_inbound_dc=true) [16:30:30.898] [webrtc_ldc] signaling TX type=candidate id=B2nPJBgiG8 (no sdp field) [16:30:30.900] [webrtc_ldc] PC state=1 peer=B2nPJBgiG8 [16:30:30.959] [webrtc_ldc] signaling TX type=candidate id=B2nPJBgiG8 (no sdp field) 2026-05-14 16:30:30.961 WARN [2135] [rtc::impl::IceTransport::LogCallback@390] juice: Send failed, errno=101 [16:30:30.964] [webrtc_ldc] signaling TX type=offer id=B2nPJBgiG8 sdp_chars=1136 [16:30:30.964] [webrtc_ldc] sent local SDP type=offer [16:30:30.979] [webrtc_ldc] signaling RX <?xml version="1.0" encoding="UTF-8"?> <signaling> <id>B2nPJBgiG8</id> <type>answer</type> <sdp><![CDATA[v=0 o=- 1432640720720693603 2 IN IP4 127.0.0.1 s=- t=0 0 a=group:BUNDLE cam-video 0 a=msid-semantic: WMS m=video 9 UDP/TLS/RTP/SAVPF 102 c=IN IP4 0.0.0.0 a=rtcp:9 IN IP4 0.0.0.0 a=candidate:2719503686 1 udp 2113937151 67ae8833-aee7-43b7-87c1-8bab0d36e5ff.local 63708 typ host generation 0 network-cost 999 a=ice-ufrag:kGfl a=ice-pwd:Zg0oVICjNPQvWJfZKQSJ4iFH a=ice-options:trickle a=fingerprint: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 a=setup:active a=mid:cam-video a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid a=recvonly a=rtcp-mux a=rtpmap:102 H264/90000 a=rtcp-fb:102 goog-remb a=rtcp-fb:102 nack a=rtcp-fb:102 nack pli a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f m=application 9 UDP/DTLS/SCTP webrtc-datachannel c=IN IP4 0.0.0.0 a=ice-ufrag:kGfl a=ice-pwd:Zg0oVICjNPQvWJfZKQSJ4iFH a=ice-options:trickle a=fingerprint: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 a=setup:active a=mid:0 a=sctp-port:5000 a=max-message-size:262144 ]]></sdp> </signaling> [16:30:30.979] [webrtc_ldc] answer sdp first line: v=0 [16:30:30.980] [webrtc_ldc] RTP MID header ext_id=4 mid=cam-video uri=urn: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 state=2 peer=B2nPJBgiG8 [16:30:31.034] [webrtc_ldc] DataChannel open tag=local label=app I-Frame insertion must occur in less than 100 ms to be NDI|HX compliant

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询