1. 程式人生 > >ORTP庫使用詳解

ORTP庫使用詳解

一、關於 oRTP

  oRTP 是一款開源軟體,實現了 RTP 與 RTCP 協議。 目前使用 oRTP 庫的軟體主要是linphone(一款基於IP 進行視訊和語音通話的軟體)。 
  oRTP作為 linphone 的 RTP 庫,為基於 RTP 協議傳輸語音和視訊資料提供保障。

二、原始碼的構建框架

  類似於 mediastream2 中的 filter,在RTP 中也有比較重要的一個結構,就是 payload type。該結構用於指定編碼型別,以及與其相關的時鐘速率、取樣率等一些引數,參見下圖。 
   

這裡寫圖片描述

圖 2-1

  實際上在 RTP 的包頭就有專門的域用來定義當前傳輸的資料是什麼編碼型別的。在程式碼中,不同的媒體型別有不同的 payloadtype 結構體與之對應,像h263、g729、MPEG4 等。因為每種編碼都有其獨有的特點,而且許多引數也不一樣,所以 RTP 包頭中使用 payload 域標記負載的型別,一方面接收端可以就此判斷負載的型別,從而選擇對應的解碼器進行解碼播放;另一方面,程式碼在進行時間戳等方面的計算時可以更加方便一點。

  payloadtype 結構體定義了 payload 的許多屬性,比如是音訊還是視訊資料、時鐘取樣率、每次取樣的位元數、正常的位元率、MIME型別、通道等等。程式碼中已有常見音視訊編解碼器對應的 payloadtype 結構體實現,應用程式在初始化 oRTP 庫時,可以根據自己的需求,選擇其中的一部分新增到系統中。所有系統當前支援的 payload 型別都被放在一個數組中,由全域性變數 av_profile 這個結構體例項統領,如下圖所示: 

這裡寫圖片描述

圖 2-2

  這些 payloadtype 結構體在 payload 陣列中的位置就是以編碼型別的定義為索引的。編碼型別值的定義在 RFC3551
 第六部分“payload type definitions”進行了描述。avprofile.c 檔案定義了所有的 payload type。而有關 payload type 和 profile 的操作在檔案 payloadtype.c 檔案中實現。

  除了 payloadtype 結構體外,一個更重要的結構體是 rtpsession。該結構體即是一個會話的抽象,與會話相關的各種資訊都定義在該結構體上或者能夠通過該結構體找到。要使用 oRTP 進行媒體資料的傳輸,需要先建立一個會話,之後所有資料的傳輸都在會話上完成或基於會話完成。rtpsession 結構體的定義如下: 
   

這裡寫圖片描述

圖 2-3

  可以看到,這是一個非常大的結構體,從側面說明了要維護的與會話相關的量還是比較多的。

  關於該結構體的比較詳細的說明會在後面給出。Session 的初始化通過介面 rtp_session_init 完成,外部獲得一個新的 session 是通過呼叫介面 rtp_session_new 完成。關於 session 的其他有關配置和獲取資訊的操作都可以在檔案 rtpsession.c 中找到定義。

  使用 oRTP 進行資料傳輸時,可以在一個任務上完成多個會話流的接收和傳送。這得益 於 oRTP 中排程模組的支援。要使用排程模組,應用需要在進行 oRTP 的初始化時對排程進行初始化,將需要排程管理的會話註冊到排程模組中,這樣當進行接收和傳送操作時,先向排程詢問當前會話是否可以進行傳送和接收,如果不能進行收發操作,則處理下一個會話。這有點類似 I/O 介面上的 select 操作。排程模組使用的資料結構主要為 rtpscheduler,如下圖所示: 
   

這裡寫圖片描述

