WebRTCWebRTC音视频直播技术

实现简易webrtc 网关

2017-04-07  本文已影响1511人  little_wang

本文实现一个简易的单向webrtc网关,使用chrome浏览器浏览服务器上的h264视频文件。
代码地址 https://github.com/wangdxh/Desert-Eagle/tree/master/webrtcgateway
网关服务器使用c++开发,通过webrtc底层协议和chrome浏览器进行交互。

webrtc交互需要一些udp网络和ice协议基础,参考资料:
web性能权威指南 第3章UDP的构成和第18章webrtc
ice协议rfc5245

大致交互流程

webrtc使用sdp除了描述媒体类型,还有一些额外的字段来描述ice的连接候选项。

使用到的库

交互流程

客户端和服务器在同一个网段内,程序不支持指定stun服务器,主机单网卡,所以ice候选项只有一个主机类型的候选项。

sdp内容

server发给浏览器的offer sdp如下:

v=0
o=- 1495799811084970 1495799811084970 IN IP4 10.10.10.11
s=Streaming Test
t=0 0
a=group:BUNDLE video
a=msid-semantic: WMS janus
m=video 1 RTP/SAVPF 96
c=IN IP4 10.10.10.11
a=mid:video
a=sendonly
a=rtcp-mux
a=ice-ufrag:j9UX
a=ice-pwd:J5bBevBdPbtH5oYhxy0cMJ
a=ice-options:trickle
a=fingerprint:sha-256 23:83:58:1C:2C:BB:E3:A2:2C:19:00:0C:AD:CD:99:EF:28:F7:F6:A8:99:3E:FF:97:48:C4:BF:DA:1D:71:83:8B
a=setup:actpass
a=connection:new
a=rtpmap:96 H264/90000
a=ssrc:12345678 cname:janusvideo
a=ssrc:12345678 msid:janus janusv0
a=ssrc:12345678 mslabel:janus
a=ssrc:12345678 label:janusv0
a=candidate:1 1 udp 2013266431 10.10.10.11 55194 typ host

sdp里面除了媒体描述的信息之外新增了几个选项:

浏览器返回的answer sdp和trickle 候选项如下:

v=0
o=- 8873346408483571164 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE video
a=msid-semantic: WMS
m=video 9 RTP/SAVPF 96
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:USWn
a=ice-pwd:A/BfLTfs/lGlOE1QMmNi+YPU
a=fingerprint:sha-256 15:71:96:BA:29:2F:BB:FF:1A:F6:3C:07:0B:9B:9C:2B:BF:37:7D:D8:D8:5B:36:9F:9F:57:08:31:82:43:88:D7
a=setup:active
a=mid:video
a=recvonly
a=rtcp-mux
a=rtpmap:96 H264/90000
a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
candidate:2153010912 1 udp 2113937151 10.10.10.11 55195 typ host generation 0 ufrag USWn network-cost 500063890C

更详细的信息请参考rfc5245,ice协议的rfc描述的非常清楚易懂。

网页发起请求

webrtchtml文件夹下面有个index.html 页面,点击call时

js代码在js文件夹下面的main.js内。

服务器端

websocket交互的部分就不描述了,代码比较简洁。

ice初始化

libnice依赖于glib,所以初始化glib即可,libnice不需要进行初始化

g_networking_init();
gloop = g_main_loop_new(NULL, FALSE);
gloopthread = g_thread_new("loop thread", &loop_thread, gloop);    

void* loop_thread(void *data)
{
    GMainLoop* ploop = (GMainLoop*)data;
    g_main_loop_run(ploop);
    return 0;
}
ice建立

服务器收到websocket请求之后,会创建一个niceagent对象,

agent = nice_agent_new(g_main_loop_get_context (gloop), NICE_COMPATIBILITY_RFC5245);
g_object_set(agent, "controlling-mode", controlling, NULL);        

NiceAddress addr_local;
nice_address_init (&addr_local);
nice_address_set_from_string (&addr_local, strhost.c_str());
nice_agent_add_local_address (agent, &addr_local);

g_signal_connect(agent, "candidate-gathering-done", G_CALLBACK(cb_candidate_gathering_done), this);
g_signal_connect(agent, "component-state-changed", G_CALLBACK(cb_component_state_changed), this);
g_signal_connect (agent, "new-selected-pair-full",G_CALLBACK (cb_new_selected_pair_full), this);
guint stream_id = nice_agent_add_stream(agent, componentnum);
nice_agent_attach_recv(agent, streamid, componetid, g_main_loop_get_context (gloop), cb_nice_recv, this);
nice_agent_gather_candidates(agent, streamid)
nice_agent_get_local_credentials(agent, stream_id,&local_ufrag, &local_password)
cands = nice_agent_get_local_candidates(agent, stream_id, 1);
NiceCandidate *c = (NiceCandidate *)g_slist_nth(cands, 0)->data;
nice_agent_set_remote_credentials(agent, 1, ufrag, pwd)
nice_agent_set_remote_candidates(agent, 1, 1, plist)

ice 通过回调cb_nice_recv收到对端的数据之后,要区分数据包是dtls包,还是rtp,rtcp的包,区分完成之后,将不同的数据包分流到对应的业务处理中。

bool is_dtls(char *buf) 
{
    return ((*buf >= 20) && (*buf <= 64));
}
bool is_rtp(char *buf) 
{
    rtp_header *header = (rtp_header *)buf;
    return ((header->type < 64) || (header->type >= 96));
}

bool is_rtcp(char *buf) 
{
    rtp_header *header = (rtp_header *)buf;
    return ((header->type >= 64) && (header->type < 96));
}

nice_开头的接口参考libnice api页面

dtls协商

SSL_new(ssl_ctx);
SSL_set_accept_state(ssl);
 SSL_do_handshake(ssl);
......
SSL_is_init_finished(ssl)
SSL_get_peer_certificate
......
unsigned char material[SRTP_MASTER_LENGTH*2];
unsigned char *local_key, *local_salt, *remote_key, *remote_salt;
SSL_export_keying_material(ssl, material, SRTP_MASTER_LENGTH*2, "EXTRACTOR-dtls_srtp", 19, NULL, 0, 0)
remote_key = material;
local_key = remote_key + SRTP_MASTER_KEY_LENGTH;
remote_salt = local_key + SRTP_MASTER_KEY_LENGTH;
local_salt = remote_salt + SRTP_MASTER_SALT_LENGTH;

srtp发送码流

dtls 协商完成后会得到远端和本地的srtp的key和salt,用于创建srtp对象,远端的key和salt创建的srtp对象命名为srtp_in,所有进来的srtp数据需要通过改对象进行解密,解密为rtp;发送的srtp对象命名为srtp_out,所以需要发送的rtp,需要加密为srtp。

srtp_policy_t remote_policy
remote_policy.ssrc.type = ssrc_any_inbound;
memcpy(remote_policy.key, remote_key, SRTP_MASTER_KEY_LENGTH);
memcpy(remote_policy.key + SRTP_MASTER_KEY_LENGTH, remote_salt, SRTP_MASTER_SALT_LENGTH);
srtp_t srtp_in;
err_status_t res = srtp_create(&srtp_in, &remote_policy);
int res = srtp_protect(dtls_->srtp_out, buf, &protectedlen);
int sent = nice_agent_send(agent, streamid, componentid, protectedlen, buf);

rtcp处理

本demo暂时没有实现rtcp的处理,只是演示一下webrtc的基本码流协商传输流程。

测试

结果

image.png

参考

image.png
上一篇下一篇

猜你喜欢

热点阅读