WebRTC源碼分析——視頻流水線創建(上)

1. 引言

常見的音視頻會話中,一端將本地的音視頻數據傳輸給對端將至少經歷3個步驟:採集->編碼->傳輸,將數據從採集模塊到發送模塊的流動稱爲音視頻數據的流水線。接下來幾篇文章中將以視頻數據爲原本討WebRTC是如何創建此視頻流水線的:數據如何採集,如何從採集模塊一步步流向網絡發送模塊,最終傳輸出去的。linux

2. 採集

視頻採集模塊是數據流水線的起始點,負責從視頻源採集原始視頻幀,推送給流水線的下一站:能夠是本地渲染模塊進行本地回顯,也能夠是編碼模塊進行數據編碼壓縮。android

視頻源能夠是攝像頭,也能夠是桌面、窗口抓屏(遠程桌面,基於視頻流的電子白板等應用),甚至能夠是磁盤上的視頻文件,圖片文件。WebRTC中提供了基於攝像頭的視頻採集框架,是本文要討論的重點。固然WebRTC也提供了桌面,窗口抓屏框架,這套框架對外所提供的接口與基於攝像頭的採集接口有所不一樣。整個視頻流水線創建是以攝像頭採集接口爲基礎的,從而致使這麼個問題:當須要將抓屏數據當作視頻源往外推送時,須要使用適配器模式來實現一套基於攝像頭的視頻採集接口。在基於視頻流的互動白板中曾如此實現過,但不是本文討論重點,所以,將放在別的文章中進行闡述。windows

視頻採集模塊是平臺相關的模塊,MacOS/IOS通常使用AVFoundation框架或者QuikTime框架,Linux平臺通常使用V4L2庫,Android上通常使用Camera1或者Camera2框架,Windows平臺則使用DS(DirectShow)或者是MF(MediaFoundation)。因爲WebRTC是個很是活躍的工程,代碼架構一直在不停的變更之中,好比2019年4月份的代碼還有VideoCaptureMF的代碼,而且還註釋着Vista及以上的版本建議使用MediaFoundation採集框架,而2019年11月份的代碼MediaFoundation相關的代碼已經被移除。再好比MacOs/IOS,Android的相關代碼已經被移動到sdk/objc和sdk/android目錄下。本文以modules/video_capture下的代碼來作闡述,平臺無關的代碼在該直接目錄下,平臺相關的實如今modules/video_capture/windows,modules/video_capture/linux目錄下,如圖所示:數組

1.png

2.1 視頻採集相關UML

2.png

DeviceInfo接口提供了設備枚舉相關功能,其平臺相關子類實例以組合的形式提供給VideoCapture。微信

  • 枚舉設備個數,獲取某個設備名稱。
  • 枚舉某個設備所支持的全部能力(VideoCaptureCapability: 分辨率,最大幀率,顏色空間,是否逐行掃描)
  • 獲取某個設備的全部能力中與外部設置的能力最匹配的那個能力。

VideoCaptureModule視頻採集模塊的虛基類,它定義一系列視頻採集的通用接口函數:網絡

  • Start/StopCapture用來開始/結束視頻採集(平臺相關);
  • CaptureStarted用來判斷當前capture運行狀態(平臺相關);
  • Register/DeCaptureDataCallback用來註冊/註銷數據回調模塊(平臺無關);
  • Set/GetApplyRotation用來設置視頻旋轉角度(平臺無關)。

VideoCaptureImpl類是VideoCaptureModule的實現子類。作了3個事:架構

  • 聲明靜態Ctreate方法,用於建立平臺相關的VideoCaptureImpl子類,在Windows平臺上爲VideoCaptureDS,在Linux平臺上實現的子類是VideoCaptureV4L2。該方法一處聲明,多處實現,在相應平臺編譯時,只會加載對應平臺的實現代碼;
  • 平臺相關的接口,留待平臺相關的子類中實現,主要是開始/結束視頻採集;
  • 實現平臺無關的接口:註冊視頻數據回調,應用視頻旋轉相關函數。其中註冊數據回調將一個實現了VideoSinkInterface<VideoFrame>接口的對象賦予VideoCaptureImpl::_dataCallBack成員。當採集模塊獲得一幀視頻數據,就能夠經過該對象的OnFrame()方法推送出來。