圖 2-4

  List 儲存了所有要處理的會話,r\w\e 的意義類似於 select,在這裡分別代表接收、傳送以及異常。posixtimer.c、rtptimer.c、scheduler.c、sessionset.c 等檔案實現了排程模組。 資料在底層實際的接收和傳送是通過 socket 介面完成的,這些實現在 rtpsession_inet.c 檔案中。為了方便將 oRTP 移植到不同平臺上,oRTP 實現了對作業系統介面的封裝,包括常用的任務的建立及銷燬,條件變數及互斥鎖,程序間的管道通訊機制等。這些在 port.c 檔案中實現。

  除了作業系統相關的介面外,oRTP 為了便於內部操作,實現了部分資料結構,一個是雙向連結串列,在檔案 utils.c 中;一個是佇列,在檔案 str_utilis.c 檔案中。連結串列的實現比較簡單,佇列的實現相對較複雜一點。首先,佇列資料結構由三部分組成:佇列頭、訊息塊以及資料塊,圖示如下: 
   

這裡寫圖片描述

圖 2-5

  上圖中從左到右依次為佇列頭,訊息塊和資料塊。佇列頭指向訊息塊,訊息塊之間可以構成雙向連結串列,這是佇列的基本要素。訊息塊本身不帶 buffer,資料是由專門的資料塊來儲存的,並被訊息塊指向。上圖是一個初始化後的狀態,訊息塊的讀寫指標都指向資料塊的 buffer 的開始位置。資料塊的 base 和 lim 指標則分別指向 buffer 空間的開始地址和結束地址處。向 buffer 中寫入和讀出資料後的狀態變化如下圖: 
   

這裡寫圖片描述

圖 2-6

  除了向佇列新增訊息塊外,上述資料結構設計還支援向一個訊息塊新增新的訊息塊,這樣可以支援一個訊息塊儲存較大塊的資料,如下圖所示: 
   

這裡寫圖片描述

圖 2-7

  訊息塊的 b_cont 指標用於連線新的訊息塊。

  在傳送上層應用的 payload 資料之前,oRTP 會構造一個訊息塊,資料指標會指向 payload,這避免了資料拷貝。較低層的介面處理資料時依賴於訊息塊結構。接收後的資料從訊息塊中拷貝到使用者 buffer。接收的 rtp 和 rtcp 包的解析處理函式在檔案 rtpparse.c 和 rtcpparse.c 檔案中實現。另外,rtcp.c 檔案實現了 rtcp 資料包的構造處理。

  在基於 ip 的音視訊流傳輸中,防抖動能力是一個重要的特性,這在一定程度上能夠保證使用者有良好的體驗。在 oRTP 中,是通過 jitter 模組完成這部分工作的。相關資料結構如下圖所示: 
   

這裡寫圖片描述

圖 2-8

  要使用 jitter 功能,需要使能 enabled 變數,如果要支援自適應補償,則需要使能 adaptive 變數。對於資料傳輸過程中產生的一些事件(比如 ssrc 發生改變,資料為 dtmf 資料等),在 oRTP 中是通過 signaltable(訊號表)來處理的。signaltable 關聯了事件型別與其上的回撥函式。oRTP 使用 signaltable 處理如下一些事件:ssrc_changed(ssrc發生改變),payload_type_changed (payload type 發生改變),telephone-event_packet(telephone event包到達),telephone-event(telephone 事件), timestamp_jump(timestamp jump事件),network_error(網路錯誤事件),以及 rtcp_bye(rtcp bye 包事件)。使用者可針對這些事件註冊回撥處理函式,當底層接收函式接收到 rtp 包後,會對包進行檢查,發現是上述某些事件的話,則觸發回撥函式的執行。rtpsignaltable.c 檔案實現了對該表的操作,包括初始化、新增 callback、刪除 callback,以及執行 callback。

  oRTP 中對於事件的處理是基於事件結構體和事件佇列的。佇列用於存放事件結構體,結 構體用於存放事件的資料。相關的處理在檔案 event.c 中定義。特別的,對於 telephone 事件 的處理放在 telephone_event.c 檔案中,其中包括瞭如何構造用於傳輸 telephone_event 的 rtp 包,如何將 telephone 事件新增到包中,如何傳送 dtmf 資料,以及接收到對應資料包後該如何處理。關於 telephone_event 的構成如下圖所示: 
   

