目录

一. 前言

二. RTP协议介绍

三. H264码流结构介绍

四. RTP与H264的结合

1. 单一NALU模式

2. 组合包模式

3. 分片模式

五. 代码实战

六. 效果展示


一. 前言

        在音视频通话中我们通常使用 RTP 协议包荷载音视频码流数据,例如用户摄像头采集图像后进行编码成帧,再将帧数据拆分到 RTP 协议包后发送到流媒体服务器,本文将介绍如何使用 JRTPLIB 发送 H264 码流数据。

二. RTP协议介绍

        参考这篇博客

三. H264码流结构介绍

        H264 码流是由很多 NALU 组成的,NALU 分为 NALU header 和 NALU payload,每个 NALU 前通常包含一个 StartCode,StartCode 必须是 0x00000001 或者 0x000001。

        NALU Header 结构如下所示。

F:forbidden_zero_bit,H264 规范要求该位为 0

NRI:nal_ref_idc,取值 0~3,指示该 NALU 重要性,对于 NRI=0 的 NALU 解码器可以丢弃它而不影响图像的回放,该值越大说明该 NALU 越重要,需要受到保护。如果当前 NALU 属于参考帧的数据,或者是序列参数集,图像参数集等重要信息,则该值必须大于 0

Type:表示 NALU 数据类型,该字段占 5 位,取值 0 ~ 31,如下列出了一些常见的 Type 对应的含义。

Type 含义
0 未指定
1 非 IDR 图像的片
5 IDR 图像的片
6 SEI(辅助增强信息)
7 SPS(序列参数集)
8 PPS(图像参数集)
24 STAP-A(单一时间组合包模式 A,用于一个 RTP 包荷载多个 NALU)
25 STAP-B(单一时间组合包模式 B)
26 MTAP16(多个时间的组合包模式 A)
27 MTAP24(多个时间的组合包模式 B)
28 FU-A(分片模式 A,用于将单个 NALU 分到多个 RTP 包)
29 FU-B(分片模式 B)

        如下是我们通过二进制格式打开某个 h264 文件后的内容,可以看到第一个 NALU 前的 StartCode=0x00000001,其 NALU header 内容为 0x67,说明它是一个 SPS,第二个 NALU 前的 StartCode=0x00000001,其 NALU header 内容为 0x68,说明它是一个 PPS。

四. RTP与H264的结合

        由于一个 RTP 包荷载的数据量有限,而 NALU 大小却差异很大,有些 NALU 可以刚好装到一个 RTP 中,有些 NALU 较小,一个 RTP 包可以携带多个 NALU,而有些 NALU 很大,需要分成多个 RTP 包传输,因此 RTP 荷载 H264 码流数据有三种模式(单一 NALU 模式,组合包模式,分片模式)。

        在 WebRTC 中对于组合包模式和分片模式,仅支持非交错模式,即 STAP-A,FU-A 模式。

1. 单一NALU模式

        一个 RTP 包荷载一个 NALU,结构如下。RTP 头部之后的一个字节为 NALU Header,之后是 NALU 数据部分,此时 NALU Header Type 取值为 1-23。

2. 组合包模式

        一个 RTP 包荷载多个 NALU,适用于较小的 NALU,结构如下。

        STAP NAL HDR 中的 Type 取值为 24/25,NALU Size 表示后面的 NALU 的字节数(不包含这 2 个字节)。

3. 分片模式

        用于将一个 NALU 分片到多个 RTP 包中。

        FU-A 结构如下所示,RTP 头部之后的第一个字节为 FU indicator,第二个字节为 FU header。

         FU indicator 结构如下所示,其中 F 和 NRI 与 NALU 的 F 和 NRI 含义相同,而 Type 需要设置为 28/29,指示当前为分片模式 A/B。

 

FU header 结构如下所示,字段含义如下。

S:start 标记位,当该位为 1 时表示 NALU 分片的第一个分片。

E:end 标记位,当该位为 1 时表示 NALU 分片的最后一个分片。

R:保留位,接收者可以忽略该位。

Type:NALU 原来的 Type 类型(1-23)。

         因此对于一个 NALU 分片到多个 RTP 包传输的情况,不同 RTP 的 FU indicator 是完全一样的,FU header 只有 S,E 位有所区别。

五. 代码实战

jrtp_h264.cpp

#include <jrtplib3/rtpsession.h>
#include <jrtplib3/rtplibraryversion.h>
#include <jrtplib3/rtpudpv4transmitter.h>
#include <jrtplib3/rtpsessionparams.h>
#include <jrtplib3/rtppacket.h>
#include <jrtplib3/rtperrors.h>
#include <iostream>
#include <stdio.h>
#include <string>
#include "h264.h"

using namespace std;
using namespace jrtplib;

const string SSRC = "10000";
const string H264_FILE_PATH = "./test_640x480_15_nv12.h264";
const int FRAME_RATE = 15;
const int MTU_SIZE = 1500;
const int MAX_RTP_PACKET_LENGTH = 1360;

void checkerror(int rtperr) {
	if (rtperr < 0) {
		std::cout << "ERROR: " << RTPGetErrorString(rtperr) << std::endl;
		exit(-1);
	}
}