2.2 採集模塊的內部數據流

1. 以VideoCaptureDS爲例,平臺相關的採集模塊採集到一幀視頻後,平臺相關的函數ProcessCapturedFrame()方法進行處理。ProcessCapturedFrame()將視頻幀直接傳遞給VideoCaptureImpl::IncomingFrame()方法框架

2. VideoCaptureImpl::IncomingFrame()方法將對視頻幀按需求進行旋轉,並利用libyuv庫轉換成I420類型,再給視頻幀加上ntp時間戳。通過上述處理後,IncomingFrame()將視頻幀進一步傳遞給VideoCaptureImpl::DeliverCapturedFrame()運維

3. VideoCaptureImpl::DeliverCapturedFrame()將調用VideoSinkInterface::OnFrame(),將視頻幀傳遞給回調對象_dataCallBack,即數據的下一站,從而將視頻幀推送出採集模塊。ide

3 流水線創建

視頻採集模塊做爲底層模塊,須要和上層模塊協做才能把採集到的視頻數據發送到上層的顯示和編碼模塊,爲數據流水線提供源源不斷的視頻數據。從控制流來說,視頻採集模塊在初始化階段由上層模塊進行建立並開啓視頻採集,在結束的時候由上層模塊中止視頻採集並銷燬模塊。從數據流來說,採集到的視頻數據經過回調接口傳遞到上層模塊,進行數據流水線上的下一步處理。

3.1 VideoCapture->VideoTrack的流水線

不論視頻流最終目的地是流向本地渲染模塊仍是要流向編碼器,首先都要通過VideoTrack這個對象。從控制流上來說:一個VideoTrack對象的建立過程就是VideoCapture->VideoTrack流水線創建過程:

3.png

從數據流來說:而視頻數據流動方向正好和建立的方向相反:

image.png

相關的類圖以下:

5.png

1. VideoCaptureModule->VideoSource

VideoCaptureModule做爲數據源頭組合到VideoSource對象中,同時VideoSource又實現了VideoSinkInterface<VideoFrame>接口,能夠把本身註冊到VideoCaptureModule中。從而實現了視頻幀從VideoCaptureModule->VideoSource的流動。

VideoSource持有一個很是重要的成員VideoBroadcaster對象,該對象的UML類圖以下。
6.png

一方面VideoBroadcaster實現了VideoSinkInterface接口,成爲一個Sink,這樣VideoSource獲得採集模塊的視頻幀後,首先會流入到內部的VideoBroadcaster成員對象,而非直接從VideoSource流出;另外一方面VideoSource和VideoBroadcaster都實現了VideoSourceInterface接口,對外VideoSource做爲視頻源存在,向數據流下一站提供註冊方法AddOrUpdateSink();該方法內部調用VideoBroadcaster的AddOrUpdateSink(),從而將數據流下一站VideoSink註冊到VideoBroadcaster,存入成員std::vector<SinkPair> sinks_中。到此,應該不難想到VideoBroadcaster既有了數據流入,還知道數據的下一站(可能多個),那麼VideoBroadcaster::OnFrame()中就能夠經過循環調用下一站的OnFrame方法將視頻幀廣播出去。

爲何要如此設計?由於,在WebRTC 1.0的官方規範中說明了一個視頻源是能夠被多個視頻軌共用的。經過上述方式能夠實現共用的概念。

2. VideoSource->VideoTrackSource

VideoTrackSource沒有實現VideoSinkInterface接口,所以,實質上視頻數據是不會流入到VideoTrackSource中的,但其組合了VideoSource對象,而且實現了VideoSourceInterface接口,添加到VideoTrackSource中的VideoSink會被添加到VideoSource,而後進一步添加到VideoBroadcast中。對外部來講,VideoTrackSource就是視頻源。