這裡寫圖片描述

圖 2-9

  最左邊的結構體是 rtp 包中存放的有關 telephone event 的資料,通過 packet 指標可以找到telephone event 的詳細資訊。最終放入事件佇列的也是 packet 指向的內容。

  在使用 oRTP 提供的 rtp 庫之前,需要先對其進行初始化,這部分的實現在 oRTP.c 檔案 中。oRTP的初始化主要呼叫兩個介面:ortp_init 和ortp_scheduler_init。其中 ortp_init完成 了 payload 的註冊,ortp_scheduler_init 完成了排程任務的初始化。

三、有關時間戳的說明

1、關於 RTP 傳輸中時間戳的說明(這部分來自於網路)

  時間戳單位:RTP 協議中使用的時間戳,其單位不是秒之類的,而是以取樣頻率為基礎的。這樣做的目的就是為了使時間戳單位更為精準。比如說一個音訊的取樣頻率為 8000Hz,那麼我們可以把時間戳單位設為 1 / 8000。

  時間戳增量:相鄰兩個 RTP 包之間的時間差(以時間戳單位為基準)。

  取樣頻率:每秒鐘抽取樣本的次數,例如音訊的取樣率一般為 8000Hz 
   
  幀率:每秒傳輸或者顯示幀數,例如 25f/s 在 RTP 協議中並沒有規定時間戳的粒度,這取決於有效載荷的型別。因此 RTP 的時間戳又稱為媒體時間戳,以強調這種時間戳的粒度取決於訊號的型別。例如,對於 8kHz 取樣的話音訊號,若每隔 20ms 構成一個數據塊,則一個數據塊中包含有 160 個樣本(0.02 × 8000 = 160)。因此每傳送一個 RTP 分組,其時間戳的值就增加 160。

  如果取樣頻率為 90000Hz,則由上面討論可知,時間戳單位為 1/90000,我們就假設 1s 鐘被劃分了 90000 個時間塊,如果每秒傳送 25 幀,那麼,每一個幀的傳送佔多少個時間塊呢?當然是 90000/25 = 3600。因此,我們根據定義“時間戳增量是傳送第二個 RTP 包相距傳送第一個 RTP 包時的時間間隔”,故時間戳增量應該為 3600。

  關於 RTCP 中 NTP 時間戳的計算問題:從 1900 年到現在的經過的秒數賦值給 NTP 時間戳的高 32 位,這個時間的低 32 位通過當前獲取的納秒時間值計算得到。將 1 秒劃分為 2 的 32 次方來表示,則一份子持續的時間大約位 232 皮秒。如果當前時間為 x 秒 232 毫秒,則232毫秒為232000 微秒,232000000 納秒,232000 000 000 皮秒,即1000 000 000多個 232皮秒。也就是說在 NTP 時間戳的低32位劃分的2的32次方個232皮秒塊中佔用了1000 000 000個塊,轉換為16進製表示為0x3b9aca00,也就是說噹噹前時間的低位為232毫秒的話,NTP 時間戳的低 32 位就設定為 0x3b9aca00。

  在 linux 系統中,我們常用的一個時間是 1970 年 1 月 1 日以來的時間所經過的秒數。在 RTCP 中,我們可以將當前所獲得的上述時間加上83AA7E80(十六進位制)就是 1900 年 1 月 1 日以來所經過的秒數了。換為十進位制,則為 2208988800。計算方法為 (70 * 365 + 17) * 24 * 60 * 60。

2、程式碼中有關時間戳變數的說明

  在資料的接收和傳送過程中,用到了許多記錄時間的變數。通過這些時間變數,oRTP 完成對 rtp 資料的流控功能。所有這些變數都定義在 rtpstream 結構體中,如下圖所示:(這裡只是截取了時間相關的變數)

