RTP協議


1、RTP概述

實時傳輸協議(Real-time Transport Protocol)是一種網絡傳輸協議。為IETF提出的一個標志,對應的RFC文檔為RFC3550(RFC1889為過期版本)。RFC3550不僅定義了RTP,而且定義了配套的相關協議RTCP(Real-time Transport Control Protocol,實時傳輸控制協議)。RTP用來為IP網絡上的語音、圖像、傳真等多種需要實時傳輸的多媒體數據提供端到端的實時傳輸服務。RTP為Internet上端到端的實時傳輸提供時間信息和流同步,但並不保證服務質量,服務質量由RTCP來提供。

2、RTP應用環境

RTP用於在單播或多播網絡中傳送實時數據。它們典型的應用場合如下:

  • 簡單的多播音頻會議。語音通信通過一個多播地址和一對端口來實現。一個端口用於音頻數據(RTP),一個端口用於控制包(RTCP)。
  • 音頻和視頻會議。如果在一次會議中同時使用了音頻和視頻會議,這兩種媒體將分別在不同的RTP會話中傳送,每個會話使用不同的傳輸地址(IP地址+端口)。如果一個用戶同時使用了兩個會話,則每個會話對應的RTCP包都使用規范化名字CNAME(Canonical Name)。與會者可以根據RTCP包中的CNAME來獲取相關聯的音頻和視頻,然后根據RTCP包中的計時信息(Network time protocal)來實現音頻和視頻的同步。
  • 翻譯器和混合器。翻譯器和混合器都是RTP級的中繼系統。翻譯器用在通過IP多播不能直接到達用戶區,例如發送者和接收者之間存在防火牆。當與會者能接收的音頻編碼格式不一樣,比如有一個與會者通過一條低速鏈路接入高速會議,這是就要使用混合器。在進入音頻數據格式需要變化的網絡前,混合器將來自一個源或多個源的音頻包進行重構,並把重構后的多個音頻合並,采用另一種音頻編碼進行編碼后,再轉發這個新的RTP包。從一個混合器出來的所有數據包要用混合器作為它們的同步源(SSRC,見RTP封裝)來識別,可以通過貢獻源列表(CSRC表,見RTP的封裝)可確認談話者。

 3、流媒體

流媒體是指Internet上使用流式傳輸技術的連續時基媒體。當前在Internet上傳輸音頻和視頻等信息主要有兩種方式:下載和流式傳輸兩種方式。

下載情況下,用戶需要先下載整個媒體文件到本地,然后才能播放媒體文件。在視頻直播等應用場合,由於生成整個媒體文件要等直播結束,也就是用戶至少要在直播結束后才能看到直播節目,所以用下載方式不能實現直播。

流式傳輸是實現流媒體的關鍵技術。使用流式傳輸可以邊下載邊觀看流媒體節目。由於Internet是基於分組傳輸的,所以接收端收到的數據包往往有延遲和亂序(流式傳輸構建在UDP上)。要實現流式傳輸,就是要從降低延遲和恢復數據包時序入手。在發送端,為降低延遲,往往對傳輸數據進行預處理(降低質量和高效壓縮)。在接收端為了恢復時序,采用了接收緩沖;而為了實現媒體的流暢播放,則采用了播放緩沖。

使用接收緩沖,可以將接收到的數據包緩存起來,然后根據數據包的封裝信息(如包序號和時戳等),將亂序的包重新排序,最后將重新排序了的數據包放入播放緩沖播放。

為什么需要播放緩沖呢?容易想到,由於網絡不可能很理想,並且對數據包排序需要處理時耗,我們得到排序好的數據包的時間間隔是不等的。如果不用播放緩沖,那么播放節目會很卡,這叫時延抖動。相反,使用播放緩沖,在開始播放時,花費幾十秒鍾先將播放緩沖填滿(例如PPLIVE),可以有效地消除時延抖動,從而在不太損失實時性的前提下實現流媒體的順暢播放。

到目前為止,Internet 上使用較多的流式視頻格式主要有以下三種:RealNetworks 公司的RealMedia ,Apple 公司的QuickTime 以及Microsoft 公司的Advanced Streaming Format (ASF) 。

