實現簡易webrtc 網關

本文實現一個簡易的單向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的連接候選項。

  • chrome瀏覽器首先獲取服務器提供的offer sdp,收到sdp之後,創建應答sdp和ice 候選項發送到服務器。
  • 雙方都收到sdp之後會首先進行ice連接(即一條udp鏈路)。
  • 連接建立之後,發起dtls交互,得到遠端和本地的srtp的key(分別用於解密遠端到來的srtp和加密本地即將發出去的rtp數據包)。
  • 然後就可以接收和發送rtp,rtcp數據了,發送之前要進行srtp加密,然後通過ice的連接發送出去。
  • dtls 和 srtp 的數據包都是通過ice的udp連接進行傳輸的。

使用到的庫

交互流程

客戶端和服務器在同一個網段內,程序不支持指定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裏面除了媒體描述的信息之外新增了幾個選項:

  • a=rtcp-mux
    表示rtp和rtcp使用同一個端口進行發送和接收
  • a=mid:video
    標識這一路媒體的id名稱,用於a=group
  • a=group:BUNDLE video
    現在只有一路視頻流,當音視頻都有的時候 a=group:BUNDLE video audio 標識音頻和視頻流複用同一個端口進行發送和接收,通過ssrc進行區分不同的流。所以當既有rtcp-mux,又有a=group把多路流打包到一起的時候,只要創建一個主機類型候選項即可。
  • a=ice-ufrag 和 a=ice-pwd
    用於ice進行stun協商時進行對端認證。
  • a=fingerprint
    當雙方進行dtls協商交互srtp的加密key時,對對端進行驗證。
  • a=candidate
    ice的候選項 通知對端本地的ice連接的候選項,本文只使用了本地地址的主機候選項,沒有使用stun服務器。
  • a=ice-options:trickle
    通知對端支持trickle,即sdp裏面描述媒體信息和ice候選項的信息可以分開傳輸。

瀏覽器返回的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時

  • 創建 RTCPeerConnection
  • 發起websocket連接向服務器請求sdp,收到sdp之後RTCPeerConnection調用setLocalDescription,將描述設置爲本地描述。
  • RTCPeerConnection調用createAnswer創建answer sdp將創建的sdp通過websocket返回給服務端。
  • RTCPeerConnection的onaddstream回調,通知檢測到遠端的流之後,將其設置爲video標籤的源。
  • RTCPeerConnection的onicecandidate回調,通知有本地的ice回調上來了,收到之後將ice候選項通過websocket發送到服務器。
  • 服務器和瀏覽器都收到對方的sdp和候選項之後,就開始底層ice的連接建立和碼流傳輸。

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對象,

  • 設置控制模式爲true,按照ice的規範,主動發起offer的一端,應設置爲控制一端。
  • 從websocket連接得到本地的ip地址,將其ip增加到niceagent的地址列表中,這樣生成候選項的列表只包含設置的ip地址,否則會根據本地所有的網卡信息去生成。
  • 設置候選項採集完成,componet狀態改變,ice selected pair完成 回調。
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);
  • 創建流和組件
    通過nice_agent_add_stream添加組件,流就是視頻流或者音頻流,nice_agent_add_stream的返回值是從1開始的,添加流的時候,需要指定這條流的組件數componetnum,組件也就是這條流要使用的udp端口的個數,以視頻流爲例,如果rtp和rtcp的端口是分開的,那麼創建的組件數就是2,rtp的組件id是1,rtcp的組件id是2(這是烏龜的屁股—規定 參考rtc5245);如果rtp和rtcpmux,那麼只需要創建一個組件就可以了,組件id就是1。
    當視頻和音頻group bundle,並且rtcp-mux時,只需要一個stream,一個組件就可以了。
  • 採集流的候選項
    首先要nice_agent_attach_recv,attach之後纔會把流和組件對象和glib的網絡層綁定起來,attach的回調函數用來回調收到的網絡數據。
    nice_agent_gather_candidates 開啓收集流的候選項,收集完成之後,會通過之前的回調 “candidate-gathering-done” 返回。
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)
  • 候選項採集完成
    流的候選項採集完成之後通過,前面設置的回調函數cb_candidate_gathering_done返回:
    根據回調中的流id,獲取ice協商時的用戶名和密碼; 獲取收集到的候選項,這裏我們只有一個候選項,從NiceCandidate中可以得到候選項的foundation,優先級,ip地址,端口,候選項類別(這裏都是主機類型)等。
    將相關信息組建成sdp,就是服務器端的offer sdp,通過websocket發送給客戶端。
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;
  • 瀏覽器返回sdp 和 候選項
    收到sdp和候選項後,解析出ice協商用戶名和密碼,候選項的信息,設置到相應的流中。這裏流和組件都只有1個,值都是1。