這裡寫圖片描述

圖 3-1

  下面對這些變數的含義進行集中的說明:

uint32_t snd_time_offset;
/* the scheduler time when the application send its first timestamp */
  • 1
  • 2
  • 1
  • 2

  應用程式傳送其第一個時間戳時的排程器時間 
  

uint32_t snd_ts_offset;/* the first application timestamp sent by the application */
  • 1
  • 1

  被應用程式傳送的第一個應用程式時間戳

uint32_t snd_rand_offset; /* a random number added to the user offset to make the stream timestamp*/新增到使用者offset 上的一個隨機數,用來產生流的時間戳

uint32_t snd_last_ts; /* the last stream timestamp sended */流上最後傳送的時間戳

前述三個時間變數是 offset 結尾的,分別標記了第一個時間戳,包括排程器的時間偏移, 在應用開始傳送資料時,應用傳送資料的時間偏移,也即是自己的時間戳,還有一個隨機數 用來新增到偏移上的,而第四個才是真正標記流裡面當前最新發送的資料的時間戳。

uint32_t rcv_time_offset; /*the scheduler time when the application ask for its first timestamp*/應用程式詢問其第一個時間戳時的排程時間,這裡詢問意指獲取接收到的資料包—此應該指開始接收資料時的排程器時間

uint32_t rcv_ts_offset;/* the first stream timestamp */第一個流的時間戳—-此應該指第一 個rtp 包到來時其流上帶的時間戳值

uint32_t rcv_query_ts_offset;/* the first user timestamp asked by the application */被應用程 序詢問的第一個user時間戳—此應該指應用接收資料流時的時間

uint32_t rcv_last_ts; /* the last stream timestamp got by the application */應用程式得到的流的最後一個時間戳—此應該指應用程式收到的最後一個rtp 包的時間戳,是包裡的時間戳值, 而非應用自己的時間。

uint32_t rcv_last_app_ts; /* the last application timestamp asked by the application */被應用 程式詢問的最後一個應用程式時間戳—此處應該指應用收最後一個包時的應用時間,是應用 按照 payload 型別及其取樣率增長的時間戳記錄,不是系統時間,也不是包裡的時間

uint32_t rcv_last_ret_ts; /* the timestamp of the last sample returned (only for continuous audio)*/最後一個返回的取樣的時間戳,僅僅對於連續的音訊而言

接收相對於傳送來講存在一個問題,就是接收資料包時當前系統有個時間,資料包裡面 也有時間戳記錄的時間,排程器也有記錄時間。而對於傳送,當前應用的時間就是給包的時 間戳時間,這兩個值對於傳送來講是一樣的。

uint32_t hwrcv_extseq; /* last received on socket extended sequence number */在socket 上最 後接收的擴充套件的序列號

uint32_t hwrcv_seq_at_last_SR;每次傳送報告包後,該變數更新為hwrcv_extseq,因此是 最近傳送rtcp 報告包時的最高擴充套件序列號。

uint32_t hwrcv_since_last_SR;每收到一個 rtp 包,該變數加 1,在 rtcp 報告報構造好後, 該變數就清為零,因此說明這個變數計數的是從上一個報告包以來接收的rtp 包數目。

根據上面三個變數就可以計算出丟包率。首先,最近一次丟失包數(就是自從上一次sr 或者rr傳送以來)通過hwrcv_extseq – hwrcv_seq_at_last_SR – hwrcv_since_last_SR計算得到。 但是丟包率為啥要除以hwrcv_since_last_SR 比較奇怪。這個值是自從上一次傳送報告包以 來累計接收的包數。這個值不應該就是期望接收的包數。(最高序列號減去最初序列號)

累計包丟失數通過每次的丟包數累加得到。uint32_t last_rcv_SR_ts;/* NTP timestamp (middle 32 bits) of last received SR */最後一個接收到的 sr 的 NTP 時間戳,取的是中間的 32bit。這個值也是報告包中上 LSR 值的來 源。