上面在談接收緩沖時,說到了流媒體數據包的封裝信息(包序號和時戳等),這在后面的RTP封裝中會有體現。另外,RealMedia這些流式媒體格式只是編解碼有不同,但對於RTP來說,它們都是待封裝傳輸的流媒體數據而沒有什么不同。

4、RTP的協議層次

4.1 傳輸的子層

RTP(實時傳輸協議),顧名思義,它是用來提供實時傳輸的,因而可以看作是傳輸層的一個子層,下圖給出了流媒體應用中的協議體系結構。

從上圖可以看出,RTP被划分在傳輸層,它建立在UDP之上。同UDP協議一樣,為了實現其傳輸功能,RTP也有固定的封裝格式。

4.2 應用層的一部分

也有人把RTP歸為應用層的一部分,這是從應用開發者的角度來說的。操作系統中TCP/IP等協議所提供的是我們最常用的服務,而RTP的實現還是要考開發者自己。因此,從開發的角度來說,RTP的實現喝應用層協議的實現沒不同,所有將RTP看成應用層協議。

RTP實現者在發送RTP數據時,需先將數據封裝成RTP包,而在接收到RTP數據包,需要將數據從RTP包中提取出來。

5、RTP的封裝

一個協議的封裝時為了滿足協議的功能需求,從前面提出的功能需求,可以推測RTP封裝中應該有同步源喝時間戳等信息。完整的RTP格式如下所示:

上圖引自RFC3550。
由上圖中可知道RTP報文由兩個部分構成:RTP報頭和RTP有效負載。報頭格式如上圖所示,其中:

  1. V:RTP協議的版本號,占2位,當前協議版本號為2。
  2. P:填充標志,占1位,如果P=1,則在該報文的尾部填充一個或多個額外的八位組,它們不是有效載荷的一部分。
  3. X:擴展標志,占1位,如果X=1,則在RTP報頭后跟有一個擴展報頭。
  4. CC:CSIC計數器,占4位,指示CSIC 標識符的個數。
  5. M: 標記,占1位,不同的有效載荷有不同的含義,對於視頻,標記一幀的結束;對於音頻,標記會話的開始。
  6. PT: 有效載荷類型,占7位,用於說明RTP報文中有效載荷的類型,如GSM音頻、JPEM圖像等,在流媒體中大部分是用來區分音頻流和視頻流的,這樣便於客戶端進行解析。
  7. 序列號:占16位,用於標識發送者所發送的RTP報文的序列號,每發送一個報文,序列號增1。這個字段當下層的承載協議用UDP的時候,網絡狀況不好的時候可以用來檢查丟包。同時出現網絡抖動的情況可以用來對數據進行重新排序,在helix服務器中這個字段是從0開始的,同時音頻包和視頻包的sequence是分別記數的。
  8. 時戳(Timestamp):占32位,時戳反映了該RTP報文的第一個八位組的采樣時刻。接收者使用時戳來計算延遲和延遲抖動,並進行同步控制。
  9. 同步信源(SSRC)標識符:占32位,用於標識同步信源。該標識符是隨機選擇的,參加同一視頻會議的兩個同步信源不能有相同的SSRC。
  10. 貢獻信源(CSRC)標識符:貢獻源列表,0~15項,每個項32bit,用來標志對一個RTP混合器產生的新包所有RTP包的源。由混合器將這些由貢獻的SSRC標志符插入表中。SSRC標志符都被列出來,以便接收端能正確指出交談雙方的身份。

如果擴展標志被置位則說明緊跟在報頭后面是一個頭擴展,其格式如下:

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |      defined by profile       |           length              |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                        header extension                       |
   |                             ....                              |


下圖為某網關上抓取的RTP報文:

參數說明:
v=2
p=0
x=0
cc=0
m=0
pt=ITU-T G.711 PCMU(0) //即pcmu語音編碼
seq=8004
timestamp=1157612680
ssrc=0xcd934761
payload=27... //為實際負載。

並且可以看到,RTP報文由UDP報文承載,UDP由IP報文承載。

 6、RTCP的封裝