VideoTrackSource另外實現了視頻源狀態相關的接口,以及狀態通告相關的接口NotifierInterface,用於向更高一層(VideoTrack)通告視頻源的狀態。因爲與數據流的討論無關,此處只說起,不詳述。

3. VideoTrackSource->VideoTrack

如同VideoTrackSource通常,VideoTrack也沒有實現VideoSinkInterface接口,所以,視頻數據也不會流入到VideoTrack中,但其組合了VideoTrackSource,而且間接實現了VideoSourceInterface接口。想要從VideoTrack中獲取視頻流的站點,只要實現VideoSinkInterface接口,經過VideoTrack的AddOrUpdateSink()註冊進來便可,由於該VideoSink會通過VideoTrackSource->VideoSource->VideoBroadcaster,最終能夠從VideoBroadcaster得到視頻流。

VideoTrack另外實現了ObserverInterface接口,用於以觀察者的身份來接收響應VideoTrackSource關於視頻源狀態的報告。

VideoTrack還實現了VideoTrackInterface接口,其中提供了一個重要的屬性:ContentHint。這個屬性告知編碼器在碼率下降時,應該如何應對:下降幀率?下降分辨率?對於桌面採集應用來講,咱們應該設置該屬性爲kDetailed或者是kText,這樣編碼器編碼該視頻流的時候不會下降分辨率,量化參數qp值也不會設置的過大。

3.2 VideoTrack到本地渲染

從以前的描述,咱們很清楚的知道視頻幀是如何流動到VideoTrack的(雖然實質上並無流動到VideoTrack類),咱們也知道該如何從VideoTrack中獲取視頻數據:1)實現VideoSinkInterface接口,2)經過VideoTrack的AddOrUpdateSink()註冊進去便可。事實上,本地渲染就是如此作的:要麼直接使用WebRTC提供的平臺相關的渲染類,這些類都實現了VideoSinkInterface接口;要麼能夠本身實現Renderer類,並實現VideoSinkInterface接口,在OnFrame方法中獲取視頻幀,並進行渲染操做。render經過VideoTrack的AddOrUpdateSink()註冊進去時,會一直被投遞到VideoBroadcaster被其持有,從VideoBroadcaster處直接獲得視頻幀。

WebRTC中提供的渲染類相關的UML類圖:

7.png

3.3 VideoTrack到編碼器

要說清楚VideoTrack中的視頻幀如何到達編碼器的,首要問題是搞清楚在WebRTC中哪一個類表明了編碼器,這纔好研究視頻數據的流向。

在WebRTC中VideoStreamEncoder類表徵着一個視頻編碼器,接收原始視頻幀做爲輸入,產生編碼後的比特流做爲輸出。該類位於src/video/video_stream_encoder.h中,以下截圖爲該類的說明:

8.png

搞清楚了目的地後,接下來就是分析視頻流如何從VideoTrack一步步流向VideoStreamEncoder,這條流水線又是如何創建起來的。

從數據流來說,數據從VideoTrack->VideoStreamEncoder過程當中大概經歷了這麼幾個對象:
9.png

這幾個對象的UML類圖及其關係以下所示:按照以前的分析,咱們知道要正真得到視頻幀,該類須要實現VideoSinkInterface接口,在OnFrame()在該方法中獲得上一站傳來的視頻幀。經過下面類圖,咱們能夠看到實質上只有VideoStreamEncoder是一個VideoSink對象。而VideoTrack經過以對象成員的方式一直被傳遞到VideoStreamEncoder。因爲VideoTrack實現了VideoSourceInterface,VideoStreamEncoder又能夠反向設置到VideoTrack中,根據以前的結論,VideoStreamEncoder最終會存儲在VideoBroadcaster中,由VideoBroadcaster將視頻幀直接傳遞給VideoStreamEncoder。

10.png

