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>" }
浏览器请求预览 →设备端收到后创建 PeerConnection
→ 加视频/音频/DataChannel 轨
→ 发 offer + ICE candidate
2.设备 → 对端 candidate
{"type": "candidate", "id": "<peer_id>", "candidate": "candidate:…", "mid": "…" }
candidate 消息相当于告诉对方可以用来传媒体的地址,供ICE候选试连通用,不传视频,只交换地址信息。浏览器本端收集到地址后同样 ws.send,两边都要把自己的 IP:端口告诉对方,ICE 才能试通。只有一边发 candidate,往往对不上或很难连通。
3.设备 → 对端 offer
设备发出的媒体协同:告诉浏览器,我要怎么推流、用什么编码、ICE/DTLS 参数是什么。
浏览器收到后才能:
setRemoteDescription(offer)
据此生成 answer 回给设备
双方再交换 candidate,走 ICE连接
没有 offer,后面的answer建联都不会开始。
4.对端 → 设备answer
收到浏览器 answer → setRemoteDescription → 完成协商
这是浏览器对设备 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
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/weixin_44651073/article/details/167492108



