五:數據的接收和發送 linux
一、 發送過程:算法
應用發送數據時調用接口 rtp_session_send_with_ts 完成。參數爲會話句柄,數據緩衝區地址,數據長度以及應用當前的時間戳。在該接口中,會先調用 rtp_session_create_packet 接口,根據緩衝區地址及數據長度,構造一個新的消息塊,並根據會話信息初始化 rtp 頭信息。完了將緩衝區中的數據拷貝到消息塊中。最後以消息塊爲參數,調用 rtp_session_sendm_with_ts 接口進行數據發送。rtp_session_sendm_with_ts調用更底層的函數 __rtp_session_sendm_with_ts,在該函數中完成具體的發送處理。下面具體分析該函數的實現:網絡
若是發送尚未啓動,也就是說當前是第一次啓動,則snd_ts_offset 變量首先被設置爲應用當前開始的值。若是啓動了調度,則snd_time_offset 設置爲調度器運行到如今的時間。這應該算是時間戳的初始化。session
若是調度被啓用了,則對部分時間戳作一些調整,以下:app
首先計算包應該發送的時間,就是packet time。計算方法爲在發送第一個數據包時的調度器時間加上包的發送間隔,這個間隔根據應用當前給的時間與第一次的發送給的時間的差 值除以payload 的時鐘速率計算獲得,好比第一次發送的時間爲 100,當前爲 300,也就是說發送通過了 200 個單位,若是 payload 的 clock rate 爲 10,則說明通過了 20 個時間戳單位, 也就是說當前包的時間戳爲調度器時間加 20。(packet time 實際上應該是將下一個包的發送時間轉換爲調度器時間,交給調度器讓調度器來調度)若是計算的packet time與調度器當前的運行時間的差值小於2 的 31 此方,而且兩者不相等,則設置該等待點在 packet time喚醒。(關於該比較,參見其餘說明部分)socket
在發送數據前,RTP的時間戳設置爲應用傳進來的當前的時間戳。snd_last_ts時間戳也 設置爲應用當前給的時間戳。tcp
以後就調用實際的發送接口 rtp_session_rtp_send 進行發送。該接口具體會調用 send 系統調用將數據包發送到網絡的另外一端。ide
發送完成後調用rtp_session_rtcp_process_send 查看是否須要發送 rtcp 包,依據的原則是:函數
若是由應用程序詢問的最後的時間戳減去以接收單位計算的最後一個rtcp 包發送的時間大於 rtcp 報告包應該發送的時間間隔,或者最後發送數據包的時間戳與按照發送時間戳單位計算的最後一個 rtcp 報告包發送的時間的差值大於 rtcp 應該發送的間隔,就構造 rtcp 的發送者報告包發送。測試
在構造 rtcp 控制包的過程當中,ssrc 源同步描述符采用session 上的源同步描述信息,NTP時間戳使用系統當前的時間加上 1900 到 1970 年間的秒數,實際上這個時間就是 1900 年噹噹前的秒數了(參見時間戳說明部分)。RTP 時間戳使用snd_last_ts,也就是最後發送的流的時間戳。發送的包數和包字節計數使用session 上RTP 流上統計的計數。另外,若是數據包有被收到,則包含一個報告塊,目前的設計也僅只包含一個報告塊。數據包構造完成後直 接發送。
若是會話當前的模式爲 send-only,則調用 rtp_session_rtcp_recv接收處理 rtcp 包。若是會話支持接收模式,則rtcp 包的接收會在 rtp 接收過程當中處理。
二、 接收過程。
數據包的接收是經過調用rtp_session_recv_with_ts 接口完成的。該接口其實是調用rtp_session_recvm_with_ts 從底層接收數據,將返回的消息塊中的有效數據(不包含rtp 頭) 拷貝到用戶的buffer 中。下面具體看 rtp_session_recvm_with_ts 的實現:
若是接收尚未啓動,rcv_query_ts_offset設置爲應用給定的初始時間,也就是應用詢問的時間,記錄了一個開始時間偏移。若是發送沒有啓動或者爲 recv-only 模式,則 session 的 last_rcv_time 設置爲系統當前的時間。若是設置了調度器,那麼rcv_time_offset 設置爲調度器啓動後運行到當前所用的時間,這個做爲接收的時間偏移。若是接收已經啓動了,爲了不針對同一個時間戳連續屢次接收,這裏判斷若是當前應用參數給的時間等於rcv_last_app_ts 也即應用程序最近一次詢問的時間戳,那麼read_socket 變量設置爲 FALSE, 避免連續接收。
接下來進入正常的處理流程,首先將rcv_last_app_ts 設置爲當前應用時間,也就是更新當前最後一次接收的時間。若是read_socket 設置了,調用 rtp_session_rtp_recv和 rtp_session_rtcp_recv 接口實際的從底層 socket接收數據。
在 rtp_session_rtp_recv 中接收到數據後會調用 rtp_session_rtp_parse 對數據包進行解析。在rtp_session_rtp_parse 中若是發現數據包是 telephone event 包,則會建立一個事件,將其發送到事件隊列上,具體處理參見事件部分的說明。Jitter中相關變量的更新也是在該接口中 進行處理的,經過調用jitter_control_new_packet接口完成。最後將數據包放到接收隊列上等
待進一步的處理。 從rtp_session_rtp_recv出來後,會檢查會話的telephone event隊列,若是不爲空,則說
明收到了撥號包,一方面須要調用註冊的回調函數,另外一方面則須要將其發送給事件隊列。以後接收就返回了。若是該隊列上沒有包,則繼續處理:
若是設置了接收同步標識,rcv_ts_offset被設置爲當前收到的 RTP 數據包中的時間戳。這做爲流的第一個時間戳。rcv_last_ret_ts變量則設置爲當前應用給出的時間。這裏僅僅是給一個初始的值。以後清掉同步標識。所以以前的偏移 rcv_ts_offset 記錄了第一個 rtp 數據包的時間戳。後續到達的數據包將再也不通過這裏的處理邏輯。
調用接口 jitter_control_get_compensated_timestamp 計算流的時間戳。具體參見 jitter 模塊說明。 若是 rtp上的jitter 控制是使能的,那麼就會利用 jitter buffer 機制對數據包進行流控, 不然,就直接從隊列上取一個新的數據包。在 jitter 使能的狀況下,若是 session 的 permissive 算法被啓用了,那麼就調用 rtp_getq_permissive接口獲取數據。在該接口中,判斷若是計算出的流的時間戳與 rtp 數據包中記錄的時間戳的差值小於 2的31 次方,就從隊列中彈出一個包返回,不然返回空。若是沒有啓用permissive 算法則調用 rtp_getq 接口按照正常方式接收數據包。在該接口中,咱們返回時間戳等於或者早於計算的時間戳的數據包,若是這樣的數據包不止一個,那麼扔掉更老的包,也就是從隊列上最早取出來的包,最後返回的就是最近一次取出的數據包。若是有兩個數據包有相同的時間戳,那麼只返回一個。另外,在該接口中若是有數據包也就是更老的包被丟棄了,那麼會把丟棄的包數目記載到reject 參數中返回。
若是上一步確有數據包返回,那麼會對數據包中的時間戳進行更新,這部分參見對jitter_control_update_corrective_slide接口的說明。隨後將 rcv_last_ts 時間戳更新爲包原始到達時的時間戳值,即未更新前的值。接着調用rtp_session_rtcp_process_recv 接口進行 rtcp 的接收處理。(以前是發送處理)觸發條件和觸發後時間量的修改同發送部分。若是最後一次rtcp 的 sr 報告中的發送計數小於統計量中的發送包數的統計,則調用 make_sr 構造 sr 報告包,同時將以前的統計計數更新爲統計量中保存的值。若是該值不小於,則說明不須要發送rtcp 的 sr 報告包,可是若是同時接收的包數大於零,就是說有數據包被接收到,則調用 make_rr 構造 rtcp 的 rr 包。若是包構形成功,則調用 rtp_session_rtcp_send發送包。
以後若是沒有啓動調度,則直接將包返回給上層,不須要再進行特殊處理,不然進行調度的處理。相似與發送部分,一樣是根據應用給定的時間和應用第一次調用接收時的時間差值做爲參數,調用rtp_session_ts_to_time 接口計算出包的調度時間間隔。這個間隔加上應用詢問第一個包時調度器運行的時間做爲包的下次調度時間。若是這個時間在調度器當前的時間以後,則就將這個時間做爲喚醒點,等待調度器調度。
接收和發送過程當中各個時間戳值的關係以下圖所示:
圖5-1
六: 防抖動的實現
關於 jitter 結構體中部分變量的說明:(關於該結構體參見圖 2-8) 其中 jitt_comp 爲用戶定義的防抖動補償時間,jitt_comp_ts爲將其轉化爲時間戳單位的值,adapt_jitt_comp_ts爲使用自適應算法計算後的補償時間值。 slide 爲包指望接收時間和應用接收時間的差值的平均值,prev_slide 爲上一次保存的 slide值。 jitter 爲 diff(最新獲得的包的時間戳值與本地接收時間值的差,用於計算slide)與更新後的 slide 的差值的平均值,olddiff 爲上一次計算出的diff 值,inter_jitter爲間隔抖動,參見 rtcp 協議(參考1)。
corrective_step和 corrective_slide 爲校訂步進值和校訂滑動值,在更新包裏帶的時間戳時會用到。
adaptive和 enabled 在介紹 jitter 結構體時已作說明。
在會話初始化的時候,會調用 rtp_session_set_jitter_buffer_params,該接口設置 jitter buffer 的參數。代碼中實際上將默認的 jitter時間設置爲了 80 毫秒,也就是四個數據塊的間隔(針對8KHZ 音頻採樣數據而言),輸入數據包的隊列長度設置爲了 100,也就是能夠緩衝 100 個數據包。同時,打開了jitter 的自適應(adaptive)特性,也就是jitter 自適應補償(adaptive compensation)。在實際中,用戶也能夠單獨調用rtp_session_set_jitter_compensation 設置 jitter 補償時間,能夠調用rtp_session_enable_adaptive_jitter_compensation 單獨設置是否打開自適應補償功能。
在設置jitter buffer 的時候,會調用接口 jitter_control_init完成 jitter 的初始化。在該接口中,jitt_comp 設置爲用戶設置的值,該值就是補償值。另外,調用jitter_control_set_payload 將該補償值轉換爲時間戳單位的值,設置給jitter_comp_ts,轉換依賴於 payload 的時鐘採樣率。校訂步進值(corrective_step)設置爲(160 * 8000 )/pt->clock_rate;大部分音頻採樣率都是8KHZ,因此應該是按照 160 的時間戳單位來校訂。
要使用jitter,須要使能 enabled 變量,要使用 adaptive,須要打開 adaptive 變量。
數據發送過程不須要 jitter 作什麼控制,關鍵是在接收中。數據接收完後並非直接交給上層應用,而是放到 buffer 中,其實就是隊列。Buffer 的大小在 jitter 初始化部分設置,默認爲 100(隊列的長度),也就是能夠緩衝 100 個包,這對一秒鐘動輒百十來個網絡包的媒體流來說,其實也緩衝不了多少數據。另外,接收到的包只要解析經過,都先緩衝到隊列中, 若是包數目超過了隊列大小,則移除最老的包,這也符合常理。後續爲應用傳遞的包都是從隊列上取出來的,因此取的也就是最老的包。在數據包是否須要取出來上傳給應用就須要jitter 來控制了。
對於已經緩衝到本地的數據包,沒有 jitter buffer 控制的狀況下咱們直接將其返回,若是有控制,則須要判斷包的時間戳,只有比給定時間戳老的包(早於給定時間戳到達)才上傳給應用。那麼這裏控制包是否上傳給應用,關鍵的因素就在於給定的時間戳值,這個值是怎麼來的呢?在程序中,經過調用jitter_control_get_compensated_timestamp接口計算獲得。基本計算式爲:
Ts = user_ts + slide –adapt_jitt_comp_ts
爲了更好的理解上面的計算式,咱們來看上述幾個值是如何計算出來的。首先,user_ts, 這是應用程序接收流時給出的時間戳,是基於應用接收的速度和payload 類型計算出來的, 典型的,對於採樣率爲 8KHZ 的音頻數據來講,該時間戳的增長步進值爲 160.(按照每20 毫秒採樣一次來算)。若是網絡傳輸沒有延遲,數據包處理不須要消耗時間,那麼user_ts 應該和 rtp 包裏帶的時間戳值是一致的。
slide,根據前面的介紹,爲數據包指望接收的時間和實際本地接收的時間差值的平均值。每次接收到新的rtp包後該值即進行更新,新的 slide 值爲以前值乘以 0.99 加上新計算的值乘以0.01。
adapt_jitt_comp_ts的計算依賴於 jitter 的值。Jitter按前面介紹,爲 diff 與更新後的 slide 的差值的平均值。一樣是已計算獲得的值乘以 0.99加上新的到的值乘以 0.01 獲得。若是數據包均勻到達,那麼diff 的值和slide 的值應該就是相等的,這樣 jitter 的值就爲零。相反, 若是 jitter 的值變化比較大,那麼說明數據包每次到來的間隔參差不一,必定程度上反映抖動比較大。
inter_jitter爲間隔抖動,含義和計算參見 rtcp 發送間隔分析(參考3)。從上面計算能夠看出,inter-jitter反映了兩次間隔的抖動狀況,而 slide 則反映了一個比較長期的均勻的抖動狀況。
若是打開了adaptive,slide 的值就會不斷更新,而且每當收到 50 個包後,adapt_jitt_comp_ts就會被更新。新值爲 jitt_comp_ts 和 2*jitter 中較大的一個。
上述計算過程可參見接口jitter_control_new_packet。
如今回過頭來再看上面的計算式,slide其實是個小於零的值,所以咱們其實是努力將時間戳靠近包裏自帶的時間戳值上去的,補償值在必定程度上起到了緩衝的做用。
關於數據包時間戳值的更新,若是從隊列中取出了數據包,而且當前數據包的時間戳值與以前收到的數據包的時間戳值不一致,在打開adaptive 的狀況下,將對數據包的時間戳值進行校訂,算法以下:
當前 slide 減去以前的 slide,若是差值大於校訂步進值correction_step,則校訂滑動值 correction_slide 加上一個 correction_setp,prev_slide更新爲 slide 加上 correction_step 的值。若是差值小於 correction_step的負值,則將其轉換爲正值後按照以前的方式進行相同的更新。以後將包裏的時間戳修改成加上 correction_slide 後的值。(如此修改後後續還有用否?數據包已經交給上層了。)
從上面描述的機制來看,jitter buffer機制利用緩衝在必定程度上能保證數據包以比較均 勻的速度上傳給應用。
七: 事件的處理
在 rtp 會話上,保存了 signal table、表中的各個事件的回調函數以及事件隊列。若是接收到 signal table 中所註冊的事件信息,則調用註冊的回調函數進行處理,而對於 telephone event 包,除了調用回調函數外,還須要將其發送到事件隊列,以便上層應用進行進一步的處理。
八: 其餘須要說明的
Rtp 這塊有關時間戳的比較計算,主要經過幾個宏來完成。
RTP_TIMESTAMP_IS_NEWER_THAN(ts1,ts2)比較 ts1 小 於 等於ts2 , RTP_TIMESTAMP_IS_STRICTLY_NEWER_THAN(ts1,ts2)則比較ts1 小於 ts2,沒有等於關係。可是實際實現中,差值都是與2 的 31 次方來比較,原理以下:
就是將差值結果強制轉換爲 uinit 後於 0x8000000 進行比較,這樣大於等於零返回成功, 小於零返回失敗。仍是簡單的比較關係而已。
關於 rtcp 同步 rtp 流的問題:
從 rtp 傳輸過來的媒體流可能爲音頻流和視頻流,兩者採用了不一樣的編碼方式和不一樣的分採樣率,同時這兩個流中都帶有時間戳信息。如何將這兩個流進行同步,這能夠經過rtcp 的報告包來完成。在 rtcp 的報告包中帶有具備絕對的基於 NTP 的時間戳信息,同時也帶有與 rtp 流中相同的採樣時間戳信息,依據這兩個時間戳信息就能夠對同一個媒體流進行同步。Rtcp 中帶有的惟一的 cname 信息能夠將同一個媒體的音頻和視頻流信息關聯起來,雖然這兩個流使用不一樣的源描述符 ssrc。
關於 rtcp 發送的間隔:
這塊程序中只是按照 5 秒的間隔來進行計算,是固定的。存在巨大的侷限性,可是對於點對點的通訊來說,問題不是很大。另外測試 eyebeam 程序,發現是按 3 秒左右來發送rtcp 包的,不知是計算獲得仍是固定值。
這塊能夠按照協議要求對 oRTP 進行改進。
關於 rtcp 包的接收發送規則:
目前從代碼來看,每次發送一個 rtp 包後檢查是否須要發送 rtcp 包。每次接收rtp 包時同時檢查是否能夠接收 rtcp 包,完了後檢查是否須要發送 rtcp 包。可是協議的規則不是這樣。
關於 rtcp 包的構建問題:
Rtcp包必須以複合包的形式向網絡上傳輸,並且必須至少包含兩個基本的包,第一個必須是發送者報告包或者接受者報告包,另外包括 sdes 源描述項包。
關於 rtcp 協議方面的問題,能夠參考 rtcp 協議的說明文檔,參考資料 1
九: 使用 oRTP 庫
oRTP 提供了測試程序來測試 oRTP 庫,同時測試程序也是如何使用 oRTP 最好的例子。
測試程序在源代碼的 test 目錄下,包括髮送測試 rtpsend.c,接收測試rtprecv.c,並行發送測試 mrtpsend.c,並行接收測試 mrtprecv.c,還有有關telephone event 相關的測試。這裏的說明 只針對上述四個有關接收發送測試程序的測試結果。
程序中提供的測試例子,大部分都是針對音頻數據的,時間戳增長值也都是設置爲160 了(如何計算得來,參見時間戳部分的說明),這在接收和發送視頻數據時存在問題,須要作些修改,具體見問題列表。
海思3716 平臺上 oRTP 代碼的編譯:. /configure --host=arm-hisiv200-linux【指定交叉環境】--prefix=【指定安裝目錄】 Make Make install完了以後會在指定的目錄下建立 include、lib以及 share 三個文件夾。include 包含了咱們要使用 oRTP 庫的頭文件,lib 目錄包含了編譯完的庫,share下是文檔說明。能夠把 test 目錄下的文件拿到安裝目錄下,跟庫一塊兒編譯出來可執行文件進行測試。
測試問題列表:
一、 測試程序默認爲測試音頻流,沒有包含視頻流,時間戳步進值爲 160,就是按照20ms 採樣週期加 8KHZ 採樣率計算出來的。這就致使接收和發送視頻流的速度都特別慢。所以接收視頻數據時須要調整步進值爲3600,即 90000 採樣率加 25 幀每秒計算得來。實際使用過程當中可根據測試效果進行調整。
二、單獨調整第一條還不能徹底解決速率問題,還須要調整 payload type。接收過程能夠根據接收的類型發生改變(即會產生payload type changed 事件),從而對此做出相應調整,故對接收過程影響不大,可是發送過程由於默認payload 類型設置爲了 payload_type_pcmu8000,致使發送速度很慢,(基於 8KHZ 採樣率)調整爲33 (payload_type_mp2v)便可。
三、 33在 oRTP 中默認並無添加支持,可仿照 avprofile.c 文件中的實現單獨添加。
四、 測試中發現 multiple recv 測試程序接收不到數據,將文件操做接口換爲fopen,及 ascii 標準類型的文件讀寫接口便可,緣由待查。
五、 多會話接收目前是按照端口號不一樣來進行的。
六、 關閉調度模式和 block 模式能夠加速視頻流的推送
七、 在沒有調度器的狀況下,控制視頻流發送速度,能夠改善馬賽克狀況。其實發送過快
也不行,用 vlc 播放測試來看。
八、 在啓動調度器的狀況下,設置應用時間戳增量值(user_ts)或者調整該變量也能夠調
整視頻流的發送速度,調整馬賽克狀況。
九、 數據發送速率與採樣率,user_ts 時間戳值增長以及buffer 大小均有關係。
十、 在並行發送和接收測試時(msend、mrecv),須要設置爲非阻塞模式,不然程序可能會被卡死。
十: 參考
1 RFC3550RTP: A Transport Protocol for Real-Time Applications 2 RFC3551RTP Profile for Audio and Video Conferences with Minimal Control 3 RTCP 發送間隔分析