從控制流來說,若是不深刻研究細節,僅從WebRTC的外層API來看,經過PeerConnection->AddTrack();PeerConnection->CreateOffer();PeerConnection->SetLocalDescription()這三步就創建起了這條流水線。後續簡要分析這3個方法內部對創建上述視頻流水線作出的貢獻。

1. AddTrack()

在建立出VideoTrack後,經過PeerConnection->AddTrack()接口會爲每一個要發送的視頻Track建立一個VideoRtpSender對象,視頻Track成爲VideoRtpSender的成員,實現邏輯上視頻流向VideoTrack->VideoRtpSender流動。 另外,若是SDP使用kUnifiedPlan方式,還會爲每一個track建立一個獨立的

RtpTranceiver對象,組合包含該track的VideoRtpSender,並添加到PC的成員RtpTranceiver數組中。

VideoRtpSender對象有兩個重要的成員是與本文的討論相關的track_和media_channel_。分別就是VideoTrack和WebRtcVideoChannel對象,是視頻流的上一站和下一站。執行AddTrack()並不會將兩者關聯起來,只會將VideoTrack添加到VideoRtpSender中。但最終VideoRtpSender->SetSsrc()方法被調用時完成兩者綁定。

  • VideoRtpSender->SetSsrc()被調用的時機?
  • 若是SDP使用kUnifiedPlan方式,VideoRtpSender被建立時,media_channel_並無跟隨一塊兒被建立,那麼什麼時候何地media_channel_會被建立。

2. CreateOffer()

PeerConnection->CreateOffer()方法的詳細過程是很是複雜的,它收集本地的音視頻能力和網絡層傳輸能力造成SDP描述結構。雖然該方法沒有直接參與視頻流水線構建,可是其爲下一步PeerConnection->SetLocalDescription()操做提供了必要信息,使得其能完成視頻流水線的構建。

下面簡要分析PeerConnection->CreateOffer()的過程當中與視頻相關的部分,大體的調用過程以下:

11.png

圖中特殊標記有兩個函數:

PeerConnection::GetOptionsForUnifiedPlanOffer()會遍歷PC中全部的RtpTransceiver,爲每一個RtpTransceiver建立一個媒體描述信息對象MediaDescriptionOptions,在最終的生成的SDP對象中,一個MediaDescriptionOptions就是一個m-line。 根據因爲以前的分析,一個Track對應一個RtpTransceiver,實質上在SDP中一個track就會對應到一個m-line。上述遍歷造成全部媒體描述信息MediaDescriptionOptions會存入到MediaSessionOptions對象中,該對象在後續過程當中一路傳遞,最終在MediaSessionDescriptionFactory::CreateOffer()方法中被用來完成SDP建立。

另外MediaSessionDescriptionFactory::CreateOffer()建立SDP過程當中,會爲每一個媒體對象,即每一個track:audio、video、data建立對應的MediaContent。上圖右邊展現了爲視頻track建立VideoContent過程,標黃的靜態方法CreateStreamParamsForNewSenderWithSsrcs()會爲每一個RtpSender生成惟一的ssrc值。ssrc是個關鍵信息,正如以前分析,但須要說明的一點是此處並不會調用RtpSender->SetSsrc()方法,ssrc當前只存在於SDP信息中,等待SetLocalDescription()的解析。

3. SetLocalDescription()

在CreateOffer()成功的回調中,一方面,咱們會經過信令將Offer SDP發送給對端;另外一方面調用SetLocalDescription()進行本地設置操做。

SetLocalDescription()的大體步驟以下:

12.png

如上圖, SetLocalDescription()過程是至關複雜的,咱們抓住視頻流水線上關鍵節點的建立以及關聯過程來進行重點描述。重點函數在上圖中都標黃顯示。

流水線上對象的建立:

1) PeerConnection::UpdateTransceiverChannel()方法中檢查PC中的每一個RtpTranceiver是存在MediaChannel,不存在的會調用WebRtcVideoEngine::CreateMediaChannel()建立WebRtcVideoChannel對象,並賦值給RtpTranceiver的RtpSender和RtpReceiver,這兒解決了VideoRtpSender的media_channel_成員爲空的問題;