int main() {

    // 获取本地用于发送的端口以及对端的IP和端口
    uint16_t localport;
    std::cout << "Enter local port(even): ";
	std::cin >> localport;

    std::string ipstr;
	std::cout << "Enter the destination IP address: ";
	std::cin >> ipstr;
	uint32_t destip = inet_addr(ipstr.c_str());
	if (destip == INADDR_NONE) {
		std::cerr << "Bad IP address specified" << std::endl;
		return -1;
	}
    destip = ntohl(destip);

    uint16_t destport;
	std::cout << "Enter the destination port: ";
	std::cin >> destport;

    // 设置RTP属性
    RTPUDPv4TransmissionParams tranparams;
    tranparams.SetPortbase(localport);

    RTPSessionParams sessparams;
    sessparams.SetOwnTimestampUnit(1.0 / H264_SAMPLE_RATE);

    RTPSession sess;
    int status = sess.Create(sessparams, &tranparams);
    checkerror(status);

    RTPIPv4Address destAddr(destip, destport);
    status = sess.AddDestination(destAddr);
	checkerror(status);

    sess.SetDefaultPayloadType(H264_PAYLOAD_TYPE);
    sess.SetDefaultMark(false);
    sess.SetDefaultTimestampIncrement(H264_SAMPLE_RATE / FRAME_RATE);

    FILE* fh264 = fopen(H264_FILE_PATH.c_str(), "rb");
    if (fh264 == NULL) {
        std::cout << "打开h264文件失败" << std::endl;
        exit(-1);
    }

    uint8_t sendbuf[MTU_SIZE] = { 0 };
    NaluHeader* naluHeader;
    uint8_t* naluPayload;
    FuIndicator* fuIndicator;
    FuHeader* fuHeader;

    Nalu* nalu = AllocNalu();
    RTPTime sendDelay(0.060);

    while (true) {
        if (feof(fh264)) {
            fseek(fh264, 0, SEEK_SET);
        }
        bool wait = true;
        int size = GetAnnexbNalu(fh264, nalu);
        if (size == 0) {
            continue;
        } else if (size < 0) {
            exit(0);
        } else {
            if (size <= MAX_RTP_PACKET_LENGTH) {
                memset(sendbuf, 0, MTU_SIZE);
                naluHeader = (NaluHeader*) &sendbuf[0];
                naluHeader->F = nalu->forbiddenBit >> 7;
                naluHeader->NRI = nalu->nalReferenceIdc >> 5;
                naluHeader->Type = nalu->nalUintType;

                naluPayload = &sendbuf[1];
                memcpy(naluPayload, nalu->buf+nalu->startCodePrefixLen+1, nalu->len-1);

                uint32_t timestampInc = 0;
                if (nalu->nalUintType == 1 || nalu->nalUintType == 5) { // 非IDR图像的片 / IDR图像的片
                    timestampInc = H264_SAMPLE_RATE / FRAME_RATE;
                } else {
                    wait = false;
                }
                sess.SendPacket((void *) sendbuf, nalu->len, H264_PAYLOAD_TYPE, true, timestampInc);
                std::cout << "SendPacket, size: " << size << ", type: " << (int) nalu->nalUintType << std::endl;
            } else {
                int nPacket = nalu->len / MAX_RTP_PACKET_LENGTH;
                int leftLen = nalu->len % MAX_RTP_PACKET_LENGTH;
                if (leftLen > 0) {
                    nPacket += 1;
                }
                int n = 0;
                while (n < nPacket) {
                    memset(sendbuf, 0, MTU_SIZE);
                    bool isFirst = (n == 0);
                    bool isLast = (n == nPacket-1);
                    fuIndicator = (FuIndicator*) &sendbuf[0];
                    fuIndicator->F = nalu->forbiddenBit >> 7;
                    fuIndicator->NRI = nalu->nalReferenceIdc >> 5;
                    fuIndicator->Type = 28;

                    fuHeader = (FuHeader*) &sendbuf[1];
                    fuHeader->S = isFirst ? 1 : 0;
                    fuHeader->E = isLast ? 1 : 0;
                    fuHeader->R = 0;
                    fuHeader->Type = nalu->nalUintType;

                    naluPayload = &sendbuf[2];
                    
                    int sendLen = 2;    // fu_indicator, fu_header
                    uint32_t timestampInc = 0;
                    bool mark = false;
                    if (isLast) {
                        sendLen += leftLen;
                        timestampInc = H264_SAMPLE_RATE / FRAME_RATE;
                        mark = true;
                    } else {
                        sendLen += MAX_RTP_PACKET_LENGTH;
                    }
                    memcpy(naluPayload, nalu->buf + nalu->startCodePrefixLen + n * MAX_RTP_PACKET_LENGTH + 1, sendLen);

                    sess.SendPacket((void*) sendbuf, sendLen, H264_PAYLOAD_TYPE, mark, timestampInc);
                    ++n;
                    std::cout << "SendPacket, size: " << size << ", type: " << (int) nalu->nalUintType << std::endl;
                }
            }
        }
        if (wait) {
            RTPTime::Wait(sendDelay);
        }
    }

    FreeNalu(nalu);
    sess.BYEDestroy(RTPTime(3, 0), 0, 0);

    return 0;
}