struct timeval last_rcv_SR_time;/* time at which last SR was received*/最後一個 sr 被 接收到的時間,這個時間是用系統當前的時間來表示的。這個值記錄了接收到最後一個SR時的系統時間,再發送當前報告包時,再次獲取系統當前時間,然後二者相減,得到的值乘 以65536 得到以1/65536 為單位的時間值。

uint16_t snd_seq; /* send sequence number */傳送序列號。累加變數,儲存會話的序列號的 增長。

uint32_t last_rtcp_report_snt_r;/* the time of the last rtcp report sent, in recv timestamp unit */最後一個rtcp 報告發送的時間,按照接收時間戳單位。程式中這個值是用 rcv_last_app_ts變數的值來更新的。就是應用最後一次進行 rtp 接收時其時間戳增長到的值。 不管收沒收到就是這個值了?

uint32_t last_rtcp_report_snt_s;/* the time of the last rtcp report sent, in send timestamp unit */最後一個rtcp報告發送的時間,按照發送時間戳單位。程式中這個值是用snd_last_ts變數的值來更新的,就是應用最後一次進行rtp 傳送操作時其時間戳增長到的值。不管有沒 有傳送 rtcp 報告包出去?

uint32_t rtcp_report_snt_interval; /* the interval in timestamp unit between rtcp report sent */按照時間戳單位表示的 rtcp 報告發送的間隔。這個值程式中使用預設時間值 5 秒與 payload的 clockrate 的乘積來表示。是不是計算過於簡單了?

uint32_t last_rtcp_packet_count; /the sender’s octet count in the last sent RTCP SR/在最後 傳送的一個rtcp sr包中記錄的傳送者傳送的 rtp 包總數。這個變數把這個值記錄了下來。記錄這個值是為了實現協議中規定的:如果之前的rtcp 包傳送之後到當前再次傳送 rtcp 包, 這期間如果傳送了rtp 包,則傳送rtcp SR 報告包,否則只需傳送 rtcp RR 包就可以了。

uint32_t sent_payload_bytes; /used for RTCP sender reports/用於rtcp 傳送者報告的 payload 位元組數,資料來源。這個變數儲存了從開始傳送到傳送這個 rtcp 報告包時傳送的字 節總數,但不包括頭部和填充。

上面這些時間相關變數都是用於rtcp 包的。 unsigned int sent_bytes; /* used for bandwidth estimation /用於頻寬評估struct timeval send_bw_start; /used for bandwidth estimation */同上 上面兩個變數用於計算髮送頻寬,start記錄的開始時間,sent_bytes 記錄了傳送的位元組數,該值沒呼叫 rtp 介面傳送資料後都會進行累加更新。記錄一次頻寬值後,清為零,之後進行 下一次頻寬估計的計算。

unsigned int recv_bytes; /* used for bandwidth estimation /同上struct timeval recv_bw_start; / used for bandwidth estimation */同上 作用和處理邏輯都同上面傳送部分。

四、排程的實現

  要使用 oRTP 的排程功能,需要在初始化 oRTP 庫時呼叫介面 ortp_scheduler_init 對排程模組進行初始化。在該介面中建立一個RtpScheduler 型別的結構體__ortp_scheduler(參見圖2–4),並呼叫rtp_scheduler_init 初始化它。

在 rtp_scheduler_init 中,分配定時器 posix_timer(rtptimer型別結構體,參見圖 2-4)掛 載到排程結構體上。(定時器初始間隔設定為POSIXTIMER_INTERVAL)。接著初始化 __ortp_scheduler 的其他部分,包括初始化互斥鎖、條件變數等。在排程模組執行的整個過 程中,相關操作都圍繞該結構體,__ortp_scheduler被定義為全域性變數。

