WebRTC 源碼分析 (三) Windows P2P 音視頻通話 peerconnection_...
介紹
環(huán)境: webrtc m98 、Windows
peerconnection_client 是一個WebRTC提供的示例程序,主要在Windows平臺上演示如何使用WebRTC庫來實現(xiàn)點對點的實時音頻和視頻通話。它是一個客戶端應(yīng)用程序,配合 peerconnection_server 信令服務(wù)器使用,通過信令服務(wù)器進行信令交換,建立并維護兩個或者多個客戶端之間的P2P連接。通過該示例對于我們?nèi)チ私釽ebRTC的整體架構(gòu)和運行流程有非常大幫助。
程序入口和主體框架
找到編譯好的 webrtc 示例 VS 程序,通過 ?VS 打開 all.sln 程序,然后將 peerconnection_client 設(shè)置為啟動項,如下圖所示
img_v2_67fbd162-8eeb-4f1b-9c18-e1b61936d83g啟動通話后,效果如下:
img_v2_77dfb6a9-6c95-4ef0-9983-18b6e03692cg如果你是在本地開 2 個客戶端調(diào)試,那么可以通過開啟 OBS 的虛擬攝像頭達到上面的效果。
peerconnection_client 主要是由以下幾個部分構(gòu)成
peerconnection_client UML (1)-
main.cc: 這是程序的入口點,它創(chuàng)建并運行應(yīng)用程序的消息循環(huán),初始化并運行主窗口。它會創(chuàng)建PeerConnectionClient和Conductor對象,并且鏈接他們,使得它們能一起協(xié)作。 -
main_wnd.cc: 它是主窗口類的實現(xiàn)。這個類負責(zé)所有的用戶界面操作,如按鈕點擊、視頻顯示窗口、狀態(tài)更新等。它還將用戶操作的事件通知到Conductor對象。 -
peer_connection_client.cc: 這個類是一個客戶端,它會連接到PeerConnectionServer信令服務(wù)器,然后向服務(wù)器注冊,并處理來自服務(wù)器的信令消息,以及發(fā)送到服務(wù)器的信令消息。 -
conductor.cc: 它是整個程序的核心,負責(zé)管理PeerConnectionClient對象和MainWnd對象。它還創(chuàng)建并管理WebRTC的PeerConnection對象,以及處理所有的WebRTC事件。例如,當(dāng)用戶點擊"用戶列表 item"時,MainWnd對象會將此事件通知給Conductor,Conductor會命令PeerConnectionClient向信令服務(wù)器發(fā)送一個信令消息,以便開始一個新的呼叫。
我們來看下 main.cc 中的核心代碼,也就是入口函數(shù):
int?PASCAL?wWinMain(HINSTANCE?instance,
????????????????????HINSTANCE?prev_instance,
????????????????????wchar_t*?cmd_line,
????????????????????int?cmd_show)?{
??rtc::WinsockInitializer?winsock_init;??//?初始化?Winsock
??CustomSocketServer?ss;??//?自定義?Socket?服務(wù)器
??rtc::AutoSocketServerThread?main_thread(&ss);??//?使用自定義?Socket?服務(wù)器創(chuàng)建主線程
??WindowsCommandLineArguments?win_args;??//?處理命令行參數(shù)
??int?argc?=?win_args.argc();
??char**?argv?=?win_args.argv();
??absl::ParseCommandLine(argc,?argv);??//?解析命令行參數(shù)
??//?InitFieldTrialsFromString?會存儲?char*,所以這個字符數(shù)組必須比應(yīng)用程序的生命周期更長
??const?std::string?forced_field_trials?=
??????absl::GetFlag(FLAGS_force_fieldtrials);
??webrtc::field_trial::InitFieldTrialsFromString(forced_field_trials.c_str());
??//?如果用戶指定的端口超出了允許的范圍?[1,?65535],則中止程序
??if?((absl::GetFlag(FLAGS_port)?1)?||?(absl::GetFlag(FLAGS_port)?>?65535))?{
????printf("Error:?%i?is?not?a?valid?port.\n",?absl::GetFlag(FLAGS_port));
????return?-1;
??}
??std::string?server?=?absl::GetFlag(FLAGS_server);??//?獲取服務(wù)器地址
??MainWnd?wnd(server.c_str(),?absl::GetFlag(FLAGS_port),??//?創(chuàng)建主窗口
??????????????absl::GetFlag(FLAGS_autoconnect),?absl::GetFlag(FLAGS_autocall));
??if?(!wnd.Create())?{
????RTC_DCHECK_NOTREACHED();??//?如果窗口創(chuàng)建失敗,則終止程序
????return?-1;
??}
??rtc::InitializeSSL();??//?初始化?SSL
??PeerConnectionClient?client;??//?創(chuàng)建?PeerConnectionClient?對象
??rtc::scoped_refptr?conductor(
??????new?rtc::RefCountedObject(&client,?&wnd)) ;??//?創(chuàng)建?Conductor?對象
??//?主循環(huán)
??MSG?msg;
??BOOL?gm;
??while?((gm?=?::GetMessage(&msg,?NULL,?0,?0))?!=?0?&&?gm?!=?-1)?{??//?獲取并處理消息,如果獲取失敗或者程序接收到退出消息,則退出循環(huán)
????if?(!wnd.PreTranslateMessage(&msg))?{??//?如果消息沒有被預(yù)處理
??????::TranslateMessage(&msg);??//?翻譯消息
??????::DispatchMessage(&msg);??//?分發(fā)消息
????}
??}
??if?(conductor->connection_active()?||?client.is_connected())?{??//?如果連接仍然活動,或者客戶端仍然連接著
????while?((conductor->connection_active()?||?client.is_connected())?&&??//?等待連接關(guān)閉
???????????(gm?=?::GetMessage(&msg,?NULL,?0,?0))?!=?0?&&?gm?!=?-1)?{
??????if?(!wnd.PreTranslateMessage(&msg))?{??//?如果消息沒有被預(yù)處理
????????::TranslateMessage(&msg);??//?翻譯消息
????????::DispatchMessage(&msg);??//?分發(fā)消息
??????}
????}
??}
??rtc::CleanupSSL();??//?清理?SSL
??return?0;?
入口函數(shù)的作用,就是初始化并啟動WebRTC peerconnection,處理命令行參數(shù),設(shè)置窗口界面,并開始接收和處理Windows消息,直到peer connection關(guān)閉和程序結(jié)束。
窗口管理
窗口管理的工作主要在 main_wnd.cc ?create 函數(shù),我們看一下它是如何創(chuàng)建 WebRTC 這個窗口的,
bool?MainWnd::Create()?{
??RTC_DCHECK(wnd_?==?NULL);?//?檢查窗口句柄是否為NULL,以確保窗口尚未創(chuàng)建。
??if?(!RegisterWindowClass())?//?注冊窗口類。如果注冊失敗,返回false。
????return?false;
??ui_thread_id_?=?::GetCurrentThreadId();?//?獲取當(dāng)前線程ID并存儲,這將用于后續(xù)的UI操作。
??//?創(chuàng)建一個新的窗口實例。這個窗口是一個具有內(nèi)置子窗口的主窗口,標題為"WebRTC"。
??wnd_?=?::CreateWindowExW(WS_EX_OVERLAPPEDWINDOW,?kClassName,?L"WebRTC",
???????????????????????????WS_OVERLAPPEDWINDOW?|?WS_VISIBLE?|?WS_CLIPCHILDREN,
???????????????????????????CW_USEDEFAULT,?CW_USEDEFAULT,?CW_USEDEFAULT,
???????????????????????????CW_USEDEFAULT,?NULL,?NULL,?GetModuleHandle(NULL),?this);
??//?發(fā)送一個消息給新創(chuàng)建的窗口,設(shè)置其字體為默認字體。
??::SendMessage(wnd_,?WM_SETFONT,?reinterpret_cast(GetDefaultFont()),
????????????????TRUE);
??CreateChildWindows();?//?創(chuàng)建子窗口,如編輯框、按鈕等。
??SwitchToConnectUI();?//?切換到"連接"用戶界面狀態(tài)。
??return?wnd_?!=?NULL;?//?如果窗口句柄不為NULL,說明窗口創(chuàng)建成功,返回true;否則返回false。
}
bool?MainWnd::RegisterWindowClass()?{
??if?(wnd_class_)?//?如果窗口類已經(jīng)注冊,直接返回true
????return?true;
??WNDCLASSEXW?wcex?=?{sizeof(WNDCLASSEX)};?//?初始化窗口類結(jié)構(gòu)體
??wcex.style?=?CS_DBLCLKS;?//?設(shè)置窗口樣式,這里允許接收雙擊消息
??wcex.hInstance?=?GetModuleHandle(NULL);?//?獲取當(dāng)前進程的實例句柄
??wcex.hbrBackground?=?reinterpret_cast(COLOR_WINDOW?+?1);?//?設(shè)置窗口背景顏色
??wcex.hCursor?=?::LoadCursor(NULL,?IDC_ARROW);?//?設(shè)置窗口光標樣式
??wcex.lpfnWndProc?=?&WndProc;?//?設(shè)置窗口消息處理函數(shù)
??wcex.lpszClassName?=?kClassName;?//?設(shè)置窗口類名
??
??//?調(diào)用RegisterClassExW函數(shù)注冊窗口類,注冊成功會返回一個窗口類的原子類名,失敗返回0
??wnd_class_?=?::RegisterClassExW(&wcex);
??RTC_DCHECK(wnd_class_?!=?0);?//?檢查窗口類是否注冊成功
??
??return?wnd_class_?!=?0;?//?如果窗口類注冊成功,返回true;否則返回false。
}
void?MainWnd::CreateChildWindow(HWND*?wnd,
????????????????????????????????MainWnd::ChildWindowID?id,
????????????????????????????????const?wchar_t*?class_name,
????????????????????????????????DWORD?control_style,
????????????????????????????????DWORD?ex_style)?{
??if?(::IsWindow(*wnd))?//?如果窗口已存在,直接返回,避免重復(fù)創(chuàng)建
????return;
??//?子窗口初始為隱藏狀態(tài),在調(diào)整大小后顯示
??DWORD?style?=?WS_CHILD?|?control_style;?
??//?創(chuàng)建子窗口,窗口位置和尺寸初始為100*100,實際會在后續(xù)調(diào)整
??*wnd?=?::CreateWindowExW(ex_style,?class_name,?L"",?style,?100,?100,?100,?100,
???????????????????????????wnd_,?reinterpret_cast(id),
???????????????????????????GetModuleHandle(NULL),?NULL);?
??RTC_DCHECK(::IsWindow(*wnd)?!=?FALSE);?//?檢查窗口是否創(chuàng)建成功
??//?發(fā)送消息給窗口,設(shè)置默認字體
??::SendMessage(*wnd,?WM_SETFONT,?reinterpret_cast(GetDefaultFont()),
????????????????TRUE);
}
void?MainWnd::CreateChildWindows()?{
??//?按照?tab?順序創(chuàng)建子窗口
??CreateChildWindow(&label1_,?LABEL1_ID,?L"Static",?ES_CENTER?|?ES_READONLY,?0);
??CreateChildWindow(&edit1_,?EDIT_ID,?L"Edit",
????????????????????ES_LEFT?|?ES_NOHIDESEL?|?WS_TABSTOP,?WS_EX_CLIENTEDGE);
??CreateChildWindow(&label2_,?LABEL2_ID,?L"Static",?ES_CENTER?|?ES_READONLY,?0);
??CreateChildWindow(&edit2_,?EDIT_ID,?L"Edit",
????????????????????ES_LEFT?|?ES_NOHIDESEL?|?WS_TABSTOP,?WS_EX_CLIENTEDGE);
??CreateChildWindow(&button_,?BUTTON_ID,?L"Button",?BS_CENTER?|?WS_TABSTOP,?0);
??CreateChildWindow(&listbox_,?LISTBOX_ID,?L"ListBox",
????????????????????LBS_HASSTRINGS?|?LBS_NOTIFY,?WS_EX_CLIENTEDGE);
??//?初始化?edit1_?和?edit2_?的文本內(nèi)容
??::SetWindowTextA(edit1_,?server_.c_str());
??::SetWindowTextA(edit2_,?port_.c_str());
}
//接收系統(tǒng)發(fā)送給窗口的消息
LRESULT?CALLBACK?MainWnd::WndProc(HWND?hwnd,?UINT?msg,?WPARAM?wp,?LPARAM?lp)?{
?...
??return?result;
}
這個函數(shù)的目的是創(chuàng)建一個主窗口,并根據(jù)程序的需要進行配置。在這個窗口中,會創(chuàng)建一些子窗口(ip編輯框,連接按鈕,用戶列表等),并先設(shè)置窗口的UI狀態(tài)為 "連接" 狀態(tài)。
最后通過調(diào)用 windows api ShowWindow 將創(chuàng)建好的一系列窗口顯示
void?MainWnd::LayoutConnectUI(bool?show)?{
??//?定義窗口布局和屬性的結(jié)構(gòu)體
??struct?Windows?{
????HWND?wnd;
????const?wchar_t*?text;
????size_t?width;
????size_t?height;
??}?windows[]?=?{?//?初始化窗口數(shù)組
??????{label1_,?L"Server"},??{edit1_,?L"XXXyyyYYYgggXXXyyyYYYggg"},
??????{label2_,?L":"},???????{edit2_,?L"XyXyX"},
??????{button_,?L"Connect"},
??};
??if?(show)?{?//?如果要顯示連接界面
????const?size_t?kSeparator?=?5;?//?控件之間的間隔
????size_t?total_width?=?(ARRAYSIZE(windows)?-?1)?*?kSeparator;?//?計算所有窗口的總寬度
????//?計算每個窗口的尺寸并更新總寬度
????for?(size_t?i?=?0;?i???????CalculateWindowSizeForText(windows[i].wnd,?windows[i].text,
?????????????????????????????????&windows[i].width,?&windows[i].height);
??????total_width?+=?windows[i].width;
????}
????RECT?rc;
????::GetClientRect(wnd_,?&rc);?//?獲取主窗口的客戶區(qū)大小
????size_t?x?=?(rc.right?/?2)?-?(total_width?/?2);?//?計算第一個窗口的水平位置
????size_t?y?=?rc.bottom?/?2;?//?計算窗口的垂直位置
????//?依次設(shè)置每個窗口的位置并顯示
????for?(size_t?i?=?0;?i???????size_t?top?=?y?-?(windows[i].height?/?2);
??????::MoveWindow(windows[i].wnd,?static_cast<int>(x),?static_cast<int>(top),
???????????????????static_cast<int>(windows[i].width),
???????????????????static_cast<int>(windows[i].height),?TRUE);
??????x?+=?kSeparator?+?windows[i].width;?//?更新下一個窗口的水平位置
??????if?(windows[i].text[0]?!=?'X')?//?設(shè)置窗口的文本內(nèi)容
????????::SetWindowTextW(windows[i].wnd,?windows[i].text);
??????::ShowWindow(windows[i].wnd,?SW_SHOWNA);?//?顯示窗口
????}
??}?else?{?//?如果不顯示連接界面,則隱藏所有窗口
????for?(size_t?i?=?0;?i???????::ShowWindow(windows[i].wnd,?SW_HIDE);
????}
??}
}
void?MainWnd::SwitchToConnectUI()?{
??RTC_DCHECK(IsWindow());?//?確保主窗口存在
??LayoutPeerListUI(false);?//?隱藏用戶列表界面
??ui_?=?CONNECT_TO_SERVER;?//?更新到連接狀態(tài)界面
??LayoutConnectUI(true);?//?顯示連接服務(wù)器界面
??::SetFocus(edit1_);?//?將焦點設(shè)置到第一個輸入框
??if?(auto_connect_)?//?如果設(shè)置了自動連接,則模擬點擊連接按鈕
????::PostMessage(button_,?BM_CLICK,?0,?0);
}
最后窗口會這樣顯示:
當(dāng)我們點擊上圖中的 Connect 后,系統(tǒng)會發(fā)送消息給 WndProc 窗口的接收消息的回調(diào)函數(shù)上,如果連接成功,就會切換到 用戶list UI,核心代碼如下:
void?MainWnd::SwitchToPeerList(const?Peers&?peers)?{
??//?關(guān)閉連接界面
??LayoutConnectUI(false);
??//?重置列表內(nèi)容
??::SendMessage(listbox_,?LB_RESETCONTENT,?0,?0);
??//?向列表中添加一行標題
??AddListBoxItem(listbox_,?"List?of?currently?connected?peers:",?-1);
??//?循環(huán)遍歷對等端列表,將每個對等端添加到列表中
??Peers::const_iterator?i?=?peers.begin();
??for?(;?i?!=?peers.end();?++i)
????AddListBoxItem(listbox_,?i->second.c_str(),?i->first);
??//?設(shè)置當(dāng)前用戶界面狀態(tài)為?LIST_PEERS
??ui_?=?LIST_PEERS;
??//?顯示對等端列表界面
??LayoutPeerListUI(true);
??//?將焦點設(shè)置到列表上
??::SetFocus(listbox_);
??//?如果?auto_call_?為?true,并且對等端列表不為空
??if?(auto_call_?&&?peers.begin()?!=?peers.end())?{
????//?獲取列表中的項目數(shù)量
????LRESULT?count?=?::SendMessage(listbox_,?LB_GETCOUNT,?0,?0);
????if?(count?!=?LB_ERR)?{
??????//?選中列表中的最后一個項目
??????LRESULT?selection?=?::SendMessage(listbox_,?LB_SETCURSEL,?count?-?1,?0);
??????//?如果選中成功,發(fā)送一個?WM_COMMAND?消息,模擬雙擊事件
??????if?(selection?!=?LB_ERR)
????????::PostMessage(wnd_,?WM_COMMAND,
??????????????????????MAKEWPARAM(GetDlgCtrlID(listbox_),?LBN_DBLCLK),
??????????????????????reinterpret_cast(listbox_));
????}
??}
}
SwitchToPeerList函數(shù)首先關(guān)閉了連接界面,然后將列表的內(nèi)容進行了重置,添加了一個標題到列表中,并添加了所有當(dāng)前在線的對等端到列表中。接著,它將用戶界面的狀態(tài)切換到了顯示對等端列表,并設(shè)置了列表的焦點。最后,如果設(shè)置了自動呼叫并且有對等端在線,它就會選中列表中的最后一個項目,并模擬一次雙擊事件。
這段代碼執(zhí)行后,對應(yīng)的用戶列表就可以顯示出來,比如, 如下所示:
img_v2_1986c4b8-2db5-4bff-bc5e-813f0ea8b9dg當(dāng)雙擊用戶名稱時,雙方就會發(fā)起 SDP 媒體協(xié)商,網(wǎng)絡(luò)協(xié)商等,如果都協(xié)商成功就可以傳輸并顯示音視頻畫面了,這個后面會詳細說到。
如果用戶主動關(guān)閉窗口,窗口會收到退出的消息并關(guān)閉 peerconnection 連接。
到這里窗口整個的創(chuàng)建->更新->關(guān)閉都分析完了,接下來會分析 peerconnection_client 與 server 的信令交互
信令處理
image-20230618154102953下載 pcapng 包鏈接: https://pan.baidu.com/s/1wGyyLSxd7_X2p7T8O1nPdg?pwd=frrr 提取碼: frrr
通過抓包我們得到了如下幾個信令:
GET sign_in: 用戶登錄消息
**GET sign_out:**用戶退出消息
POST message: 協(xié)商交互消息
GET wait: 用戶等待消息
這里繪制了一張簡要的時序圖
PeerConnection_Client_p2p當(dāng)用戶點擊 Connect 時,會發(fā)起登錄信息:
void?Conductor::StartLogin(const?std::string&?server,?int?port)?{
??if?(client_->is_connected())
????return;
??server_?=?server;
??//在?PeerConnectionClient?中與?server?發(fā)起信令登錄連接
??client_->Connect(server,?port,?GetPeerName());
}
調(diào)用這行代碼后,會執(zhí)行到 PeerConnectionClient::Connect 函數(shù):
void?PeerConnectionClient::Connect(const?std::string&?server,
???????????????????????????????????int?port,
???????????????????????????????????const?std::string&?client_name)?{
??RTC_DCHECK(!server.empty());
??RTC_DCHECK(!client_name.empty());
?//判斷當(dāng)前的狀態(tài)是否處于連接
??if?(state_?!=?NOT_CONNECTED)?{
????RTC_LOG(LS_WARNING)
????????<"The?client?must?not?be?connected?before?you?can?call?Connect()";
????callback_->OnServerConnectionFailure();
????return;
??}
?//判斷ip和名稱是否為空
??if?(server.empty()?||?client_name.empty())?{
????callback_->OnServerConnectionFailure();
????return;
??}
?//如果端口小于?0?使用默認的
??if?(port?<=?0)
????port?=?kDefaultServerPort;
?//設(shè)置信令服務(wù)器?IP?和端口
??server_address_.SetIP(server);
??server_address_.SetPort(port);
??client_name_?=?client_name;
??/**
??*if?(server_address_.IsUnresolvedIP()):
??檢查?server_address_?是否是一個未解析的?IP?地址
??(也就是說,它實際上是一個域名)。如果是,
??那么需要進行?DNS?解析。在這種情況下,代碼會創(chuàng)建一個?rtc::AsyncResolver?對象來進行異步的?DNS?解析,并設(shè)置一個回調(diào)函數(shù)?PeerConnectionClient::OnResolveResult,當(dāng)解析完成時這個函數(shù)會被調(diào)用。然后,代碼調(diào)用?resolver_->Start(server_address_)?來開始解析過程。
??*/
??if?(server_address_.IsUnresolvedIP())?{
????state_?=?RESOLVING;
????resolver_?=?new?rtc::AsyncResolver();
????resolver_->SignalDone.connect(this,?&PeerConnectionClient::OnResolveResult);
????resolver_->Start(server_address_);
??}?else?{
????DoConnect();//如果域名不需要解析,則直接發(fā)起連接
??}
}
這里由于我們填的是本機地址,所以不需要 DNS 解析,直接看 DoConnect
void?PeerConnectionClient::DoConnect()?{
??//創(chuàng)建一個控制連接(發(fā)送和接收命令)
??control_socket_.reset(CreateClientSocket(server_address_.ipaddr().family()));
??//用于?hanging?GET?操作(長輪詢,用于接收服務(wù)器的實時更新)
??hanging_get_.reset(CreateClientSocket(server_address_.ipaddr().family()));
??//初始化套接字信號,包括連接、數(shù)據(jù)接收等事件的回調(diào)處理。
??InitSocketSignals();
??char?buffer[1024];
??//準備一個?HTTP?GET?請求,用于登錄到服務(wù)器。這個請求的路徑是?"/sign_in",并且包含一個查詢參數(shù),即客戶端的名字。
??snprintf(buffer,?sizeof(buffer),?"GET?/sign_in?%s?HTTP/1.0\r\n\r\n",
???????????client_name_.c_str());
??onconnect_data_?=?buffer;
??//嘗試連接到控制套接字。如果連接成功,ConnectControlSocket()?將返回?true,否則返回?false。
??bool?ret?=?ConnectControlSocket();
??if?(ret)
????//如果連接成功,將狀態(tài)設(shè)置為?SIGNING_IN,表示正在進行登錄操作
????state_?=?SIGNING_IN;
??if?(!ret)?{//如果連接失敗,調(diào)用回調(diào)函數(shù)?OnServerConnectionFailure(),通知其他部分連接失敗
????callback_->OnServerConnectionFailure();
??}
??//啟動當(dāng)前線程
??rtc::Thread::Current()->Start();
}
PeerConnectionClient 與服務(wù)器交互的協(xié)議是 http 短連接,此處是創(chuàng)建了 2 個 異步的 socket, control_socket_ 主要是主動發(fā)起一些信令的操作,比如登錄,退出,offer,candide 消息等;而 hanging_get_ 它主要是向信令服務(wù)器請求對方的信令消息,比如 answer,candidate,用戶列表等,每次是先發(fā)一個 wait 信令,等待信令服務(wù)器的響應(yīng),當(dāng)信令服務(wù)器有響應(yīng)時,就會執(zhí)行這些注入的回調(diào),代碼如下:
void?PeerConnectionClient::InitSocketSignals()?{
??RTC_DCHECK(control_socket_.get()?!=?NULL);
??RTC_DCHECK(hanging_get_.get()?!=?NULL);
??/**?close?事件**/
??control_socket_->SignalCloseEvent.connect(this,
????????????????????????????????????????????&PeerConnectionClient::OnClose);
??hanging_get_->SignalCloseEvent.connect(this,?&PeerConnectionClient::OnClose);
??
????/**?connect?事件**/
??control_socket_->SignalConnectEvent.connect(this,
??????????????????????????????????????????????&PeerConnectionClient::OnConnect);
??hanging_get_->SignalConnectEvent.connect(
??????this,?&PeerConnectionClient::OnHangingGetConnect);
??
??????/**?read?事件**/
??control_socket_->SignalReadEvent.connect(this,?&PeerConnectionClient::OnRead);
??hanging_get_->SignalReadEvent.connect(
??????this,?&PeerConnectionClient::OnHangingGetRead);
}
發(fā)起登錄連接
bool?PeerConnectionClient::ConnectControlSocket()?{
??//檢查當(dāng)前的連接狀態(tài)
??RTC_DCHECK(control_socket_->GetState()?==?rtc::Socket::CS_CLOSED);
??//向信令服務(wù)器發(fā)起連接請求
??int?err?=?control_socket_->Connect(server_address_);
??if?(err?==?SOCKET_ERROR)?{
????Close();
????return?false;
??}
??return?true;
}
當(dāng)連接成功
void?PeerConnectionClient::OnConnect(rtc::Socket*?socket)?{
??//判斷發(fā)送的信令是否為空
??RTC_DCHECK(!onconnect_data_.empty());
??//發(fā)送
??size_t?sent?=?socket->Send(onconnect_data_.c_str(),?onconnect_data_.length());
??RTC_DCHECK(sent?==?onconnect_data_.length());
??onconnect_data_.clear();
}
向信令服務(wù)器發(fā)送的消息及響應(yīng)
GET /sign_in?devyk@devyk-mwin HTTP/1.0\r\n
HTTP/1.1 200 Added\r\n
Server: PeerConnectionTestServer/0.1\r\n
Cache-Control: no-cache\r\n
Connection: close\r\n
Content-Type: text/plain\r\n
Content-Length: 22\r\n
Pragma: 12\r\n
Access-Control-Allow-Origin: *\r\n
Access-Control-Allow-Credentials: true\r\n
Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n
Access-Control-Allow-Headers: Content-Type, Content-Length, Connection, Cache-Control\r\n
Access-Control-Expose-Headers: Content-Length\r\n
\r\n
devyk@devyk-mwin,12,1\n
當(dāng)?shù)诙€人連接進來的時候,收到的消息
????HTTP/1.1?200?Added\r\n
????Server:?PeerConnectionTestServer/0.1\r\n
????Cache-Control:?no-cache\r\n
????Connection:?close\r\n
????Content-Type:?text/plain\r\n
????Content-Length:?44\r\n
????Pragma:?13\r\n
????Access-Control-Allow-Origin:?*\r\n
????Access-Control-Allow-Credentials:?true\r\n
????Access-Control-Allow-Methods:?POST,?GET,?OPTIONS\r\n
????Access-Control-Allow-Headers:?Content-Type,?Content-Length,?Connection,?Cache-Control\r\n
????Access-Control-Expose-Headers:?Content-Length\r\n
????\r\n
????devyk@devyk-mwin,13,1\n
????devyk@devyk-mwin,12,1\n
解析 Socket 收到的協(xié)議
void?PeerConnectionClient::OnRead(rtc::Socket*?socket)?{
??size_t?content_length?=?0;
??//?讀取服務(wù)器發(fā)送的數(shù)據(jù)到?control_data_?緩沖區(qū),并獲取內(nèi)容長度
??if?(ReadIntoBuffer(socket,?&control_data_,?&content_length))?{
????size_t?peer_id?=?0,?eoh?=?0;
????//?解析服務(wù)器的響應(yīng),獲取?peer_id?和?eoh(頭部結(jié)束的位置)
????bool?ok?=?ParseServerResponse(control_data_,?content_length,?&peer_id,?&eoh);
????if?(ok)?{
??????if?(my_id_?==?-1)?{
????????//?如果是第一次響應(yīng),存儲服務(wù)器分配的?ID
????????RTC_DCHECK(state_?==?SIGNING_IN);
????????my_id_?=?static_cast<int>(peer_id);
????????RTC_DCHECK(my_id_?!=?-1);
????????//?如果響應(yīng)的主體部分存在內(nèi)容,則將已經(jīng)連接的對等方信息添加到?peers_?列表中
????????if?(content_length)?{
??????????size_t?pos?=?eoh?+?4;
??????????while?(pos?????????????size_t?eol?=?control_data_.find('\n',?pos);
????????????if?(eol?==?std::string::npos)
??????????????break;
????????????int?id?=?0;
????????????std::string?name;
????????????bool?connected;
????????????//?解析對等方條目,獲取名字、ID以及連接狀態(tài)
????????????if?(ParseEntry(control_data_.substr(pos,?eol?-?pos),?&name,?&id,?&connected)?&&
????????????????id?!=?my_id_)?{
??????????????//?如果對等方不是自己,將其添加到對等方列表中,并觸發(fā)連接事件
??????????????peers_[id]?=?name;
??????????????callback_->OnPeerConnected(id,?name);
????????????}
????????????pos?=?eol?+?1;
??????????}
????????}
????????RTC_DCHECK(is_connected());
????????//?觸發(fā)已登錄事件
????????callback_->OnSignedIn();
??????}?else?if?(state_?==?SIGNING_OUT)?{
????????//?如果當(dāng)前狀態(tài)是正在退出,則關(guān)閉連接并觸發(fā)斷開連接事件
????????Close();
????????callback_->OnDisconnected();
??????}?else?if?(state_?==?SIGNING_OUT_WAITING)?{
????????//?如果當(dāng)前狀態(tài)是等待退出,則退出
????????SignOut();
??????}
????}
????//?清空?control_data_?緩沖區(qū)
????control_data_.clear();
????if?(state_?==?SIGNING_IN)?{
??????//?如果當(dāng)前狀態(tài)是正在登錄,則切換到已連接狀態(tài),并連接到服務(wù)器
??????RTC_DCHECK(hanging_get_->GetState()?==?rtc::Socket::CS_CLOSED);
??????state_?=?CONNECTED;
??????hanging_get_->Connect(server_address_);
????}
??}
}
當(dāng)信令服務(wù)器發(fā)送登錄響應(yīng)時,會觸發(fā) PeerConnectionClient::OnRead() 函數(shù)。 首先從 socket 讀取響應(yīng)信息至 control_data_ 中,如果是短連接則需要關(guān)閉socket。接著驗證響應(yīng)中的狀態(tài)碼,獲取信令服務(wù)器分配的peer id。 登錄信令的響應(yīng)中會包含其他登錄客戶端的信息,這些客戶端的信令會顯示到peer list界面上。
解析其他客戶端的信息后,會觸發(fā)Conductor::OnPeerConnected函數(shù),在這個函數(shù)中會將客戶端的信息顯示到peer list界面上。
devyk@devyk-mwin,13,1\n
devyk@devyk-mwin,12,1\n
響應(yīng)信息的格式是:peer的name,信令服務(wù)器分配的peer id,是否處于登錄狀態(tài),1表示處于登錄狀態(tài),0表示登出狀態(tài)。
成功登錄信令服務(wù)器后,hanging_get socket 也開始登錄信令服務(wù)器,用于接收信令服務(wù)器發(fā)送給客戶端的信息。
當(dāng)連接成功后,發(fā)送等待消息,如果有新的信令消息,服務(wù)端就轉(zhuǎn)發(fā)過來
void?PeerConnectionClient::OnHangingGetConnect(rtc::Socket*?socket)?{
??char?buffer[1024];
??snprintf(buffer,?sizeof(buffer),?"GET?/wait?peer_id=%i?HTTP/1.0\r\n\r\n",
???????????my_id_);
??int?len?=?static_cast<int>(strlen(buffer));
??int?sent?=?socket->Send(buffer,?len);
??RTC_DCHECK(sent?==?len);
}
//發(fā)送的數(shù)據(jù)
GET /wait?peer_id=12 HTTP/1.0\r\n
當(dāng)有新的信令消息產(chǎn)生時,會以 wait 的響應(yīng)回來
HTTP/1.1 200 OK\r\n
Server: PeerConnectionTestServer/0.1\r\n
Cache-Control: no-cache\r\n
Connection: close\r\n
Content-Type: text/plain\r\n
Content-Length: 22\r\n
Pragma: 12\r\n
Access-Control-Allow-Origin: *\r\n
Access-Control-Allow-Credentials: true\r\n
Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n
Access-Control-Allow-Headers: Content-Type, Content-Length, Connection, Cache-Control\r\n
Access-Control-Expose-Headers: Content-Length\r\n
\r\n
devyk@devyk-mwin,13,1\n
然后就會觸發(fā)下面的函數(shù):
void?PeerConnectionClient::OnHangingGetRead(rtc::Socket*?socket)?{
??RTC_LOG(LS_INFO)?<??size_t?content_length?=?0;
??//從指定的socket讀取響應(yīng)信息,并做適當(dāng)?shù)奶幚怼H绻麖捻憫?yīng)中得知使用的是http短連接,那么需要關(guān)閉socket。
??if?(ReadIntoBuffer(socket,?¬ification_data_,?&content_length))?{
????size_t?peer_id?=?0,?eoh?=?0;
????//解析響應(yīng)碼,并讀取信令服務(wù)器分配的?peer?id
????bool?ok?=
????????ParseServerResponse(notification_data_,?content_length,?&peer_id,?&eoh);
????if?(ok)?{
??????//?Store?the?position?where?the?body?begins.
??????size_t?pos?=?eoh?+?4;
??????//檢查是否是自己的ID,如果是,那么這個通知可能是有新的成員加入或者有成員斷開連接。
??????//?然后,它嘗試解析主體內(nèi)容,獲取?peer?的?id,名稱和連接狀態(tài)。如果解析成功,
??????//?并且?peer?是已連接的,那么就將這個?peer?添加到?peers?列表中,
??????//并通知回調(diào)有?peer?連接;如果?peer?是斷開的,那么就從?peers?列表中移除,并通知回調(diào)有?peer?斷開連接
??????if?(my_id_?==?static_cast<int>(peer_id))?{
????????//?A?notification?about?a?new?member?or?a?member?that?just
????????//?disconnected.
????????int?id?=?0;
????????std::string?name;
????????bool?connected?=?false;
????????if?(ParseEntry(notification_data_.substr(pos),?&name,?&id,
???????????????????????&connected))?{
??????????if?(connected)?{
????????????peers_[id]?=?name;
????????????callback_->OnPeerConnected(id,?name);
??????????}?else?{
????????????peers_.erase(id);
????????????callback_->OnPeerDisconnected(id);
??????????}
????????}
??????}?else?{
??????????//用于處理offer、answer、candidate信令
????????OnMessageFromPeer(static_cast<int>(peer_id),
??????????????????????????notification_data_.substr(pos));
??????}
????}
????notification_data_.clear();
??}
??if?(hanging_get_->GetState()?==?rtc::Socket::CS_CLOSED?&&
??????state_?==?CONNECTED)?{
????hanging_get_->Connect(server_address_);
??}
}
當(dāng)信令服務(wù)器需要主動發(fā)送消息給客戶端時,會包裝成wait信令的響應(yīng)信息。有其他客戶端登錄或登出信令服務(wù)器時,會通知本端,本端會根據(jù)信令服務(wù)器反饋的信息更新peer list界面的用戶列表。 當(dāng)收到信令服務(wù)器轉(zhuǎn)發(fā)的其他客戶端的offer、answer、candidate信息時,會進入OnMessageFromPeer()函數(shù)處理。
void?PeerConnectionClient::OnMessageFromPeer(int?peer_id,
?????????????????????????????????????????????const?std::string&?message)?{
??if?(message.length()?==?(sizeof(kByeMessage)?-?1)?&&
??????message.compare(kByeMessage)?==?0)?{
????callback_->OnPeerDisconnected(peer_id);
??}?else?{
????/*收到的是offer、answer、candidate信令*/
????callback_->OnMessageFromPeer(peer_id,?message);
??}
}
分析完讀取和解析 http 協(xié)議后,我們看下如何進行 CreateOffer 的,
void?Conductor::ConnectToPeer(int?peer_id)?{
??RTC_DCHECK(peer_id_?==?-1);
??RTC_DCHECK(peer_id?!=?-1);
??if?(peer_connection_.get())?{
????main_wnd_->MessageBox(
????????"Error",?"We?only?support?connecting?to?one?peer?at?a?time",?true);
????return;
??}
??//初始化?peer?,成功就創(chuàng)建?CreateOffer
??if?(InitializePeerConnection())?{
????peer_id_?=?peer_id;
????peer_connection_->CreateOffer(
????????this,?webrtc::PeerConnectionInterface::RTCOfferAnswerOptions());
??}?else?{
????main_wnd_->MessageBox("Error",?"Failed?to?initialize?PeerConnection",?true);
??}
}
當(dāng)點擊 peer list 用戶中任意一個,會執(zhí)行到此處,如果 InitializePeerConnection 為 true ,那么就可以 CreateOffer.
//第一步:
bool?Conductor::InitializePeerConnection()?{
??//?檢查是否已存在?peer_connection_factory_?或
??//?peer_connection_,都應(yīng)該是空的,否則報錯
??RTC_DCHECK(!peer_connection_factory_);
??RTC_DCHECK(!peer_connection_);
??//?沒有?signaling_thread_?的話就創(chuàng)建一個新的
??if?(!signaling_thread_.get())?{
????signaling_thread_?=?rtc::Thread::CreateWithSocketServer();
????signaling_thread_->Start();
??}
??//?使用?signaling_thread_?創(chuàng)建?PeerConnectionFactory
??//?PeerConnectionFactory?是用于生成?PeerConnections,?MediaStreams?和?MediaTracks?的工廠類
??peer_connection_factory_?=?webrtc::CreatePeerConnectionFactory(
??????nullptr?/*?network_thread?*/,?
??????nullptr?/*?worker_thread?*/,
??????signaling_thread_.get(),?/*?signaling_thread?*/
??????nullptr?/*?default_adm?*/,
??????webrtc::CreateBuiltinAudioEncoderFactory(),
??????webrtc::CreateBuiltinAudioDecoderFactory(),
??????webrtc::CreateBuiltinVideoEncoderFactory(),
??????webrtc::CreateBuiltinVideoDecoderFactory(),?nullptr?/*?audio_mixer?*/,
??????nullptr?/*?audio_processing?*/);
????//?如果?PeerConnectionFactory?初始化失敗,清理資源并返回錯誤
??if?(!peer_connection_factory_)?{
????main_wnd_->MessageBox("Error",?"Failed?to?initialize?PeerConnectionFactory",
??????????????????????????true);
????DeletePeerConnection();
????return?false;
??}
??//?創(chuàng)建?PeerConnection,如果失敗,清理資源并返回錯誤
??if?(!CreatePeerConnection())?{
????main_wnd_->MessageBox("Error",?"CreatePeerConnection?failed",?true);
????DeletePeerConnection();
??}
??//?添加音頻和視頻軌道
??AddTracks();
??//?返回?peer_connection_?是否已初始化
??return?peer_connection_?!=?nullptr;
}
第一步: InitializePeerConnection():這個方法的目標是初始化一個PeerConnectionFactory,并創(chuàng)建一個PeerConnection。首先,它確保PeerConnectionFactory和PeerConnection不存在。如果還沒有創(chuàng)建信令線程,就創(chuàng)建一個新的。然后,使用這個信令線程創(chuàng)建一個新的PeerConnectionFactory,用于后續(xù)生成PeerConnections, MediaStreams和MediaTracks。如果PeerConnectionFactory創(chuàng)建失敗,它將清理資源并返回錯誤。最后,創(chuàng)建一個PeerConnection,添加音頻和視頻軌道,并返回是否成功初始化PeerConnection。
//第二步:
bool?Conductor::CreatePeerConnection()?{
??//?檢查?peer_connection_factory_?是否存在且?peer_connection_
??//?是否為空,否則報錯
??RTC_DCHECK(peer_connection_factory_);
??RTC_DCHECK(!peer_connection_);
??//?創(chuàng)建一個新的?PeerConnection?配置
??webrtc::PeerConnectionInterface::RTCConfiguration?config;
??config.sdp_semantics?=?webrtc::SdpSemantics::kUnifiedPlan;
??webrtc::PeerConnectionInterface::IceServer?server;
??server.uri?=?GetPeerConnectionString();
??config.servers.push_back(server);
??//?使用?PeerConnectionFactory?和配置創(chuàng)建新的?PeerConnection
??peer_connection_?=?peer_connection_factory_->CreatePeerConnection(
??????config,?nullptr,?nullptr,?this);
??return?peer_connection_?!=?nullptr;
}
第二步: CreatePeerConnection():這個方法用于創(chuàng)建一個新的PeerConnection。首先,它會檢查PeerConnectionFactory是否存在,且PeerConnection是否為空。然后,創(chuàng)建一個新的PeerConnection配置,設(shè)置SDP協(xié)議的語義為統(tǒng)一計劃,并添加ICE服務(wù)器。最后,使用PeerConnectionFactory和剛剛創(chuàng)建的配置來創(chuàng)建一個新的PeerConnection,并返回創(chuàng)建是否成功。
//第三步:
void?Conductor::AddTracks()?{
??//?如果已經(jīng)添加了軌道,則不再添加
??if?(!peer_connection_->GetSenders().empty())?{
????return;??//?Already?added?tracks.
??}
??//?創(chuàng)建音頻軌道并添加到?PeerConnection
??rtc::scoped_refptr?audio_track(
??????peer_connection_factory_->CreateAudioTrack(
??????????kAudioLabel,?peer_connection_factory_->CreateAudioSource(
???????????????????????????cricket::AudioOptions()))) ;
??auto?result_or_error?=?peer_connection_->AddTrack(audio_track,?{kStreamId});
??if?(!result_or_error.ok())?{
????RTC_LOG(LS_ERROR)?<"Failed?to?add?audio?track?to?PeerConnection:?"
??????????????????????<??}
??//?創(chuàng)建視頻源和視頻軌道并添加到?PeerConnection
??rtc::scoped_refptr?video_device?=
??????CapturerTrackSource::Create();
??if?(video_device)?{
????rtc::scoped_refptr?video_track_(
????????peer_connection_factory_->CreateVideoTrack(kVideoLabel,?video_device)) ;
????main_wnd_->StartLocalRenderer(video_track_);
????result_or_error?=?peer_connection_->AddTrack(video_track_,?{kStreamId});
????if?(!result_or_error.ok())?{
??????RTC_LOG(LS_ERROR)?<"Failed?to?add?video?track?to?PeerConnection:?"
????????????????????????<????}
??}?else?{
????RTC_LOG(LS_ERROR)?<"OpenVideoCaptureDevice?failed";
??}
??//?將界面切換到流媒體?UI
??main_wnd_->SwitchToStreamingUI();
}
第三步: AddTracks():這個方法的目標是向PeerConnection添加音頻和視頻軌道。首先,它會檢查是否已經(jīng)添加了軌道。如果已經(jīng)添加了,則不再添加。然后,創(chuàng)建一個音頻軌道并添加到PeerConnection。之后,創(chuàng)建一個視頻源和一個視頻軌道,并添加到PeerConnection。如果添加軌道失敗,會記錄錯誤信息。最后,將用戶界面切換到流媒體UI。
這些步驟(任意平臺)是設(shè)置WebRTC通信的關(guān)鍵步驟。在創(chuàng)建并初始化PeerConnectionFactory之后,我們可以創(chuàng)建PeerConnection,然后在PeerConnection上添加音頻和視頻軌道,這樣我們就可以開始進行實時的音視頻通信了。
如果這三步執(zhí)行都沒有問題,那么就是發(fā)起 offer 了,當(dāng) CreateOffer 成功時,會有成功回調(diào)
/**?SDP?設(shè)置成功回調(diào)*/
void?Conductor::OnSuccess(webrtc::SessionDescriptionInterface*?desc)?{
??peer_connection_->SetLocalDescription(
??????DummySetSessionDescriptionObserver::Create(),?desc);
??std::string?sdp;
??desc->ToString(&sdp);
??//?For?loopback?test.?To?save?some?connecting?delay.
??if?(loopback_)?{
????//?Replace?message?type?from?"offer"?to?"answer"
????std::unique_ptr?session_description?=
????????webrtc::CreateSessionDescription(webrtc::SdpType::kAnswer,?sdp);
????peer_connection_->SetRemoteDescription(
????????DummySetSessionDescriptionObserver::Create(),
????????session_description.release());
????return;
??}
??Json::StyledWriter?writer;
??Json::Value?jmessage;
??jmessage[kSessionDescriptionTypeName]?=
??????webrtc::SdpTypeToString(desc->GetType());
??jmessage[kSessionDescriptionSdpName]?=?sdp;
??SendMessage(writer.write(jmessage));
}
void?Conductor::SendMessage(const?std::string&?json_object)?{
??std::string*?msg?=?new?std::string(json_object);
??main_wnd_->QueueUIThreadCallback(SEND_MESSAGE_TO_PEER,?msg);
}
當(dāng) CreateOffer 成功時,首先調(diào)用 webrtc SetLocalDescription API 設(shè)置當(dāng)前的 SDP,
然后會將 offer sdp 發(fā)送給信令服務(wù)器,通過抓包,我們拿到了具體的 sdp 信息
POST /message?peer_id=13&to=12 HTTP/1.0\r\n
Content-Length: 5608\r\n
Content-Type: text/plain\r\n
\r\n
{\n
"sdp" : "v=0\r\no=- 6269511735434714595 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0 1\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS stream_id\r\nm=audio 9 UDP/TLS/RTP/SAVPF 63 111 103 104 9 0 8 106 105 13 110 1
"type" : "offer"\n
}\n
這是 peer_id=13 發(fā)送給 12 的 offer 信令,對應(yīng)的響應(yīng)如下:
HTTP/1.1 200 OK\r\n
Server: PeerConnectionTestServer/0.1\r\n
Cache-Control: no-cache\r\n
Connection: close\r\n
Content-Type: text/plain\r\n
Content-Length: 0\r\n
Access-Control-Allow-Origin: *\r\n
Access-Control-Allow-Credentials: true\r\n
Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n
Access-Control-Allow-Headers: Content-Type, Content-Length, Connection, Cache-Control\r\n
Access-Control-Expose-Headers: Content-Length\r\n
\r\n
服務(wù)端通過轉(zhuǎn)發(fā)給另一個 peer wait 的 offer 響應(yīng)
HTTP/1.1 200 OK\r\n
Server: PeerConnectionTestServer/0.1\r\n
Cache-Control: no-cache\r\n
Connection: close\r\n
Content-Type: text/plain\r\n
Content-Length: 5608\r\n
Pragma: 13\r\n
Access-Control-Allow-Origin: *\r\n
Access-Control-Allow-Credentials: true\r\n
Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n
Access-Control-Allow-Headers: Content-Type, Content-Length, Connection, Cache-Control\r\n
Access-Control-Expose-Headers: Content-Length\r\n
\r\n
{\n
"sdp" : "v=0\r\no=- 6269511735434714595 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0 1\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS stream_id\r\nm=audio 9 UDP/TLS/RTP/SAVPF 63 111 103 104 9 0 8 106 105 13 110 1
"type" : "offer"\n
}\n
另一方收到 offer 響應(yīng)后,會執(zhí)行剛剛我們分析的 OnMessageFromPeer 函數(shù)
void?Conductor::OnMessageFromPeer(int?peer_id,?const?std::string&?message)?{
??RTC_DCHECK(peer_id_?==?peer_id?||?peer_id_?==?-1);
??RTC_DCHECK(!message.empty());
??/*此時被動peer還沒有創(chuàng)建PeerConnection對象*/
??if?(!peer_connection_.get())?{
????RTC_DCHECK(peer_id_?==?-1);
????peer_id_?=?peer_id;
????/*創(chuàng)建PeerConnection對象*/
????if?(!InitializePeerConnection())?{
??????RTC_LOG(LS_ERROR)?<"Failed?to?initialize?our?PeerConnection?instance";
??????client_->SignOut();
??????return;
????}
??}?else?if?(peer_id?!=?peer_id_)?{
????RTC_DCHECK(peer_id_?!=?-1);
????RTC_LOG(LS_WARNING)
????????<"Received?a?message?from?unknown?peer?while?already?in?a?"
???????????"conversation?with?a?different?peer.";
????return;
??}
??/*將收到的消息解析成json對象*/
??Json::Reader?reader;
??Json::Value?jmessage;
??if?(!reader.parse(message,?jmessage))?{
????RTC_LOG(LS_WARNING)?<"Received?unknown?message.?"?<????return;
??}
??std::string?type_str;
??std::string?json_object;
??/*從json消息中解析出消息的類型*/
??rtc::GetStringFromJsonObject(jmessage,?kSessionDescriptionTypeName,
???????????????????????????????&type_str);
??if?(!type_str.empty())?{
????if?(type_str?==?"offer-loopback")?{
??????//?This?is?a?loopback?call.
??????//?Recreate?the?peerconnection?with?DTLS?disabled.
??????if?(!ReinitializePeerConnectionForLoopback())?{
????????RTC_LOG(LS_ERROR)?<"Failed?to?initialize?our?PeerConnection?instance";
????????DeletePeerConnection();
????????client_->SignOut();
??????}
??????return;
????}
????/*獲取消息的類型*/
????absl::optional?type_maybe?=
????????webrtc::SdpTypeFromString(type_str);
????if?(!type_maybe)?{
??????RTC_LOG(LS_ERROR)?<"Unknown?SDP?type:?"?<??????return;
????}
????/*從json消息中獲取sdp,此處為offer。*/
????webrtc::SdpType?type?=?*type_maybe;
????std::string?sdp;
????if?(!rtc::GetStringFromJsonObject(jmessage,?kSessionDescriptionSdpName,
??????????????????????????????????????&sdp))?{
??????RTC_LOG(LS_WARNING)
??????????<"Can't?parse?received?session?description?message.";
??????return;
????}
????/*將offer轉(zhuǎn)成webrtc可以理解的對象*/
????webrtc::SdpParseError?error;
????std::unique_ptr?session_description?=
????????webrtc::CreateSessionDescription(type,?sdp,?&error);
????if?(!session_description)?{
??????RTC_LOG(LS_WARNING)
??????????<"Can't?parse?received?session?description?message.?"
?????????????"SdpParseError?was:?"
??????????<??????return;
????}
????RTC_LOG(LS_INFO)?<"?Received?session?description?:"?<????/*將offer通過SetRemoteDescription設(shè)置到PeerConnection中*/
????peer_connection_->SetRemoteDescription(
????????DummySetSessionDescriptionObserver::Create(),
????????session_description.release());
????/*收到了對端的offer,本端需要產(chǎn)生answer。*/
????if?(type?==?webrtc::SdpType::kOffer)?{
??????peer_connection_->CreateAnswer(
??????????this,?webrtc::PeerConnectionInterface::RTCOfferAnswerOptions());
????}
??}?else?{?//處理?candidate?消息
????std::string?sdp_mid;
????int?sdp_mlineindex?=?0;
????std::string?sdp;
????if?(!rtc::GetStringFromJsonObject(jmessage,?kCandidateSdpMidName,
??????????????????????????????????????&sdp_mid)?||
????????!rtc::GetIntFromJsonObject(jmessage,?kCandidateSdpMlineIndexName,
???????????????????????????????????&sdp_mlineindex)?||
????????!rtc::GetStringFromJsonObject(jmessage,?kCandidateSdpName,?&sdp))?{
??????RTC_LOG(LS_WARNING)?<"Can't?parse?received?message.";
??????return;
????}
????webrtc::SdpParseError?error;
????std::unique_ptr?candidate(
????????webrtc::CreateIceCandidate(sdp_mid,?sdp_mlineindex,?sdp,?&error)) ;
????if?(!candidate.get())?{
??????RTC_LOG(LS_WARNING)?<"Can't?parse?received?candidate?message.?"
?????????????????????????????"SdpParseError?was:?"
??????????????????????????<??????return;
????}
????if?(!peer_connection_->AddIceCandidate(candidate.get()))?{
??????RTC_LOG(LS_WARNING)?<"Failed?to?apply?the?received?candidate";
??????return;
????}
????RTC_LOG(LS_INFO)?<"?Received?candidate?:"?<??}
}
這一段代碼較長,其實就3個意思
- 實例化 PeerConnectionFactoy 和 PeerConnectionClient
- 設(shè)置遠端的 SDP,并 CreateAnswer
- 收到對方發(fā)來的 candidate 消息,并添加到 PeerConnectionClient 中
上面第二點中的 CreateAnswer 創(chuàng)建成功后,也會想 CreateOffer 一樣,有成功的回調(diào),然后再發(fā)送給對方,這里就不再過多描述了。后面我們再看一下 candidate 消息
當(dāng) CreateOffer 、CreateAnswer 后,WebRTC 會通過 OnIceCandidate 回調(diào)信息將一些候選者的信息通知給我們
void?Conductor::OnIceCandidate(const?webrtc::IceCandidateInterface*?candidate)?{
??RTC_LOG(LS_INFO)?<"?"?<sdp_mline_index();
??//?For?loopback?test.?To?save?some?connecting?delay.
??if?(loopback_)?{
????if?(!peer_connection_->AddIceCandidate(candidate))?{
??????RTC_LOG(LS_WARNING)?<"Failed?to?apply?the?received?candidate";
????}
????return;
??}
??Json::StyledWriter?writer;
??Json::Value?jmessage;
??jmessage[kCandidateSdpMidName]?=?candidate->sdp_mid();
??jmessage[kCandidateSdpMlineIndexName]?=?candidate->sdp_mline_index();
??std::string?sdp;
??if?(!candidate->ToString(&sdp))?{
????RTC_LOG(LS_ERROR)?<"Failed?to?serialize?candidate";
????return;
??}
??jmessage[kCandidateSdpName]?=?sdp;
??SendMessage(writer.write(jmessage));
}
這里主要是將 webrtc ice 中收集到的 candidate 組裝成 json 然后發(fā)送給信令服務(wù)器,服務(wù)器再轉(zhuǎn)發(fā)給另一端
????POST?/message?peer_id=13&to=12?HTTP/1.0\r\n
????Content-Length:?186\r\n
????Content-Type:?text/plain\r\n
????\r\n
????{\n
???????"candidate"?:?"candidate:1019731727?1?udp?2122260223?192.168.1.104?53072?typ?host?generation?0?ufrag?IEDW?network-id?3?network-cost?10",\n
???????"sdpMLineIndex"?:?0,\n
???????"sdpMid"?:?"0"\n
????}\n
通過抓包得到了如上 candidate 消息,注意 candidate 會存在多個消息,雙方收到后并添加到 PeerConnectionClient 中,如果網(wǎng)絡(luò)協(xié)商成功,那么就可以進行采集->編碼->傳輸了。
最后一個信令是 退出信令 ,當(dāng)關(guān)閉窗口時,發(fā)送如下格式的信令
request:
GET /sign_out?peer_id=13 HTTP/1.0\r\n
response:
HTTP/1.1 200 OK\r\n
Server: PeerConnectionTestServer/0.1\r\n
Cache-Control: no-cache\r\n
Connection: close\r\n
Content-Type: text/plain\r\n
Content-Length: 0\r\n
Access-Control-Allow-Origin: *\r\n
Access-Control-Allow-Credentials: true\r\n
Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n
Access-Control-Allow-Headers: Content-Type, Content-Length, Connection, Cache-Control\r\n
Access-Control-Expose-Headers: Content-Length\r\n
\r\n
到此,所有信令就分析完了,建議大家可以通過抓包去分析對應(yīng)的流程。
媒體流處理
當(dāng)媒體協(xié)商,網(wǎng)絡(luò)協(xié)商完成后,就能進行等待收對方發(fā)過來的音視頻流了,當(dāng)有新軌道產(chǎn)生,會執(zhí)行 OnAddTrack 回調(diào)
void?Conductor::OnAddTrack(
????rtc::scoped_refptr?receiver,
????const?std::vector>&
????????streams) ?{
??RTC_LOG(LS_INFO)?<"?"?<id();
??main_wnd_->QueueUIThreadCallback(NEW_TRACK_ADDED,
???????????????????????????????????receiver->track().release());
}
經(jīng)過一系列的線程切換,最后會執(zhí)行到如下代碼:
void?Conductor::UIThreadCallback(int?msg_id,?void*?data)?
{
...
????case?NEW_TRACK_ADDED:?{
??????auto*?track?=?reinterpret_cast(data);
??????if?(track->kind()?==?webrtc::MediaStreamTrackInterface::kVideoKind)?{
????????/*獲取遠端video?track*/
????????auto*?video_track?=?static_cast(track);
????????/*送至MainWnd處理*/
????????main_wnd_->StartRemoteRenderer(video_track);
??????}
??????track->Release();
??????break;
????}
...
}
void?MainWnd::StartRemoteRenderer(webrtc::VideoTrackInterface*?remote_video)?
{
??/*生成遠端視頻渲染器,同時將遠端視頻渲染器注冊到webrtc中。*/
??remote_renderer_.reset(new?VideoRenderer(handle(),?1,?1,?remote_video));
}
最后,當(dāng)有視頻幀產(chǎn)生時,會通過 OnFrame 回調(diào)給 ViewRenderer (其實 WebRTC 的接口設(shè)計在各平臺上基本上一致的。前面我們分析 Android 視頻渲染或者采集,也都是通過 OnFrame 虛函數(shù)給回調(diào)的)
//?OnFrame方法,當(dāng)接收到新的視頻幀時被調(diào)用
void?MainWnd::VideoRenderer::OnFrame(const?webrtc::VideoFrame&?video_frame)?{
??//?用AutoLock確保同一時刻只有一個線程可以訪問此方法
??{
????AutoLock?lock(this) ;
????//?獲取視頻幀的I420格式的緩沖區(qū)
????rtc::scoped_refptr?buffer(
????????video_frame.video_frame_buffer()->ToI420()) ;
????//?如果視頻幀的旋轉(zhuǎn)角度不為0,則將視頻幀旋轉(zhuǎn)至指定角度
????if?(video_frame.rotation()?!=?webrtc::kVideoRotation_0)?{
??????buffer?=?webrtc::I420Buffer::Rotate(*buffer,?video_frame.rotation());
????}
????//?設(shè)置視頻幀的寬度和高度
????SetSize(buffer->width(),?buffer->height());
????//?確保image_已經(jīng)被初始化
????RTC_DCHECK(image_.get()?!=?NULL);
????//?將I420格式的圖像數(shù)據(jù)轉(zhuǎn)換為ARGB格式,然后存儲到image_中
????libyuv::I420ToARGB(buffer->DataY(),?buffer->StrideY(),?buffer->DataU(),
???????????????????????buffer->StrideU(),?buffer->DataV(),?buffer->StrideV(),
???????????????????????image_.get(),
???????????????????????bmi_.bmiHeader.biWidth?*?bmi_.bmiHeader.biBitCount?/?8,
???????????????????????buffer->width(),?buffer->height());
??}
??//?使窗口重繪
??InvalidateRect(wnd_,?NULL,?TRUE);
}
這個方法是WebRTC在接收到新的視頻幀時的處理過程。它首先獲取視頻幀的I420格式的緩沖區(qū),然后檢查視頻幀是否需要旋轉(zhuǎn),如果需要就進行旋轉(zhuǎn)。接著設(shè)置視頻幀的寬度和高度,然后將I420格式的圖像數(shù)據(jù)轉(zhuǎn)換為ARGB格式,并存儲在image_中。最后,通過調(diào)用InvalidateRect函數(shù)使窗口無效,這會觸發(fā)窗口的重繪事件,即顯示新的視頻幀。
接下來窗口會收到 WM_PAINT 消息,標識即需要重新繪制窗口
//?OnPaint方法,當(dāng)窗口需要重繪時被調(diào)用
void?MainWnd::OnPaint()?{
??PAINTSTRUCT?ps;
??//?開始繪制
??::BeginPaint(handle(),?&ps);
??RECT?rc;
??//?獲取窗口客戶區(qū)的大小
??::GetClientRect(handle(),?&rc);
??VideoRenderer*?local_renderer?=?local_renderer_.get();
??VideoRenderer*?remote_renderer?=?remote_renderer_.get();
??//?如果正在進行流媒體播放并且本地和遠程渲染器都存在
??if?(ui_?==?STREAMING?&&?remote_renderer?&&?local_renderer)?{
????//?使用AutoLock確保同一時刻只有一個線程可以訪問這些渲染器
????AutoLock?local_lock(local_renderer) ;
????AutoLock?remote_lock(remote_renderer) ;
????//?獲取遠程渲染器的視頻信息
????const?BITMAPINFO&?bmi?=?remote_renderer->bmi();
????int?height?=?abs(bmi.bmiHeader.biHeight);
????int?width?=?bmi.bmiHeader.biWidth;
????//?獲取遠程渲染器的視頻圖像
????const?uint8_t*?image?=?remote_renderer->image();
????//?如果圖像存在,開始進行繪制
????if?(image?!=?NULL)?{
??????//?創(chuàng)建一個設(shè)備上下文與ps.hdc兼容的內(nèi)存設(shè)備上下文
??????HDC?dc_mem?=?::CreateCompatibleDC(ps.hdc);
??????//?設(shè)置位圖拉伸模式為HALFTONE
??????::SetStretchBltMode(dc_mem,?HALFTONE);
??????//?設(shè)置映射模式以保持寬高比
??????HDC?all_dc[]?=?{ps.hdc,?dc_mem};
??????for?(size_t?i?=?0;?i?????????SetMapMode(all_dc[i],?MM_ISOTROPIC);
????????SetWindowExtEx(all_dc[i],?width,?height,?NULL);
????????SetViewportExtEx(all_dc[i],?rc.right,?rc.bottom,?NULL);
??????}
??????//?創(chuàng)建一個與ps.hdc兼容的位圖
??????HBITMAP?bmp_mem?=?::CreateCompatibleBitmap(ps.hdc,?rc.right,?rc.bottom);
??????//?將新位圖選入內(nèi)存設(shè)備上下文,同時保留舊的位圖
??????HGDIOBJ?bmp_old?=?::SelectObject(dc_mem,?bmp_mem);
??????//?將設(shè)備上下文坐標轉(zhuǎn)換為邏輯坐標
??????POINT?logical_area?=?{rc.right,?rc.bottom};
??????DPtoLP(ps.hdc,?&logical_area,?1);
??????//?創(chuàng)建一個黑色的畫刷并填充矩形
??????HBRUSH?brush?=?::CreateSolidBrush(RGB(0,?0,?0));
??????RECT?logical_rect?=?{0,?0,?logical_area.x,?logical_area.y};
??????::FillRect(dc_mem,?&logical_rect,?brush);
??????//?刪除創(chuàng)建的畫刷
??????::DeleteObject(brush);
??????//?計算繪制圖像的起始位置,以使圖
??????//?計算繪制圖像的起始位置,以使圖像位于中心
??????int?x?=?(logical_area.x?/?2)?-?(width?/?2);
??????int?y?=?(logical_area.y?/?2)?-?(height?/?2);
??????//?使用StretchDIBits函數(shù)將視頻幀圖像畫到內(nèi)存設(shè)備上下文
??????StretchDIBits(dc_mem,?x,?y,?width,?height,?0,?0,?width,?height,?image,
????????????????????&bmi,?DIB_RGB_COLORS,?SRCCOPY);
??????//?如果窗口足夠大,就在右下角畫一個本地視頻流的縮略圖
??????if?((rc.right?-?rc.left)?>?200?&&?(rc.bottom?-?rc.top)?>?200)?{
????????const?BITMAPINFO&?bmi?=?local_renderer->bmi();
????????image?=?local_renderer->image();
????????int?thumb_width?=?bmi.bmiHeader.biWidth?/?4;
????????int?thumb_height?=?abs(bmi.bmiHeader.biHeight)?/?4;
????????StretchDIBits(dc_mem,?logical_area.x?-?thumb_width?-?10,
??????????????????????logical_area.y?-?thumb_height?-?10,?thumb_width,
??????????????????????thumb_height,?0,?0,?bmi.bmiHeader.biWidth,
??????????????????????-bmi.bmiHeader.biHeight,?image,?&bmi,?DIB_RGB_COLORS,
??????????????????????SRCCOPY);
??????}
??????//?使用BitBlt函數(shù)將內(nèi)存設(shè)備上下文的內(nèi)容復(fù)制到屏幕設(shè)備上下文
??????BitBlt(ps.hdc,?0,?0,?logical_area.x,?logical_area.y,?dc_mem,?0,?0,
?????????????SRCCOPY);
??????//?清理創(chuàng)建的對象
??????::SelectObject(dc_mem,?bmp_old);
??????::DeleteObject(bmp_mem);
??????::DeleteDC(dc_mem);
????}?else?{
??????//?如果還沒有接收到視頻流,就填充黑色背景,并繪制提示文本
??????HBRUSH?brush?=?::CreateSolidBrush(RGB(0,?0,?0));
??????::FillRect(ps.hdc,?&rc,?brush);
??????::DeleteObject(brush);
??????//?設(shè)置字體、文本顏色和背景模式,然后繪制提示文本
??????HGDIOBJ?old_font?=?::SelectObject(ps.hdc,?GetDefaultFont());
??????::SetTextColor(ps.hdc,?RGB(0xff,?0xff,?0xff));
??????::SetBkMode(ps.hdc,?TRANSPARENT);
??????std::string?text(kConnecting);
??????if?(!local_renderer->image())?{
????????text?+=?kNoVideoStreams;
??????}?else?{
????????text?+=?kNoIncomingStream;
??????}
??????::DrawTextA(ps.hdc,?text.c_str(),?-1,?&rc,
??????????????????DT_SINGLELINE?|?DT_CENTER?|?DT_VCENTER);
??????::SelectObject(ps.hdc,?old_font);
????}
??}?else?{
????//?如果不在流媒體播放狀態(tài),就填充白色背景
????HBRUSH?brush?=?::CreateSolidBrush(::GetSysColor(COLOR_WINDOW));
????::FillRect(ps.hdc,?&rc,?brush);
????::DeleteObject(brush);
??}
??//?結(jié)束繪制
??::EndPaint(handle(),?&ps);
}
代碼有點長,這里做一下總結(jié):
- 如果正在播放流媒體且本地和遠程渲染器都存在,則繪制遠程視頻流。如果窗口足夠大,就在右下角繪制本地視頻流的縮略圖。
- 如果還沒有接收到視頻流,則在黑色背景上顯示提示信息。
- 如果不在播放流媒體的狀態(tài),則只填充窗口的背景。
到此,對端視頻可以正常的顯示出來了。
總結(jié)
該篇文章詳細的分析了 peerconnection_client 客戶端的窗口交互、信令交互、和視頻渲染等處理,篇幅較長,建議自己先 debug peerconnection_client demo, 如流程上有不懂的再來看該篇對應(yīng)的處理講解。下一篇文章會進行 Windows P2P 的實戰(zhàn)開發(fā),與之前的 Web 和 Android 可以進行音視頻通話。