2) PeerConnection::UpdateSessionState()方法中,將SDP中的信息應用到上一步建立的視頻媒體通道對象WebRtcVideoChannel上,調用WebRtcVideoChannel::AddSendStream()方法爲通道建立WebRtcVideoSendStream,若是有多個視頻Track,會有多個WebRtcVideoSendStream分別與之對應。WebRtcVideoSendStream對象存入WebRtcVideoChannel的std::map<uint32_t, WebRtcVideoSendStream*> send_streams_成員,以ssrc爲key。建立WebRtcVideoSendStream,其構造函數中會進一步建立VideoSendStream,VideoSendStream的構造中會進一步建立

VideoStreamEncoder對象。 到此,全部有關的對象都已經建立完成。

流水線的創建:

以前就分析過VideoRtpSender->SetSsrc()方法很是重要,該方法在PeerConnection::ApplyLocalDescription()中最後被調用。會觸發Track被傳遞,從VideoRtpSender傳遞到WebRtcVideoChannel,再傳遞到WebRtcVideoSendStream,成爲WebRtcVideoSendStream的成員source_。 從而實現了邏輯上VideoRtpSender->WebRtcVideoChannel->WebRtcVideoSendStream流水線的創建;

WebRtcVideoSendStream::SetVideoSend()方法緊接着又觸發調用VideoSendStream的SetSource()方法,以WebRtcVideoSendStream爲視頻源參數(看以前的類圖,WebRtcVideoSendStream實現了VideoSourceInterface接口)一路傳遞給VideoStreamEncoder的成員VideoSourceProxy。在這個VideoSourceProxy::SetSource方法中,反向調用WebRtcVideoSendStream::AddOrUpdateSink()方法將VideoStreamEncoder做爲VideoSink(看以前的類圖,VideoStreamEncoder實現了VideoSinkInterface接口)添加到了WebRtcVideoSendStream。注意,在WebRtcVideoSendStream::AddOrUpdateSink()中會調用source_->AddOrUpdateSink()進一步將VideoStreamEncoder添加到了VideoTrack(如以前的描述VideoTrack已經被傳遞到WebRtcVideoSendStream成爲WebRtcVideoSendStream的成員source_)。在邏輯上實現了視頻流從WebRtcVideoSendStream->VideoSendStream->VideoStreamEncoder這段流水線。

至此,以發送端角度來看,從採集到編碼器的整個流水線都已創建完畢。

4 總結

1. 從WebRTC提供的API角度看,從CreateVideoTrack(),AddTrack(),CreateOffer(),SetLocalDescription()這四步就創建起了發端從採集到編碼器的視頻流水線。固然具體細節比較複雜。

2. 雖然涉及的類不少,實質上一個視頻幀從採集模塊開始,流向編碼器模塊並無通過太多的對象。接收數據的對象都實現了VideoSinkInterface接口,視頻幀就在這幾個對象的OnFrame方法中源源不斷流動。WebRTC中數據老是從Source向Sink流動。

end
                                                             

 

做者簡介

                                    黎意爲好將來高級C/C++工程Ⅲ

招聘信息

好將來技術團隊正在熱招測試、後臺、運維、客戶端等各個方向高級開發工程師崗位,你們可掃描下方二維碼或微信搜索關注「好將來技術」公衆號,點擊「技術招聘」欄目瞭解詳情,歡迎感興趣的夥伴加入咱們!

也許你還想看

淺析深度知識追蹤如何助力智能教育

輕量型TV端遙控器交互類庫最佳實踐

"考試"背後的科學:教育測量中的理論與模型(IRT篇)

用技術助力教育 | 一塊兒感覺榜樣的力量

想了解一個異地多校平臺的架構演進過程嗎?讓我來告訴你!

摩比秀換裝遊戲系統設計與實現(基於Egret+DragonBones龍骨動畫)

如何實現一個翻頁筆插件

產研人的疫情戰事,沒有一點兒的喘息

相關文章
相關標籤/搜索