初始化完後呼叫rtp_scheduler_start 啟動排程任務。排程任務的執行體為 rtp_scheduler_schedule,引數為排程結構體自身。

排程任務執行後,首先初始化 timer。在這過程中將 timer 設定為執行狀態,儲存系統當 前時間值。接著進入任務的while 迴圈,遍歷 scheduler 上註冊的所有會話。如果不為空, 說明應用有會話需要排程管理。此時會呼叫 rtp_session_process 進行處理。所有需要排程管 理的會話按上述邏輯處理完之後,廣播訊號量unblock_select_cond 喚醒所有因等待 select而 睡眠的任務,意即讓這些任務去檢查自己的會話是否需要進行處理了,這塊後續還會說明。 此時排程器完成了自己當前的工作開始準備進入睡眠狀態,而其他的任務開始檢查掩碼結果 以決定是需要進行資料的收發還是等待下次排程。

排程的睡眠是通過呼叫 timer 的 timer_do 介面來完成的,這裡就是posix_timer_do 介面。 在該介面中,計算系統當前的時間,並和初始啟動的時間(排程器初始化時儲存)做差運算, 結果轉換為毫秒單位。posix_timer_time記錄了下一次排程器超時到達的時間,每次就讓 posix_timer_time減去系統當前時間與啟動時間的差值,如果大於零,說明排程時間還沒有 到達,就呼叫 select 等待(posix_timer_time-差值)時間,然後重新獲取系統當前時間,計 算新的差值。流程圖如下:

這裡寫圖片描述

圖 4-1

  直觀一點來說就是,排程器的排程精度由 POSIXTIMER_INTERVAL確定,每次排程器執行,如果處理會話集合(session set)的時間超過該間隔,就會接著處理下次排程,如果沒有用完,即剩餘 diff 時間,這點時間就通過 select 系統呼叫耗掉。因此,排程器每次進行排程的時間點基本是確定的,diff 時間根據處理會話集合消耗時間的不同,每次的大小都是 不一樣的。

  排程任務每次都基本上會在固定點檢查所有需要由它來管理的會話,也就是應用新增到 會話集合中的所有會話。如果在處理這些會話的過程中,時間超過了排程器設定的預設間隔, 那麼排程器處理完本次迴圈後會接著進行下一輪的迴圈,否則,會等待,直到下一個排程點 時間到來。

  排程器檢查每個會話是通過 rtp_session_process 介面完成的。對於某一個會話,呼叫該接 口將按如下邏輯進行處理:首先檢查會話的傳送部分的 waitpoint 結 構體,將其時間與排程 器當前時間進行比較(上述結構體中的時間是收發介面設定的需要喚醒的時間點)。如果該 會話需要進行喚醒,也就是在等待喚醒,而且其等待的喚醒點也到了,(就是當前排程器時 間已經超過了喚醒點)則清除需要進行喚醒的標識,然後在排程器結構體(排程器初始化時 建立的全域性變數)的w_session 集合上將該會話的掩碼位置置位,並通過條件變數喚醒該任 務。同樣的邏輯檢查r_session 集合。總的來看,排程器就是檢查各個會話設定的喚醒點是 否到了。如果到了則喚醒並設定其在集合中的掩碼標誌位。這樣收發任務通過檢查掩碼標識 位就知道是否可以繼續進行收發了。一旦可以收發,應用會再次將這些掩碼位置重新清除掉, 這樣在下次收發前就需要再次等待排程器進行檢查並設定。

  上層應用通過呼叫介面 rtp_session_set_scheduling_mode 將一個 session 新增到排程器中。 新增過程為先獲得排程器全域性資料結構,給會話的 sched 指標,即該會話的 sched 指標指向 全域性排程器資料結構;會話flags新增 RTP_SESSION_SCHEDULED,意即讓排程器管理會 話;最後呼叫 rtp_scheduler_add_session 介面將會話添加註冊到排程器管理的會話集合上。 rtp_scheduler_add_session 介面中,先將會話掛到排程器資料結構的會話連結串列上(排程器每次 迴圈時就從該連結串列上獲取要處理的會話),然後在all_sessions 集合中找到一個空閒位置,記 錄該掩碼位置,將當前會話在該集合中的掩碼位置進行置位操作。這樣排程器通過會話連結串列 就可以找到要排程的會話,進而找到會話上記錄的掩碼位置,從而在集合中對會話進行設定。 類似的,將會話從集合中移除的介面為rtp_scheduler_remove_session,基本處理邏輯就是找 到會話列表中的該會話,將其從連結串列中移除,並把它在集合中的位置清零。

  上層應用檢查是否需要收發資料是通過檢查會話集合來完成。首先,應用呼叫session_set_new 介面建立一個新的集合。在該介面中我們建立一個SessionSet 結構體並將其 初始化,後續的操作就在該結構體上完成。對於需要排程的會話,呼叫介面session_set_set將其在該集合中的掩碼位設定為 1,也就是打上標識。應用在每次接收或者傳送前,呼叫接 口session_set_select檢查是否可以傳送或者接收。該介面會將 caller 掛起直到有事件到達。 session_set_select類似我們常用的系統呼叫 select,其使用方式也是類似的。

  Session_set_select是應用與排程器打交道比較重要的一個介面,下面看看它的實現:

  首先呼叫ortp_get_scheduler 獲取到排程器全域性結構體 進入 while(1)迴圈如果接收集合不為空,(也就是要檢查是否有接收事件), 呼叫session_set_init 初始化一個臨時存放結果的集合 呼叫session_set_and 檢查會話集合。處理基於三個量,一個是初始化時新增到排程 中進行接收檢測的會話集合r_sessions(這個集合代表排程器可以處理那些會話), 一個是使用者呼叫select 時進行檢查的會話集合,也就是應用要處理的集合(這個集 合代表使用者要處理那些會話),一個就是當前排程處理的會話集合的最大值all_max (排程器從小到大檢查到 all_max 位置就檢查了其需要檢查的所有會話掩碼位)。 在處理中,集合就是一個數組,陣列每一個元素的每一個 bit 位代表了一個會話。 這樣,以 all_max 為上限,檢查每一個會話對應的 bit 位,將排程器結構體上的接 收集合和使用者集合進行與運算(注意:這裡接收集合是排程器處理完的,其中被設 置的會話表明有接收事件到達。),得到的結果既是排程器處理後可以接收的會話, 也是在應用環境中添加了的要處理的會話,記為result set。同時將接收集合中被添 加到 result 集合中的位清除(因為已經獲取了)。最終 session_set_and介面返回 result 集合中被設定的 bit 位數,也就是實際可以處理的會話個數。 如果有會話的事件到達,即返回值大於零,將新的結果拷貝回用戶集合,告知使用者 那些會話有事件到達了。

  對於傳送和 error 集合做同樣類似的處理 如果最終三個集合處理完後有事件(不管是接收還是傳送還是error),則直接返回, 否則在條件變數上等待,直到排程器返回有事件到達。

  跳到 While(1)進行下次迴圈處理

  除了session_set_select 介面供使用者呼叫外,oRTP 還提供了帶有超時處理的 select 介面:session_set_timedselect,該介面可以設定跳出時間,而不是像session_set_select 那樣為死等 模式。

  綜合應用和排程器兩部分處理,可以看出,排程器的精度(排程間隔)在一定程度上可 以影響資料接收速度。因為如果本次檢查會話上不能進行收發資料操作,那麼下次的檢查就 必須等到下個排程點,即使在當前檢查剛過資料就到來了,應用也得等到下次排程點,由調 度器檢查後應用才能知道,而這段時間資料就必須等待。這樣的話,如果排程間隔過大,那 麼接收速度必然減慢。

  應用在收發資料時,除了可以使用排程器管理會話外,還可以設定阻塞與非阻塞模式。 關於排程器與阻塞模式的關係:如果使用排程器,可以不用管阻塞模式,即排程器可以工作 在阻塞模式下,也可以工作在非阻塞模式下。如果要使用阻塞模式,則需要啟動排程器,這 是必須的,即阻塞模式必須工作在排程器使用的情況下。(因為阻塞功能的實現本身就依賴 於排程器)。對於排程器啟動並且為非阻塞模式,當資料不能收發時,上層任務可以在應用 層做其他操作來等待。對於排程器啟動並設定為阻塞模式,當資料不能收發時,上層應用任 務會等待條件變數,該條件變數只有等到排程器 signal 之後,上層任務才能繼續執行。所以, 如果上層應用啟動了多個傳送或者接收埠,那麼非阻塞模式下有一個或多個埠不能傳送

  或者接收時,會嘗試其他埠是否可以傳送,如果都不能使用,則可以空迴圈。而阻塞模式 下,如果有一個埠被阻塞了,那麼其他埠都無法進行資料的收發了,即必須等待該埠 有事件並被排程器觸發後才有機會進行其他埠的傳送或者接收。所以,在多接收發送應用 情況下不應使用阻塞模式。

  在非阻塞模式下,應用的等待時間消耗在 session_set_select 介面中了。阻塞模式下,應 用可能就阻塞在傳送接收介面中了。

  使用目前的庫,存在一個問題,在使用排程的情況下開啟阻塞模式,則會導致程式掛住。 具體原因分析來在於,阻塞模式下,包傳送時其喚醒時間點packet time在排程器scheduler time後面了,這樣排程器檢查時就認為不需要進行喚醒,因為此時已經比排程器 old 了。根 本原因在於阻塞時等待排程器執行,導致排程器時間超過了 packet time。而非阻塞模式下, 包會直接傳送出去,這樣其實包的暫緩傳送是在下次,也即是下次select 等待時,排程器趕 上包的傳送時間,然後喚醒包傳送,而阻塞模式下下次 select 時排程器已經趕上了並超過了 包的傳送時間。

  關於排程器與應用的關係如下圖所示:

這裡寫圖片描述

圖 4-2

  排程器檢查 session set,喚醒到時間的接收流並設定掩碼位。應用檢查掩碼位得到接收流是否被喚醒,然後進行接收處理,在接收處理中會清掉排程器設定的掩碼位。

五、資料的接收和傳送

1、傳送過程

  應用傳送資料時呼叫介面 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 設定為排程器執行到現在的時間。 這應該算是時間戳的初始化。

  如果排程被啟用了,則對部分時間戳做一些調整,如下:

  首先計算包應該傳送的時間,就是packet time。計算方法為在傳送第一個資料包時的調 度器時間加上包的傳送間隔,這個間隔根據應用當前給的時間與第一次的傳送給的時間的差 值除以payload 的時鐘速率計算得到,比如第一次傳送的時間為 100,當前為 300,也就是 說傳送經過了 200 個單位,如果 payload 的 clock rate 為 10,則說明經過了 20 個時間戳單位, 也就是說當前包的時間戳為排程器時間加 20。(packet time 實際上應該是將下一個包的傳送 時間轉換為排程器時間,交給排程器讓排程器來排程)如果計算的packet time與排程器當 前的執行時間的差值小於2 的 31 此方,並且二者不相等,則設定該等待點在 packet time喚 醒。(關於該比較,參見其他說明部分)

  在傳送資料前,RTP的時間戳設定為應用傳進來的當前的時間戳。snd_last_ts時間戳也 設定為應用當前給的時間戳。

  之後就呼叫實際的傳送介面 rtp_session_rtp_send 進行傳送。該介面具體會呼叫 send 系統 呼叫將資料包傳送到網路的另一端。

  傳送完成後呼叫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 接收過程中處理。

2、接收過程

  資料包的接收是通過呼叫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

為了