RTP需要RTCP為其服務器質量提供保證。RTCP的主要功能是:服務質量的監視與反饋、媒體間的同步,以及多播組中成員的標識。在RTP會話期間,各參與者周期性地傳送RTCP包。RTCP包中含有已發送的數據包的梳理、丟失的數據包數量等統計資料。因此,各參與者可以利用這些信息動態地修改傳輸速率,甚至改變有效載荷類型。RTP和RTCP配合使用,它們能以有效的反饋和最小的開銷使傳輸效率最佳化,因而特別適合傳送網上的實時數據。

RTCP也是用UDP報文來傳送的,但RTCP封裝的僅僅是一些控制信息,因而分組很短,所有可以將多個RTCP分組封裝在一個UDP報文中。RTCP由如下五種分組類型:

上述五種分組的封裝大同小異,下面只講訴SR類型,其他類型請參考RFC5330.

發送端報告分組SR(Sender Report)用來使發送端以多播方式向所有接收端報告發送情況。SR分組的主要內容有:相應的RTP流的SSRC,RTP流中最新產生的RTP分組的時間戳和NTP,RTP流包含的分組數,RTP流包含的字節數。SR包的封裝如下所示。

  • 版本(V):同RTP包頭域。
  • 填充(P):同RTP包頭域。
  • 接收報告計數器(RC):5比特,該SR包中的接收報告塊的數目,可以為零。
  • 包類型(PT):8比特,SR包是200。
  • 長度域(Length):16比特,其中存放的是該SR包以32比特為單位的總長度減一。
  • 同步源(SSRC):SR包發送者的同步源標識符。與對應RTP包中的SSRC一樣。
  • NTP Timestamp(Network time protocol)SR包發送時的絕對時間值。NTP的作用是同步不同的RTP媒體流。
  • RTP Timestamp:與NTP時間戳對應,與RTP數據包中的RTP時間戳具有相同的單位和隨機初始值。
  • Sender’s packet count:從開始發送包到產生這個SR包這段時間里,發送者發送的RTP數據包的總數. SSRC改變時,這個域清零。
  • Sender`s octet count:從開始發送包到產生這個SR包這段時間里,發送者發送的凈荷數據的總字節數(不包括頭部和填充)。發送者改變其SSRC時,這個域要清零。
  • 同步源n的SSRC標識符:該報告塊中包含的是從該源接收到的包的統計信息。
  • 丟失率(Fraction Lost):表明從上一個SR或RR包發出以來從同步源n(SSRC_n)來的RTP數據包的丟失率。
  • 累計的包丟失數目:從開始接收到SSRC_n的包到發送SR,從SSRC_n傳過來的RTP數據包的丟失總數。
  • 收到的擴展最大序列號:從SSRC_n收到的RTP數據包中最大的序列號。
  • 接收抖動(Interarrival jitter):RTP數據包接受時間的統計方差估計。
  • 上次SR時間戳(Last SR,LSR):取最近從SSRC_n收到的SR包中的NTP時間戳的中間32比特。如果目前還沒收到SR包,則該域清零。
  • 上次SR以來的延時(Delay since last SR,DLSR):上次從SSRC_n收到SR包到發送本報告的延時。

圖為某網關上抓取的RTCP報文:

7、RTP會話過程

當應用程序建立一個RTP會話時,應用程序將確定一對目的傳輸地址。目的傳輸地址由一個網絡地址和一對端口組成,有兩個端口:一個給RTP包,一個給RTCP包,使得RTP/RTCP數據能夠正確發送。RTP數據發向偶數的UDP端口,而對應的控制信號RTCP數據發向相鄰的奇數UDP端口(偶數的UDP端口+1),這樣就構成一個UDP端口對。 RTP的發送過程如下,接收過程則相反。

  1. RTP協議從上層接收流媒體信息碼流(如H.263),封裝成RTP數據包;RTCP從上層接收控制信息,封裝成RTCP控制包。
  2. RTP將RTP 數據包發往UDP端口對中偶數端口;RTCP將RTCP控制包發往UDP端口對中的接收端口。

8、RTP Payload

RTP Packet = RTP Header + RTP Payload。

RTP Payload結構一般分為3種:

  1. 單NALU分組(Single NAL Unit Packet):一個分組只包含一個NALU。
  2. 聚合分組(Aggregation Packet):一個分組包含多個NALU。
  3. 分片分組(Fragmentation Unit):一個比較長的NALU分在多個RTP包中。

各種RTP分組在RTP Header后面跟着F|NRI|Type結構的NALU Header來判斷分組類型。不容分組類型此字段名字可能不同,H264/HEVC原始視頻流NALU也包含此結構的頭部字段。

  1. F(forbidden_zero_bit):錯誤位或語法沖突標志,一般設為0。
  2. NRI(nal_ref_idc): 與H264編碼規范相同,此處可以直接使用原始碼流NRI值。
  3. Type:RTP載荷類型,1-23:H264編碼規定的數據類型,單NALU分組直接使用此值,24-27:聚合分組類型(聚合分組一般使用24 STAP-A),28-29分片分組類型(分片分組一般使用28FU-A),30-31,0保留。

8.1 單NALU分組

此結構的NALU Header結構可以直接使用原始碼流NALU Header,所以單NALU分組Type = 1~23。封裝RTP包的時候可以直接把 查詢到的NALU去掉起始碼后的部分 當作單NALU分組的RTP包Payload部分。

8.2 聚合分組

通常采用STAP-A (Type=24)結構封裝RTP聚合分組,下圖為包含2個NALU的采用STAP-A結構的聚合分組。

  • STAP-A NAL HDR: 也是一個NALU Header (F|NRI|Type)結構,1字節。比如可能值為0x18=00011000b,Type=11000b=24,即為STAP-A。所有聚合NALU的F只要有一個為1則設為1,NRI取所有NALU的NRI最大值。
  • NALU Size: 表示此原始碼流NALU長度,2字節。
  • NALU HDR + NALU Date: 即為原始碼流一個完整NALU。

8.3 分片分組

通常采用無DON字段的FU-A結構封裝RTP分片分組。各種RTP分組在RTP Header后面都跟着 F|NRI|Type 結構,來判定分組類型。

FU indicator

采用FU-A分組類型的話Type = 28,NRI與此NALU中NRI字段相同。

FU header

 

此結構中Type采用原始碼流NALU中的Type字段,S=1表示這個RTP包為分片分組第一個分片,E=1表示為分片分組最后一個分片。除了首尾分片,中間的分片S&E都設為0。R為保留位,設為0。

9、RTP封裝H.264碼流示例程序

這個示例程序是參考ffmpeg的代碼,實現了讀取一個Sample.h264裸流文件,(打算以后支持HEVC/H.265所以文件名有HEVC),通過ffmpeg內置的函數查找NAL單元起始碼,從而獲取一個完整的NALU。根據NALU長度選擇RTP打包類型,然后再組裝RTP頭部信息,最終發送到指定IP和端口,本例發送到本機1234端口。

程序文件:

  • main.c: 函數入口
  • RTPEnc.c: RTP封裝實現
  • Network.c: UDP socket相關
  • AVC.c: 查找NALU起始碼函數,copy自ffmpeg
  • Utils: 讀取文件以及copy指定長度的內存數據

main.c

#include <stdio.h>
#include <string.h>
#include "Utils.h"
#include "RTPEnc.h"
#include "Network.h"

int main() {

    int len = 0;
    int res;
    uint8_t *stream = NULL;
    const char *fileName = "../Sample.h264";

    RTPMuxContext rtpMuxContext;
    UDPContext udpContext = {
        .dstIp = "127.0.0.1",   // 目的IP
        .dstPort = 1234         // 目的端口
    };

    // 讀整個文件到buff中
    res = readFile(&stream, &len, fileName);
    if (res){
        printf("readFile error.\n");
        return -1;
    }

    // create udp socket
    res = udpInit(&udpContext);
    if (res){
        printf("udpInit error.\n");
        return -1;
    }
    
    // 設置RTP Header默認參數
    initRTPMuxContext(&rtpMuxContext);
    // 主要業務邏輯
    rtpSendH264HEVC(&rtpMuxContext, &udpContext, stream, len);

    return 0;
}

RTPEnc.h

#ifndef RTPSERVER_RTPENC_H
#define RTPSERVER_RTPENC_H

#include "Network.h"

#define RTP_PAYLOAD_MAX     1400

typedef struct {
    uint8_t cache[RTP_PAYLOAD_MAX+12];  //RTP packet = RTP header + buf
    uint8_t buf[RTP_PAYLOAD_MAX];       // NAL header + NAL
    uint8_t *buf_ptr;

    int aggregation;   // 0: Single Unit, 1: Aggregation Unit
    int payload_type;  // 0, H.264/AVC; 1, HEVC/H.265
    uint32_t ssrc;
    uint32_t seq;
    uint32_t timestamp;
}RTPMuxContext;

int initRTPMuxContext(RTPMuxContext *ctx);

/* send a H.264/HEVC video stream */
void rtpSendH264HEVC(RTPMuxContext *ctx, UDPContext *udp, const uint8_t *buf, int size);

#endif //RTPSERVER_RTPENC_H

RTPEnc.c

#include <stdint.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "RTPEnc.h"
#include "Utils.h"
#include "AVC.h"
#include "Network.h"

#define RTP_VERSION 2
#define RTP_H264    96

static UDPContext *gUdpContext;


int initRTPMuxContext(RTPMuxContext *ctx){
    ctx->seq = 0;
    ctx->timestamp = 0;
    ctx->ssrc = 0x12345678; // 同源標志,可以設置隨機數
    ctx->aggregation = 1;   // 當NALU長度小於指定長度時,是否采用聚合分組進行打包,否則使用單NALU分組方式打包
    ctx->buf_ptr = ctx->buf;  // buf存放除RTP Header的內容
    ctx->payload_type = 0;  // 當前版本只支持H.264
    return 0;
}

// enc RTP packet
void rtpSendData(RTPMuxContext *ctx, const uint8_t *buf, int len, int mark)
{
    int res = 0;

    /* build the RTP header */
    /*
     *
     *    0                   1                   2                   3
     *    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     *   |V=2|P|X|  CC   |M|     PT      |       sequence number         |
     *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     *   |                           timestamp                           |
     *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     *   |           synchronization source (SSRC) identifier            |
     *   +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
     *   |            contributing source (CSRC) identifiers             |
     *   :                             ....                              :
     *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     *
     **/

    uint8_t *pos = ctx->cache;
    pos[0] = (RTP_VERSION << 6) & 0xff;      // V P X CC
    pos[1] = (uint8_t)((RTP_H264 & 0x7f) | ((mark & 0x01) << 7)); // M PayloadType
    Load16(&pos[2], (uint16_t)ctx->seq);    // Sequence number
    Load32(&pos[4], ctx->timestamp);
    Load32(&pos[8], ctx->ssrc);

    // 復制RTP Payload
    memcpy(&pos[12], buf, len);
    // UDP socket發送
    res = udpSend(gUdpContext, ctx->cache, (uint32_t)(len + 12));
    printf("\nrtpSendData cache [%d]: ", res);
    for (int i = 0; i < 20; ++i) {
        printf("%.2X ", ctx->cache[i]);
    }
    printf("\n");

    memset(ctx->cache, 0, RTP_PAYLOAD_MAX+10);

    ctx->buf_ptr = ctx->buf;  // buf_ptr為buf的游標指針

    ctx->seq = (ctx->seq + 1) & 0xffff; // RTP序列號遞增
}

// 拼接NAL頭部 在 ctx->buff, 然后調用ff_rtp_send_data
static void rtpSendNAL(RTPMuxContext *ctx, const uint8_t *nal, int size, int last){
    printf("rtpSendNAL  len = %d M=%d\n", size, last);

    // Single NAL Packet or Aggregation Packets
    if (size <= RTP_PAYLOAD_MAX){

        // 采用聚合分組
        if (ctx->aggregation){
            /*
             *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
             *  |STAP-A NAL HDR | NALU 1 Size | NALU 1 HDR & Data | NALU 2 Size | NALU 2 HDR & Data | ... |
             *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
             *
             * */
            int buffered_size = (int)(ctx->buf_ptr - ctx->buf);  // size of data in ctx->buf
            uint8_t curNRI = (uint8_t)(nal[0] & 0x60);           // NAL NRI

            // The remaining space in ctx->buf is less than the required space
            if (buffered_size + 2 + size > RTP_PAYLOAD_MAX) {
                rtpSendData(ctx, ctx->buf, buffered_size, 0);
                buffered_size = 0;
            }

            /*
             *    STAP-A/AP NAL Header
             *     +---------------+
             *     |0|1|2|3|4|5|6|7|
             *     +-+-+-+-+-+-+-+-+
             *     |F|NRI|  Type   |
             *     +---------------+
             * */
            if (buffered_size == 0){
                *ctx->buf_ptr++ = (uint8_t)(24 | curNRI);  // 0x18
            } else {  // 設置STAP-A NAL HDR
                uint8_t lastNRI = (uint8_t)(ctx->buf[0] & 0x60);
                if (curNRI > lastNRI){  // if curNRI > lastNRI, use new curNRI
                    ctx->buf[0] = (uint8_t)((ctx->buf[0] & 0x9F) | curNRI);
                }
            }

            // set STAP-A/AP NAL Header F = 1, if this NAL F is 1.
            ctx->buf[0] |= (nal[0] & 0x80);

            // NALU Size + NALU Header + NALU Data
            Load16(ctx->buf_ptr, (uint16_t)size);   // NAL size
            ctx->buf_ptr += 2;
            memcpy(ctx->buf_ptr, nal, size);        // NALU Header & Data
            ctx->buf_ptr += size;

            // meet last NAL, send all buf
            if (last == 1){
                rtpSendData(ctx, ctx->buf, (int)(ctx->buf_ptr - ctx->buf), 1);
            }
        }
        // 采用單NALU分組
        else {
            /*
             *   0 1 2 3 4 5 6 7 8 9
             *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
             *  |F|NRI|  Type   | a single NAL unit ... |
             *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
             * */
            rtpSendData(ctx, nal, size, last);
        }

    } else {  // 分片分組
        /*
         *
         *  0                   1                   2
         *  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3
         * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         * | FU indicator  |   FU header   |   FU payload   ...  |
         * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         *
         * */
        if (ctx->buf_ptr > ctx->buf){
            rtpSendData(ctx, ctx->buf, (int)(ctx->buf_ptr - ctx->buf), 0);
        }

        int headerSize;
        uint8_t *buff = ctx->buf;
        uint8_t type = nal[0] & 0x1F;
        uint8_t nri = nal[0] & 0x60;

        /*
         *     FU Indicator
         *    0 1 2 3 4 5 6 7
         *   +-+-+-+-+-+-+-+-+
         *   |F|NRI|  Type   |
         *   +---------------+
         * */
        buff[0] = 28;   // FU Indicator; FU-A Type = 28
        buff[0] |= nri;

        /*
         *      FU Header
         *    0 1 2 3 4 5 6 7
         *   +-+-+-+-+-+-+-+-+
         *   |S|E|R|  Type   |
         *   +---------------+
         * */
        buff[1] = type;     // FU Header uses NALU Header
        buff[1] |= 1 << 7;  // S(tart) = 1
        headerSize = 2;
        size -= 1;
        nal += 1;

        while (size + headerSize > RTP_PAYLOAD_MAX) {  // 發送分片分組除去首尾的中間的分片
            memcpy(&buff[headerSize], nal, (size_t)(RTP_PAYLOAD_MAX - headerSize));
            rtpSendData(ctx, buff, RTP_PAYLOAD_MAX, 0);
            nal += RTP_PAYLOAD_MAX - headerSize;
            size -= RTP_PAYLOAD_MAX - headerSize;
            buff[1] &= 0x7f;  // buff[1] & 0111111, S(tart) = 0
        }
        buff[1] |= 0x40;      // buff[1] | 01000000, E(nd) = 1
        memcpy(&buff[headerSize], nal, size);
        rtpSendData(ctx, buff, size + headerSize, last);
    }
}

// 從一段H264流中,查詢完整的NAL發送,直到發送完此流中的所有NAL
void rtpSendH264HEVC(RTPMuxContext *ctx, UDPContext *udp, const uint8_t *buf, int size){
    const uint8_t *r;
    const uint8_t *end = buf + size;
    gUdpContext = udp;

    printf("\nrtpSendH264HEVC start\n");

    if (NULL == ctx || NULL == udp || NULL == buf ||  size <= 0){
        printf("rtpSendH264HEVC param error.\n");
        return;
    }

    r = ff_avc_find_startcode(buf, end);
    while (r < end){
        const uint8_t *r1;
        while (!*(r++));  // skip current startcode

        r1 = ff_avc_find_startcode(r, end);  // find next startcode

        // send a NALU (except NALU startcode), r1==end indicates this is the last NALU
        rtpSendNAL(ctx, r, (int)(r1-r), r1==end);

        // control transmission speed
        usleep(1000000/25);
        // suppose the frame rate is 25 fps
        ctx->timestamp += (90000.0/25);
        r = r1;
    }
}

AVC.h

#ifndef RTPSERVER_AVC_H
#define RTPSERVER_AVC_H

#include <stdint.h>

/* copy from FFmpeg libavformat/acv.c */
const uint8_t *ff_avc_find_startcode(const uint8_t *p, const uint8_t *end);

#endif //RTPSERVER_AVC_H

AVC.c

#include <stdio.h>
#include "AVC.h"

// 查找NALU起始碼,直接copy的ffpmpeg代碼
static const uint8_t *ff_avc_find_startcode_internal(const uint8_t *p, const uint8_t *end)
{
    const uint8_t *a = p + 4 - ((intptr_t)p & 3);  // a=p后面第一個地址為00的位置上

    for (end -= 3; p < a && p < end; p++) {        // 可能是保持4字節 對齊
        if (p[0] == 0 && p[1] == 0 && p[2] == 1)
            return p;
    }

    for (end -= 3; p < end; p += 4) {
        uint32_t x = *(const uint32_t*)p;  // 取4個字節
        if ((x - 0x01010101) & (~x) & 0x80808080) { // X中至少有一個字節為0
            if (p[1] == 0) {
                if (p[0] == 0 && p[2] == 1) // 0 0 1 x
                    return p;
                if (p[2] == 0 && p[3] == 1) // x 0 0 1
                    return p+1;
            }
            if (p[3] == 0) {
                if (p[2] == 0 && p[4] == 1) // x x 0 0 1
                    return p+2;
                if (p[4] == 0 && p[5] == 1) // x x x 0 0 1
                    return p+3;
            }
        }
    }

    for (end += 3; p < end; p++) {  //
        if (p[0] == 0 && p[1] == 0 && p[2] == 1)
            return p;
    }

    return end + 3; // no start code in [p, end], return end.
}

const uint8_t *ff_avc_find_startcode(const uint8_t *p, const uint8_t *end){
    const uint8_t *out= ff_avc_find_startcode_internal(p, end);
    if(p < out && out < end && !out[-1]) out--; // find 0001 in x001
    return out;

Network.h

#ifndef RTPSERVER_NETWORK_H
#define RTPSERVER_NETWORK_H

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

typedef struct{
    const char *dstIp;
    int dstPort;
    struct sockaddr_in servAddr;
    int socket;
}UDPContext;

/* create UDP socket */
int udpInit(UDPContext *udp);

/* send UDP packet */
int udpSend(const UDPContext *udp, const uint8_t *data, uint32_t len);

#endif //RTPSERVER_NETWORK_H

Network.c

//
// Created by Liming Shao on 2018/5/11.
//

#include <stdio.h>
#include <string.h>
#include "Network.h"

int udpInit(UDPContext *udp) {
    if (NULL == udp || NULL == udp->dstIp || 0 == udp->dstPort){
        printf("udpInit error.\n");
        return -1;
    }

    udp->socket = socket(AF_INET, SOCK_DGRAM, 0);
    if (udp->socket < 0){
        printf("udpInit socket error.\n");
        return -1;
    }

    udp->servAddr.sin_family = AF_INET;
    udp->servAddr.sin_port = htons(udp->dstPort);
    inet_aton(udp->dstIp, &udp->servAddr.sin_addr);

    // 先發個空字符測試能否發送UDP包
    int num = (int)sendto(udp->socket, "", 1, 0, (struct sockaddr *)&udp->servAddr, sizeof(udp->servAddr));
    if (num != 1){
        printf("udpInit sendto test err. %d", num);
        return -1;
    }

    return 0;
}

int udpSend(const UDPContext *udp, const uint8_t *data, uint32_t len) {

    ssize_t num = sendto(udp->socket, data, len, 0, (struct sockaddr *)&udp->servAddr, sizeof(udp->servAddr));
    if (num != len){
        printf("%s sendto err. %d %d\n", __FUNCTION__, (uint32_t)num, len);
        return -1;
    }

    return len;
}

Utils.h

#ifndef RTPSERVER_UTILS_H
#define RTPSERVER_UTILS_H

#include <stdint.h>

uint8_t* Load8(uint8_t *p, uint8_t x);

uint8_t* Load16(uint8_t *p, uint16_t x);

uint8_t* Load32(uint8_t *p, uint32_t x);

/* read a complete file */
int readFile(uint8_t **stream, int *len, const char *file);

void dumpHex(const uint8_t *ptr, int len);

#endif //RTPSERVER_UTILS_H

Utils.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include "Utils.h"

uint8_t* Load8(uint8_t *p, uint8_t x) {
    *p = x;
    return p+1;
}

uint8_t* Load16(uint8_t *p, uint16_t x) {
    p = Load8(p, (uint8_t)(x >> 8));
    p = Load8(p, (uint8_t)x);
    return p;
}

uint8_t* Load32(uint8_t *p, uint32_t x) {
    p = Load16(p, (uint16_t)(x >> 16));
    p = Load16(p, (uint16_t)x);
    return p;
}

int readFile(uint8_t **stream, int *len, const char *file) {
    FILE *fp = NULL;
    long size = 0;
    uint8_t *buf;

    printf("readFile %s\n", file);
    fp = fopen(file, "r");
    if (!fp)
        return -1;

    // 下面是獲取文件大小的兩種方式
#if 0  
    // C語言方式,Windows可以使用此方式
    fseek(fp, 0L, SEEK_END);
    size = ftell(fp);
    fseek(fp, 0L, SEEK_SET);
#else 
    // Linux系統調用,不用讀取全部文件內容,速度快
    struct stat info = {0};
    stat(file, &info);
    size = info.st_size;
#endif

    buf = (uint8_t *)(malloc(size * sizeof(uint8_t)));
    memset(buf, 0, (size_t)size);

    if (fread(buf, 1, size, fp) != size){
        printf("read err.\n");
        return -1;
    }

    fclose(fp);

    *stream = buf;
    *len = (int)size;

    printf("File Size = %d Bytes\n", *len);
    return 0;
}

void dumpHex(const uint8_t *ptr, int len) {
    printf("%p [%d]: ", (void*)ptr, len);
    for (int i = 0; i < len; ++i) {
        printf("%.2X ", ptr[i]);
    }
    printf("\n");
}

RTP碼流播放方法/SDP文件

本程序只實現了發送RTP視頻流的服務器端功能,可以使用第三方軟件ffmpeg-ffplay/VLC進行播放。播放RTP流需要一個寫有視頻流信息的SDP文件(play.sdp),此程序使用的文件如下所示。

m=video 1234 RTP/AVP 96 
a=rtpmap:96 H264/90000
a=framerate:25
c=IN IP4 127.0.0.1
s=Sample Video

VLC播放

使用VLC先打開此sdp文件,然后運行此服務端程序。

FFplay

ffplay是ffmpeg中獨立的播放器程序。可以使用如下命令就行播放,同樣是先執行播放命令,后運行RTP發送程序。

ffplay -protocol_whitelist "file,rtp,udp" play.sdp

附ffmpeg RTP發送命令:ffmpeg -re -i Sample.h264 -vcodec copy -f rtp rtp://127.0.0.1:1234

關於RTP時間戳問題

RTP協議要求時間戳應該使用90kHz的采樣時鍾,也就是說一秒鍾的間隔應該設置時間差值為90000,25pfs恆定幀率的視頻每一幀時間戳就為900000/25。這是對於視頻文件而言的,對於實時采集的視頻流,可以使用視頻采集時刻作為時間戳。

因為本例使用的是.h264裸流文件,文件格式本身並沒有時間戳信息,所以本例中可以不設置時間戳信息,也可以根據幀率設置時間戳信息,通過分析網絡數據包發現FFmpeg RTP發送.h264視頻時時間戳采用的是一個固定的隨機數,並沒有逐幀遞增。
但是不設置時間戳信息的話,就會影響客戶端解碼播放。ffplay播放RTP流的時候,在沒有RTP時間戳的情況下會根據接收的速度進行解碼顯示,VLC在沒有RTP時間戳時,會先緩存一段時間的視頻流,然后正常播放,可能是通過分析NALU視頻流獲取了顯示時間信息。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM