天天看點

ORTP使用詳解

注:本文轉載自http://blog.csdn.net/suer0101/article/details/7329442

一: 關于 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進制表示為3b9aca00,也就是說當目前時間的低位為232毫秒的 話,NTP 時間戳的低 32 位就設定為 3b9aca00。

在 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;

應用程式發送其第一個時間戳時的排程器時間uint32_t snd_ts_offset;被應用程式發送的第一個應用程式時間戳

uint32_t snd_rand_offset; 添加到使用者offset 上的一個随機數,用來産生流的時間戳

uint32_t snd_last_ts; 流上最後發送的時間戳

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

uint32_t rcv_time_offset; 應用程式詢問其第一個時間戳時的排程時間,這裡詢問意指擷取接收到的資料包—此應該指開始接收資料時的排程器時間

uint32_t rcv_ts_offset;第一個流的時間戳----此應該指第一 個rtp 包到來時其流上帶的時間戳值

uint32_t rcv_query_ts_offset;被應用程 序詢問的第一個user時間戳—此應該指應用接收資料流時的時間

uint32_t rcv_last_ts; 應用程式得到的流的最後一個時間戳—此應該指應用程式收到的最後一個rtp 包的時間戳,是包裡的時間戳值, 而非應用自己的時間。

uint32_t rcv_last_app_ts; 被應用 程式詢問的最後一個應用程式時間戳—此處應該指應用收最後一個包時的應用時間,是應用 按照 payload 類型及其采樣率增長的時間戳記錄,不是系統時間,也不是包裡的時間

uint32_t rcv_last_ret_ts; 最後一個傳回的采樣的時間戳,僅僅對于連續的音頻而言

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

uint32_t hwrcv_extseq; 在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;最後一個接收到的 sr 的 NTP 時間戳,取的是中間的 32bit。這個值也是報告包中上 LSR 值的來 源。

struct timeval last_rcv_SR_time;最後一個 sr 被 接收到的時間,這個時間是用系統目前的時間來表示的。這個值記錄了接收到最後一個SR時的系統時間,再發送目前報告包時,再次擷取系統目前時間,然後二者相減,得到的值乘 以65536 得到以1/65536 為機關的時間值。

uint16_t snd_seq; 發送序列号。累加變量,儲存會話的序列号的 增長。

uint32_t last_rtcp_report_snt_r;最後一個rtcp 報告發送的時間,按照接收時間戳機關。程式中這個值是用 rcv_last_app_ts變量的值來更新的。就是應用最後一次進行 rtp 接收時其時間戳增長到的值。 不管收沒收到就是這個值了?

uint32_t last_rtcp_report_snt_s;最後一個rtcp報告發送的時間,按照發送時間戳機關。程式中這個值是用snd_last_ts變量的值來更新的,就是應用最後一次進行rtp 發送操作時其時間戳增長到的值。不管有沒 有發送 rtcp 報告包出去?

uint32_t rtcp_report_snt_interval; 按照時間戳機關表示的 rtcp 報告發送的間隔。這個值程式中使用預設時間值 5 秒與 payload的 clockrate 的乘積來表示。是不是計算過于簡單了?

uint32_t last_rtcp_packet_count; 在最後 發送的一個rtcp sr包中記錄的發送者發送的 rtp 包總數。這個變量把這個值記錄了下來。記錄這個值是為了實作協定中規定的:如果之前的rtcp 包發送之後到目前再次發送 rtcp 包, 這期間如果發送了rtp 包,則發送rtcp SR 報告包,否則隻需發送 rtcp RR 包就可以了。

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

上面這些時間相關變量都是用于rtcp 包的。 unsigned int sent_bytes; 用于帶寬評估struct timeval send_bw_start; 同上 上面兩個變量用于計算發送帶寬,start記錄的開始時間,sent_bytes 記錄了發送的位元組數,該值沒調用 rtp 接口發送資料後都會進行累加更新。記錄一次帶寬值後,清為零,之後進行 下一次帶寬估計的計算。

unsigned int recv_bytes; 同上struct timeval recv_bw_start; 同上 作用和處理邏輯都同上面發送部分。

四: 排程的實作 要使用 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

為了更好的了解上面的計算式,我們來看上述幾個值是如何計算出來的。首先,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 目錄下的檔案拿到安裝目錄下,跟庫一起編譯出來可執行檔案進行測試。

測試問題清單:

1 測試程式預設為測試音頻流,沒有包含視訊流,時間戳步進值為 160,就是按照20ms 采樣周期加 8KHZ 采樣率計算出來的。這就導緻接收和發送視訊流的速度都特别慢。是以 接收視訊資料時需要調整步進值為3600,即 90000 采樣率加 25 幀每秒計算得來。實際使用 過程中可根據測試效果進行調整。

2 單獨調整第一條還不能完全解決速率問題,還需要調整 payload type。接收過程可以根 據接收的類型發生改變(即會産生payload type changed 事件),進而對此作出相應調整,故 對接收過程影響不大,但是發送過程因為預設payload 類型設定為了 payload_type_pcmu8000,導緻發送速度很慢,(基于 8KHZ 采樣率)調整為33 (payload_type_mp2v)即可。

3 33在 oRTP 中預設并沒有添加支援,可仿照 avprofile.c 檔案中的實作單獨添加。

4測試中發現 multiple recv 測試程式接收不到資料,将檔案操作接口換為fopen,及 ascii 标準類型的檔案讀寫接口即可,原因待查。

5 多會話接收目前是按照端口号不同來進行的。

6 關閉排程模式和 block 模式可以加速視訊流的推送 

7 在沒有排程器的情況下,控制視訊流發送速度,可以改善馬賽克情況。其實發送過快也不行,用 vlc 播放測試來看。 

8 在啟動排程器的情況下,設定應用時間戳增量值(user_ts)或者調整該變量也可以調整視訊流的發送速度,調整馬賽克情況。 

9 資料發送速率與采樣率,user_ts 時間戳值增加以及buffer 大小均有關系。 

10 在并行發送和接收測試時(msend、mrecv),需要設定為非阻塞模式,否則程式可能會被卡死。 

十: 參考

1 RFC3550RTP: A Transport Protocol for Real-Time Applications 2 RFC3551RTP Profile for Audio and Video Conferences with Minimal Control 3 RTCP 發送間隔分析

繼續閱讀