為滿多媒體應(yīng)用傳輸實時數(shù)據(jù)的需要, IETF RFC 3550 定義了實時傳輸協(xié)議RTP: A Transport Protocol for Real-Time Applications 即為實時應(yīng)用程序所定義的傳輸協(xié)議, 它為交互式音頻和視頻聊天和會議應(yīng)用提供端到端的傳輸服務(wù).
RTP 解決的問題
讓我們先想想實時傳輸需要解決哪些問題:
- 順序 Sequence
多媒體數(shù)據(jù)包需要保序, 否則就會前言不搭后語, 讓人不知所云, 通常我們可以用 sequenceNumber 來標(biāo)識數(shù)據(jù)包的順序, 通過它我們可以知道:
- 是否有數(shù)據(jù)包丟失
- 是否發(fā)生數(shù)據(jù)包亂序
- 是否需要進(jìn)行無序解碼
- 時間和緩存 Timstamp and buffer
我們還需要知道數(shù)據(jù)包的時間, 在回放語音和視頻時需要按規(guī)定的時間線播放并操持音視頻同步, 所以一個 timestamp 是必需的, 通過它我們可以用來
- 回放
- 計算網(wǎng)絡(luò)抖動和延遲
- 載荷類型辨識 Payload type
我們需要知道數(shù)據(jù)包里承載的是什么內(nèi)容, 媒體內(nèi)容是音頻還是視頻, 是什么編碼類型, 所以還需要一個 payload type
- 錯誤隱藏 Error concealment
錯誤總是難以避免的, 特別是在網(wǎng)絡(luò)基于 UDP 的傳輸, 當(dāng)發(fā)現(xiàn)網(wǎng)絡(luò)丟包, 延遲, 亂序時我們要采取一些錯誤隱藏技術(shù), 比如在相鄰幀中添加冗余來掩蓋丟包的錯誤, 或者自動插入一些重復(fù)數(shù)據(jù)包
- 服務(wù)質(zhì)量反饋 QoS feedback
當(dāng)語音或視頻質(zhì)量不佳時, 接收端需要告訴發(fā)送端做出調(diào)整, 或者調(diào)整發(fā)送速率或分辨率, 或者重新發(fā)送關(guān)鍵幀等, 這就需要一些度量報告, 比如 接收者報告RR(Receiver Report ) 和發(fā)送者報告SR(Sender Report)。
根據(jù)上述的問題和需求, RTP 協(xié)議由此制訂出來, 它主要包括兩塊
- 承載具有實時性質(zhì)的數(shù)據(jù)的實時傳輸協(xié)議RTP实蔽。
- 在進(jìn)行的會話中監(jiān)視服務(wù)質(zhì)量并傳輸會話參與者信息的實時傳輸控制協(xié)議RTCP枷颊。
1. RTP
RTP 通忱晕Γ基于 UDP 傳輸, 因為多媒體數(shù)據(jù)可以忍受少量丟包, 卻不能忍受數(shù)據(jù)包延時過大, 如圖所示:
它的數(shù)據(jù)包格式如下:
包頭詳細(xì)解釋如下:
- V(version) 版本號, 2 個比特位, 當(dāng)前版本為2
- P(pad) 填充位, 1個比特位, 當(dāng)它為1時表示在數(shù)據(jù)包的末尾包含一個或多個不屬于載荷的填充字節(jié), 填充的最后一個字節(jié)包含一個計數(shù)值, 表示所填充的字節(jié)數(shù)
- X (eXtenstion) 擴(kuò)展位, 1個比特位, 當(dāng)它為1時表示存在一個擴(kuò)展頭
- CC(Contribution Count) , 貢獻(xiàn)源的數(shù)量, 4個比特位, 定義其后的CSRC(Contributing Source) 的數(shù)量, 若無貢獻(xiàn)源(這路多媒體數(shù)據(jù)沒有合并自其他數(shù)據(jù)源), 則此項為零.
- M(Marker) 標(biāo)記位, 1個比特位, 不同的 RTP Profile 配置有不同定義, 一個應(yīng)用數(shù)據(jù)幀可能分割成若干個RTP 數(shù)據(jù)包, 這個字段用來在某個RTP數(shù)據(jù)包中標(biāo)記應(yīng)用數(shù)據(jù)幀是否開始或結(jié)束
- PT(Payload Type) 載荷類型, 7個比特位, 標(biāo)識RTP 載荷的格式, 表示一個RTP 數(shù)據(jù)包中所承載的內(nèi)容到底是什么, 是音頻還是視頻, 是什么編碼
- Sequene Number 順序號, 16個比特位, 順序號的起始值是隨機(jī)的, 發(fā)送者每發(fā)送一個RTP數(shù)據(jù)包, 順序號就會加1, 接收者可以通過它來檢查是否有丟包和亂序, 由應(yīng)用程序來決定如何應(yīng)對
- Timestamp 時間戳, 32個比特位, 表示 RTP數(shù)據(jù)包中第一個字節(jié)的采樣時間, 采樣時間是單調(diào)和線性增長, 可以通過它來做同步和計算抖動, 它并不是我們通常所說的一天中的某個時間,而是從一個隨機(jī)的時間戳開始以一個相對值不斷增加的采樣個數(shù), 比如最常見的音頻編碼G.711 PCMU的采樣率是8000 Hz, 采樣間隔為125微秒, 一個數(shù)據(jù)包的長度是20ms, 其實包含了20/0.125=160個采樣, 一秒鐘就有1000ms/20ms=50幀, 時間戳每個包就會增長160, 每秒增長160*50=8000, 在多媒體回放時需要以它為參考。
- SSRC(Synchronization Source) 同步源標(biāo)識符, 32個比特位, 它在一個RTP 會話中唯一表示RTP的一個數(shù)據(jù)源, 比如麥克風(fēng), 攝像頭這樣的信號源, 也可能是一個混合器, 它作為一個中間設(shè)備將多個源混合在一起
- CSRC(Contribution Source) 貢獻(xiàn)源列表, 是n 個32比特位, n 是前面的 CC 字段指定的, 不超過2^4=16個, 它表示包含在這個數(shù)據(jù)包中的載荷的貢獻(xiàn)源, 比如我們在音頻會議中聽到三個人在討論問題, 這個數(shù)據(jù)包中包含了這三路語音混合在一起的音頻數(shù)據(jù), 這個貢獻(xiàn)源列表就有三個, 包含代表每個人的麥克風(fēng)的同步源析二。
RTP 標(biāo)準(zhǔn)頭之后就是載荷了, 如圖所示:
SDP 描述如下:
v=0
o=sample 496886 497562 IN IP4 127.0.0.1
s=sammple_rtp_session
c=IN IP4 127.0.0.1
t=0 0
m=audio 18276 RTP/AVP 102 13 98 99
a=sprop-source:1 csi=932617472;simul=1
a=sprop-simul:1 1 *
a=recv-source 1,2,3
a=rtpmap:102 iLBC/8000
a=rtpmap:13 CN/8000
a=rtpmap:98 CN/16000
a=rtpmap:99 CN/32000
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
a=ptime:20
由此可知數(shù)據(jù)包長度共為 118 字節(jié), 其中:
- 數(shù)據(jù)鏈接層: 16 字節(jié)
- IP 數(shù)據(jù)包頭: 20 字節(jié)
- UDP 數(shù)據(jù)包頭: 8 字節(jié), 其載荷為RTP數(shù)據(jù)包:
- RTP 數(shù)據(jù)包: 74 字節(jié)
- RTP 數(shù)據(jù)包頭: 12 字節(jié)
- RTP 擴(kuò)展頭: 4字節(jié)
- RTP 載荷: 58字節(jié)
- RTP 數(shù)據(jù)包: 74 字節(jié)
2. RTCP
RTCP 實時傳輸控制協(xié)議, 它的主要目的就是基于度量來控制 RTP 的傳輸來改善實時傳輸?shù)男阅芎唾|(zhì)量, 它主要有5種類型的RTCP包:
- RR接收者報告Receiver Report
- SR發(fā)送者報告 Sender Report
- SDES數(shù)據(jù)源描述報告 Source DEScription
- BYE 告別報告 Goodbye
- APP 應(yīng)用程序自定義報告 Application-defined packet
RR, SR, SDES 可用來匯報在數(shù)據(jù)源和目的之間的多媒體傳輸信息, 在報告中包含一些統(tǒng)計信息, 比如 RTP包 發(fā)送的數(shù)量, RTP包丟失的數(shù)量, 數(shù)據(jù)包到達(dá)的抖動, 通過這些報告, 應(yīng)用程序就可以修改發(fā)送速率, 也可做一些其他調(diào)整以及診斷。
它也是基于 UDP 包進(jìn)行傳輸:
RTCP 的數(shù)據(jù)包格式如下:
版本(V):同RTP包頭域。
填充(P):同RTP包頭域钥勋。
接收報告計數(shù)器(RC):5比特,該SR包中的接收報告塊的數(shù)目辆苔,可以為零算灸。
包類型(PT):8比特,SR包是200驻啤。
長度域(Length):16比特菲驴,其中存放的是該SR包以32比特為單位的總長度減一。
同步源(SSRC):SR包發(fā)送者的同步源標(biāo)識符骑冗。與對應(yīng)RTP包中的SSRC一樣赊瞬。
NTP Timestamp(Network time protocol)SR包發(fā)送時的絕對時間值先煎。NTP的作用是同步不同的RTP媒體流。1900.1.1至今的秒數(shù)64bits: 32 bits 整數(shù)部分 + 32 bits 小數(shù)部分
RTP Timestamp:與NTP時間戳對應(yīng)巧涧,與RTP數(shù)據(jù)包中的RTP時間戳具有相同的單位和隨機(jī)初始值榨婆。
Sender’s packet count:從開始發(fā)送包到產(chǎn)生這個SR包這段時間里,發(fā)送者發(fā)送的RTP數(shù)據(jù)包的總數(shù). SSRC改變時褒侧,這個域清零良风。
Sender`s octet count:從開始發(fā)送包到產(chǎn)生這個SR包這段時間里,發(fā)送者發(fā)送的凈荷數(shù)據(jù)的總字節(jié)數(shù)(不包括頭部和填充)闷供。發(fā)送者改變其SSRC時烟央,這個域要清零。
同步源n的SSRC標(biāo)識符:該報告塊中包含的是從該源接收到的包的統(tǒng)計信息歪脏。
丟失率(Fraction Lost):表明從上一個SR或RR包發(fā)出以來從同步源n(SSRC_n)來的RTP數(shù)據(jù)包的丟失率疑俭。
Cumulative number of packets lost 24bits:
累計的包丟失數(shù)目:從開始接收到SSRC_n的包到發(fā)送SR,從SSRC_n傳過來的RTP數(shù)據(jù)包的丟失總數(shù)。Extended highest sequence number received: 32 bits
收到的擴(kuò)展最大序列號:從SSRC_n收到的RTP數(shù)據(jù)包中最大的序列號婿失,EHSN = ROC*2^16, ROC 指 Sequence Number 重置回滾的次數(shù)(因為SN只有16位,很容易就會超過 2^16 的最大值, 只好再從零開始,這個重新回到零的次數(shù)即 ROC - ROll Count)Interarrival jitter: 32 bits
接收抖動,RTP數(shù)據(jù)包接受時間的統(tǒng)計方差估計Last SR (LSR): 32 bits
取最近從SSRC_n收到的SR包中的NTP時間戳的中間32比特钞艇。如果目前還沒收到SR包,則該域清零豪硅。Delay since last SR (DLSR) : 32 bits
上次SR以來的延時, 即上次從SSRC_n收到SR包到發(fā)送本報告的延時哩照。
RR(n) – SR(n)
單位: 1/65536 s
3. RTP 協(xié)議的度量要點(diǎn)
通過RTP 數(shù)據(jù)包頭和 RTCP 報告, 我們能夠度量 RTP 傳輸?shù)娜齻€主要度量指標(biāo) 往返延遲RTT, 丟包Packet Loss 和 抖動Jitter
1) 往返延時RTT
往返延時RTT 很好理解, 也就是數(shù)據(jù)包在發(fā)送和接收雙方走一個來回所花費(fèi)的時間.
當(dāng)延時在150ms以下時,通話雙方幾乎不能感覺到延時的存在懒浮,而當(dāng)延時在400ms以下時飘弧,也是用戶能夠接受的,當(dāng)延時進(jìn)一步增大后砚著,達(dá)到800ms以上次伶,正常的通話就無法進(jìn)行.
接受者報告RR可用來估算在發(fā)送者和接收者之間的往返延遲 RTT, 在接收者報告中包含:
- LSR(Last timestamp Sener Report received) 上一次發(fā)送者報告接收的時間
- DLSR(Delay since last sender report received) 上一次發(fā)送者報告接收的延遲
往返時延RTT 計算公式如下:
RTT = T1 – LSR - DLSR
看一個例子
[10 Nov 1995 11:33:25.125 UTC] [10 Nov 1995 11:33:36.5 UTC]
n SR(n) A=b710:8000 (46864.500 s)
---------------------------------------------------------------->
v ^
ntp_sec =0xb44db705 v ^ dlsr=0x0005:4000 ( 5.250s)
ntp_frac=0x20000000 v ^ lsr =0xb705:2000 (46853.125s)
(3024992005.125 s) v ^
r v ^ RR(n)
---------------------------------------------------------------->
|<-DLSR->|
(5.250 s)
A 0xb710:8000 (46864.500 s)
DLSR -0x0005:4000 ( 5.250 s)
LSR -0xb705:2000 (46853.125 s)
-------------------------------
delay 0x0006:2000 ( 6.125 s)
- A 是 RR 接收的時間, NTP timstamp 的32位表示為 0xb710:8000稽穆, 即 46864.500 s
- DLSR 上 RR 中所記錄的上次收到 SR 到這次發(fā)送 RR 所經(jīng)歷的時間冠王,單位是 1/65536 秒, 算出為 0x0005:4000 即 5.250 s
- LSR 是上次 SR 發(fā)送的時間舌镶,ntp_sec =0xb44db705柱彻, ntp_frac=0x20000000, 取中間32bit 為 0xb705:2000 即 46853.125 s
結(jié)果為 A - DLSR -LSR = 46864.500 - 5.250 - 46853.125 = 6.125 s
2) 抖動Jitter
在理想情況下, RTP數(shù)據(jù)包到達(dá)的間隔是固定的, 比如IP電話中最常用的編碼g.711, 每個包的荷載長度為20毫秒, 每秒應(yīng)該有50個數(shù)據(jù)包, 但是實際上網(wǎng)絡(luò)并不總能穩(wěn)定傳輸?shù)? 阻塞,擁塞是常有的事, Jitter 抖動即指數(shù)據(jù)到達(dá)間隔的變化,如圖所示:
抖動的計算要稍微麻煩一點(diǎn), 為避免偶發(fā)的波動造成抖動的計算偏差, 它被定義為 RTP 數(shù)據(jù)包到達(dá)間隔時間的統(tǒng)計方差乎折。
首先計算數(shù)據(jù)包接收與發(fā)送時間間隔的差別绒疗,也就是兩個 packet 傳輸延時的差異侵歇,此時所計算的 jitter 時只用到相鄰兩個 packet 的 delta骂澄, 反映的是某一時刻網(wǎng)絡(luò)延遲的變化情況
而如下的到達(dá)間隔抖動 J 定義為差值的平均偏差(平滑后的絕對值)。之所以要除以 16 是為了減少大的隨機(jī)變化的影響惕虑。 所以到達(dá)間隔時間的變化需要重復(fù)幾次才能顯著地影響抖動的估計
抖動是不可避免的, 在合理區(qū)間的抖動是可以接受, 通常采用抖動緩沖 Jitter Buffer 來解決抖動的問題, 數(shù)據(jù)包接收之后并不馬上解碼, 而是先放在緩沖區(qū)中. 假設(shè)緩沖區(qū)深度是60ms, 那么解碼總是等到緩沖區(qū)中的若干數(shù)據(jù)包總長度達(dá)到60ms時才取出解碼, 在60ms 之內(nèi)的抖動自然沒有任何影響.
3) 丟包 Packet loss
如果一個UDP 包在網(wǎng)絡(luò)上由于擁塞或超時而丟失了坟冲,或者一個TCP數(shù)據(jù)包的延時過大, 超過了最大的抖動緩沖深度, 應(yīng)用程序也就不會再等待, 直接丟棄, 這時候磨镶,我們要么采用丟包補(bǔ)償策略進(jìn)行處理, 要么發(fā)消息讓發(fā)送方重傳.
Packet loss丟包率的公式很簡單
- 丟失的包數(shù) = 期望的包數(shù) - 收到的包數(shù)
- 期望的包數(shù) = 最大sequence number – 初始的 sequence number
- 最大sequence number = sequence number循環(huán)次數(shù) * + 最后收到的 sequence number
根據(jù)上述度量指標(biāo), 多媒體應(yīng)用程序可以即時調(diào)整 Jitter Buffer 長度,編碼參數(shù), 分辨率, 或者發(fā)送速率等, 為用戶提供流暢的體驗.
4. 實例
這里寫一個小程序來打印 RTP 包頭健提,使用 ffmpeg 來推送一個 RTP 流到 8880 端口琳猫,然后寫一個簡單的 UDP 服務(wù)器從這個端口上接收RTP 數(shù)據(jù)并打印包頭。
主要代碼很短
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <string>
#include "rtputil.h"
#define BUFLEN 5120
#define PORT 8880
using namespace std;
void exitWithMsg(const char *str)
{
perror(str);
exit(1);
}
int main(void)
{
struct sockaddr_in my_addr, cli_addr;
int sockfd;
socklen_t slen=sizeof(cli_addr);
uint8_t buf[BUFLEN];
if ((sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP))==-1)
exitWithMsg("socket error");
else
printf("Server : Socket() successful\n");
bzero(&my_addr, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(PORT);
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (::bind(sockfd, (struct sockaddr* ) &my_addr, sizeof(my_addr))==-1)
exitWithMsg("bind error");
else
printf("Server : bind() successful\n");
int pktCount = 0;
while(1)
{
int pktSize = recvfrom(sockfd, buf, BUFLEN, 0, (struct sockaddr*)&cli_addr, &slen);
if(pktSize == -1) {
exitWithMsg("recvfrom()");
}
printf("The %d packet received %d from %s:%d\n", ++pktCount, pktSize, inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));
if(pktSize > 12) {
cout << dump_rtp_packet(buf, pktSize) <<endl;
}
}
close(sockfd);
return 0;
}
其中用到的 rtputil 代碼請參見 rtputil.h 和 rtputil.cpp, 主要是借用了 libsrtp 中定義的結(jié)構(gòu)體和工具方法
測試步驟
mkdir bld
cd bld
cmake ..
make
- 啟動 udp server
./udpserver
- 找一個mpegts 文件私痹,打開另外一個終端窗口脐嫂,用 ffmpeg 來推送RTP 流到 8880 端口
(例子文件 https://github.com/walterfan/webrtc_primer/blob/main/material/sintel.ts)
ffmpeg -re -i ./sintel.ts -f rtp_mpegts udp://127.0.0.1:8880
執(zhí)行結(jié)果如下
ffmpeg -re -i ../../material/sintel.ts -f rtp_mpegts udp://127.0.0.1:8880
ffmpeg version 3.3.2 Copyright (c) 2000-2017 the FFmpeg developers
built with Apple LLVM version 8.1.0 (clang-802.0.42)
configuration: --prefix=/usr/local/Cellar/ffmpeg/3.3.2 --enable-shared --enable-pthreads --enable-gpl --enable-version3 --enable-hardcoded-tables --enable-avresample --cc=clang --host-cflags= --host-ldflags= --enable-libmp3lame --enable-libx264 --enable-libxvid --enable-opencl --disable-lzma --enable-vda
libavutil 55. 58.100 / 55. 58.100
libavcodec 57. 89.100 / 57. 89.100
libavformat 57. 71.100 / 57. 71.100
libavdevice 57. 6.100 / 57. 6.100
libavfilter 6. 82.100 / 6. 82.100
libavresample 3. 5. 0 / 3. 5. 0
libswscale 4. 6.100 / 4. 6.100
libswresample 2. 7.100 / 2. 7.100
libpostproc 54. 5.100 / 54. 5.100
Input #0, mpegts, from '../../material/sintel.ts':
Duration: 00:00:26.13, start: 1.446667, bitrate: 1306 kb/s
Program 1
Metadata:
service_name : Service01
service_provider: FFmpeg
Stream #0:0[0x100]: Video: h264 (High) ([27][0][0][0] / 0x001B), yuv420p(progressive), 848x480, 25 fps, 25 tbr, 90k tbn, 50 tbc
Stream #0:1[0x101]: Audio: aac (LC) ([15][0][0][0] / 0x000F), 48000 Hz, stereo, fltp, 151 kb/s
Stream mapping:
Stream #0:0 -> #0:0 (h264 (native) -> mpeg4 (native))
Stream #0:1 -> #0:1 (aac (native) -> aac (native))
Press [q] to stop, [?] for help
Output #0, rtp_mpegts, to 'udp://127.0.0.1:8880':
Metadata:
encoder : Lavf57.71.100
Stream #0:0: Video: mpeg4, yuv420p, 848x480, q=2-31, 200 kb/s, 25 fps, 90k tbn, 25 tbc
Metadata:
encoder : Lavc57.89.100 mpeg4
Side data:
cpb: bitrate max/min/avg: 0/0/200000 buffer size: 0 vbv_delay: -1
Stream #0:1: Audio: aac (LC), 48000 Hz, stereo, fltp, 128 kb/s
Metadata:
encoder : Lavc57.89.100 aac
frame= 653 fps= 25 q=31.0 Lsize= 2385kB time=00:00:26.19 bitrate= 745.9kbits/s dup=1 drop=0 speed=0.992x
video:1675kB audio:413kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 14.238406%
[aac @ 0x7fc976810400] Qavg: 562.469
另外一個窗口顯示總共收到了1845 個 RTP 包
//...
The 1845 packet received 1328 from 127.0.0.1:54824
[rtp] dump_rtp_packet: ssrc=2444272939(0x91B0A52B), seq=3564(0x0DEC), pt=33(0x21), ts=1930119409(0x730B48F1), hdr_ext_len=12, pkt_len=1328, hdr_ext: 80210dec730b48f191b0a52b, tailer: ef3bc778eec0fbc1cf04ee01b0d704eae01709b805c26e3bc31addc8b0c06386db47abbc3196ed9663c50c65f36863bbacb7adbc31d3dc83c31f6dcc31bcff86, tailer_len=64