h264.cpp

#include "h264.h"

static const uint32_t DEFAULT_NALU_BUFFER_SIZE = 80960;

// 0x00 0x00 0x01
bool isStartCode3(const uint8_t* buf) {
    if (buf[0] == 0 && buf[1] == 0 && buf[2] == 1) {
        return true;
    }
    return false;
}

// 0x00 0x00 0x00 0x01
bool isStartCode4(const uint8_t* buf) {
    if (buf[0] == 0 && buf[1] == 0 && buf[2] == 0 && buf[3] == 1) {
        return true;
    }
    return false;
}

// startCode: 0x000001 / 0x00000001
int GetAnnexbNalu(FILE* f, Nalu* nalu) {
    if (f == NULL || nalu == NULL || feof(f)) {
        return -1;
    }
    int n = fread(nalu->buf, 1, 3, f);
    if (n != 3) {
        return -1;
    }

    int startCodePrefixLen = 0;
    if (isStartCode3(nalu->buf)) {
        startCodePrefixLen = 3;
    } else {
        n = fread(nalu->buf+3, 1, 1, f);
        if (n != 1) {
            return -1;
        }
        if (isStartCode4(nalu->buf)) {
            startCodePrefixLen = 4;
        } else {
            return 0;
        }
    }

    int pos = startCodePrefixLen;
    bool nextStartCodeFound = false;
    int nextStartCodePrefixLen = 0; 
    while (!nextStartCodeFound) {
        if (feof(f)) {
            break;
        }
        nalu->buf[pos] = fgetc(f);
        if (nalu->buf[pos] == 0x01 && nalu->buf[pos-1] == 0x00 && nalu->buf[pos-2] == 0x00) {
            nextStartCodeFound = true;
            if (nalu->buf[pos-3] == 0x00) {
                nextStartCodePrefixLen = 4;
            } else {
                nextStartCodePrefixLen = 3;
            }
        }
        ++pos;
    }
    pos -= nextStartCodePrefixLen;
    
    if (nextStartCodeFound) {
        if (fseek(f, -nextStartCodePrefixLen, SEEK_CUR) != 0) {
            return -1;
        }
    }

    nalu->startCodePrefixLen = startCodePrefixLen;
    nalu->forbiddenBit = nalu->buf[startCodePrefixLen] & 0x80;
    nalu->nalReferenceIdc = nalu->buf[startCodePrefixLen] & 0x60;
    nalu->nalUintType = nalu->buf[startCodePrefixLen] & 0x1f;
    nalu->len = pos - startCodePrefixLen;

    return pos;
}

Nalu* AllocNalu(void) {
    return AllocNalu(DEFAULT_NALU_BUFFER_SIZE);
}

Nalu* AllocNalu(uint32_t bufferSize) {
    Nalu* n = (Nalu*) calloc(1, sizeof(Nalu));
    if (!n) {
        return NULL;
    }
    n->maxSize = bufferSize;
    n->buf = (uint8_t*) calloc(bufferSize, sizeof(uint8_t));
    if (n->buf == NULL) {
        free(n);
        return NULL;
    }
    return n;
}

void FreeNalu(Nalu* nalu) {
    if (nalu) {
        if (nalu->buf) {
            free(nalu->buf);
            nalu->buf = NULL;
        }
        free(nalu);
    }
}

h264.h

#pragma once

#include <iostream>

#define H264_PAYLOAD_TYPE 96
#define H264_SAMPLE_RATE  90000

struct NaluHeader {
    uint8_t Type : 5;
    uint8_t NRI  : 2;
    uint8_t F    : 1;
};

struct FuIndicator {
    uint8_t Type : 5;
    uint8_t NRI  : 2;
    uint8_t F    : 1;
};

struct FuHeader {
    uint8_t Type : 5;
    uint8_t R    : 1;
    uint8_t E    : 1;
    uint8_t S    : 1;
};

struct Nalu {
    int startCodePrefixLen;
    uint8_t forbiddenBit;
    uint8_t nalReferenceIdc;
    uint8_t nalUintType;
    uint32_t maxSize;
    uint32_t len;
    uint8_t* buf;
    uint16_t lostPackets;
};

int GetAnnexbNalu(FILE* f, Nalu* nalu);
Nalu* AllocNalu();
Nalu* AllocNalu(uint32_t bufferSize);
void FreeNalu(Nalu* nalu);

六. 效果展示

        jrtp_h264 启动后,设置本端使用的 UDP 端口以及对端地址后,进程就开始发包了,我们使用 VLC 设置 sdp 信息接收流并播放。

m=video 12500 RTP/AVP 96
a=rtpmap:96 H264
a=framerate:15
c=IN IP4 127.0.0.1

        12500 需要对应进程设置的对端地址,127.0.0.1 需要对应进程设置的对端 IP。使用 VLC 打开该文件后就可以看到发送的 H264 码流图像了。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