<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          WebRTC 源碼分析 (三) Windows P2P 音視頻通話 peerconnection_...

          共 20358字,需瀏覽 41分鐘

           ·

          2023-06-20 10:27

          介紹

          環(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è)置為啟動項,如下圖所示

          0fcbe62c4ab547cc2bec58d7480f87de.webpimg_v2_67fbd162-8eeb-4f1b-9c18-e1b61936d83g

          啟動通話后,效果如下:

          53db63e071cb7b369c6e676965d13b87.webpimg_v2_77dfb6a9-6c95-4ef0-9983-18b6e03692cg

          如果你是在本地開 2 個客戶端調(diào)試,那么可以通過開啟 OBS 的虛擬攝像頭達到上面的效果。

          peerconnection_client 主要是由以下幾個部分構(gòu)成

          28dd63151bc141f098aefd49136bcdee.webppeerconnection_client UML (1)
          1. main.cc: 這是程序的入口點,它創(chuàng)建并運行應(yīng)用程序的消息循環(huán),初始化并運行主窗口。它會創(chuàng)建 PeerConnectionClientConductor 對象,并且鏈接他們,使得它們能一起協(xié)作。
          2. main_wnd.cc: 它是主窗口類的實現(xiàn)。這個類負責(zé)所有的用戶界面操作,如按鈕點擊、視頻顯示窗口、狀態(tài)更新等。它還將用戶操作的事件通知到 Conductor 對象。
          3. peer_connection_client.cc: 這個類是一個客戶端,它會連接到 PeerConnectionServer 信令服務(wù)器,然后向服務(wù)器注冊,并處理來自服務(wù)器的信令消息,以及發(fā)送到服務(wù)器的信令消息。
          4. conductor.cc: 它是整個程序的核心,負責(zé)管理 PeerConnectionClient 對象和 MainWnd 對象。它還創(chuàng)建并管理WebRTC的 PeerConnection 對象,以及處理所有的WebRTC事件。例如,當(dāng)用戶點擊"用戶列表 item"時,MainWnd 對象會將此事件通知給 ConductorConductor 會命令 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);
          }

          最后窗口會這樣顯示:fcdddaed9fdf14582b050ba529e1ef14.webp

          當(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)的用戶列表就可以顯示出來,比如, 如下所示:

          ab3185e46eefe7d3980dcd18d3921f17.webpimg_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 的信令交互

          信令處理

          5a682a03b0ec64f050222fc729119d0c.webpimage-20230618154102953

          下載 pcapng 包鏈接: https://pan.baidu.com/s/1wGyyLSxd7_X2p7T8O1nPdg?pwd=frrr 提取碼: frrr

          通過抓包我們得到了如下幾個信令:

          GET sign_in: 用戶登錄消息

          **GET sign_out:**用戶退出消息

          POST message: 協(xié)商交互消息

          GET wait: 用戶等待消息

          這里繪制了一張簡要的時序圖

          bd3259aea2ba3133b1b706c738ca7821.webpPeerConnection_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個意思

          1. 實例化 PeerConnectionFactoy 和 PeerConnectionClient
          2. 設(shè)置遠端的 SDP,并 CreateAnswer
          3. 收到對方發(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é):

          1. 如果正在播放流媒體且本地和遠程渲染器都存在,則繪制遠程視頻流。如果窗口足夠大,就在右下角繪制本地視頻流的縮略圖。
          2. 如果還沒有接收到視頻流,則在黑色背景上顯示提示信息。
          3. 如果不在播放流媒體的狀態(tài),則只填充窗口的背景。

          到此,對端視頻可以正常的顯示出來了。

          總結(jié)

          該篇文章詳細的分析了 peerconnection_client 客戶端的窗口交互、信令交互、和視頻渲染等處理,篇幅較長,建議自己先 debug peerconnection_client demo, 如流程上有不懂的再來看該篇對應(yīng)的處理講解。下一篇文章會進行 Windows P2P 的實戰(zhàn)開發(fā),與之前的 Web 和 Android 可以進行音視頻通話。


          瀏覽 73
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  91久久久久久久久久免费视频 | 国产性爱自拍视频 | 圆产精品久久久久久久久久久新郎 | 亚洲精品人妻无码 | 亚州在线无码视频 |