nice_agent_set_remote_credentials(agent, 1, ufrag, pwd)
nice_agent_set_remote_candidates(agent, 1, 1, plist)
  • ice 連接狀態和候選項
    設置了候選項和ice認證信息之後,niceagent會自動去和客戶端協商ice連接建立,連接狀態的改變和採用的是哪一組候選項會通過回調函數通知。
    "component-state-changed" 回調函數中只打印了狀態的變化信息,狀態變化爲connecting,connected,ready。
    "new-selected-pair-full" 回調通知底層的ice選擇了哪一對候選項(一個本地候選項,一個遠端候選項)。
  • ice連接完成
    當組件的狀態變爲ready的時候,ice的連接狀態完成。接着瀏覽器端會向服務器端主動發起dtls連接,dtls的數據包和碼流包一樣都是通過ice建立的udp通道來完成的。
    流和通道的遠端數據的接收是通過nice_agent_attach_recv設置的回調函數cb_nice_recv來回調的。

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協商

  • dtls的實現在類dtls_srtp中,服務端是作爲dtls server,由客戶端來發起dtls連接,從niceagent中接收到的dtls數據包,都輸入到dtls_srtp類中。
  • dtls 協商主要將 從niceagent上收上來的數據輸入到ssl中,再將ssl的輸出通過niceagent發送到對端。
  • 然後檢測ssl的初始化是否完成,初始化完成之後,可以通過SSL_get_peer_certificate獲取到對端的fingerprint,這裏可以比較一下是否和客戶端返回的answer sdp裏面的fingerprint相同,相同將連接狀態置爲DTLS_STATE_CONNECTED
  • ssl初始化完成,SSL_export_keying_material 導出srtp的keying material,成功之後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);
  • 當dtls協商建立完成之後,chrome瀏覽器會向服務器端發送RR的rtcp數據包,服務器端收到這個數據包之後,會獨立創建一個發送線程讀取h264文件進行發送。(在dtls連接建立,srtp創建成功之後,就可以創建發送線程了)
  • 拆封h264幀爲rtp的過程和rtsp中tcp傳輸rtp基本一致,不過沒有4字節的額外頭信息而已。
  • 通過srtp_out,進行加密之後,使用niceagent發送到對端
int res = srtp_protect(dtls_->srtp_out, buf, &protectedlen);
int sent = nice_agent_send(agent, streamid, componentid, protectedlen, buf);

rtcp處理

本demo暫時沒有實現rtcp的處理,只是演示一下webrtc的基本碼流協商傳輸流程。

測試

  • webrtchtml 文件夾下面有個bat腳本,startpythonweb.bat會開啓python web服務
  • bin目錄下 啓動webrtcgateway程序,依賴的dll都在這裏

結果

  • 在PC端的chrome瀏覽器上面可以看到h264圖像,但是手機端好像不支持h264

     

    pc chrome測試h264

  • Firefox號稱已經支持了h264,實際協商的時候createanswer 返回的sdp裏面還是vp8,ice會失敗
  • 代碼行數統計

image.png

參考

image.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章