RTP封装H.264视频规范以及C语言实现
以前上学时间做嵌入式开发板Hi3516A的流媒体项目,现在又突然想起来,不想学过就忘了浪费了,所以又自己实现了一遍读取本地视频文件发送RTP视频流的程序,算是总结一下。网上关于RTP的介绍实在是太多,但是多数都是抄来抄去没有系统性,还是贴上代码更容易理解。
RTP封装H.264码流规范
本文简单说明RTP结构和实现,详细说明请参考标准文档RTF6184: RTP Payload Format for H.264 Video。
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 |
: .... :
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- V(version): 当前版本设为2。
- P(padding): 载荷之后填充数据,用于要求固定长度RTP包场景,一般不用,设置为0。
- X(extension): 固定头部后面加头部扩展字段标志,一般不用,设为0。
- CC(CSRC count): CSRC字段长度
- M(marker): AU最后一个包标志位
- PT(payload): RTP载荷媒体数据类型,H264=96
- Sequence number: RTP包序列号,递增1。
- timestamp: 媒体采样时间戳,H264/HEVC统一采用90kHz采样时钟,如果使用帧率fps来设置时间戳,则递增数值为90000/fps。
- SSRC: 数据包同源标志,来自同一处的RTP包应采用固定统一数值。
- CSRC: 一般CC=0,不用此位。
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也包含此结构的头部字段。
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|F|NRI| Type |
+---------------+
- F(forbidden_zero_bit):错误位或语法冲突标志,一般设为0。
- NRI(nal_ref_idc): 与H264编码规范相同,此处可以直接使用原始码流NRI值。
- Type:RTP载荷类型,1-23:H264编码规定的数据类型,单NALU分组直接使用此值,24-27:聚合分组类型(聚合分组一般使用24 STAP-A),28-29分片分组类型(分片分组一般使用28FU-A),30-31,0保留。
NAL Unit Type | Packet Type | Packet Type Name |
---|---|---|
0 | reserved | - |
1-23 | NAL unit | Single NAL unit packet |
24 | STAP-A | Single-time aggregation packet |
25 | STAP-B | Single-time aggregation packet |
26 | MTAP16 | Multi-time aggregation packet |
27 | MTAP24 | Multi-time aggregation packet |
28 | FU-A | Fragmentation unit |
29 | FU-B | Fragmentation unit |
30-31 | reserved | - |
单NALU分组
此结构的NALU Header结构可以直接使用原始码流NALU Header,所以单NALU分组Type = 1~23。
封装RTP包的时候可以直接把 查询到的NALU去掉起始码后的部分 当作单NALU分组的RTP包Payload部分。
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| RTP Header |
: :
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|F|NRI| Type | |
+-+-+-+-+-+-+-+-+ |
| |
| Bytes 2..n of a single NAL unit |
| |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| :...OPTIONAL RTP padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
聚合分组
通常采用STAP-A (Type=24)结构封装RTP聚合分组,下图为包含2个NALU的采用STAP-A结构的聚合分组。
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| RTP Header |
: :
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|STAP-A NAL HDR | NALU 1 Size | NALU 1 HDR |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| NALU 1 Data |
: :
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | NALU 2 Size | NALU 2 HDR |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| NALU 2 Data |
: :
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| :...OPTIONAL RTP padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- 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。
分片分组
通常采用无DON字段的FU-A结构封装RTP分片分组。各种RTP分组在RTP Header后面都跟着 F|NRI|Type
结构,来判定分组类型。
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| RTP Header |
: :
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| FU indicator | FU header | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
| |
| FU payload |
| |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| :...OPTIONAL RTP padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
FU indicator
采用FU-A分组类型的话Type = 28,NRI与此NALU中NRI字段相同。
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|F|NRI| Type |
+---------------+
FU header
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|S|E|R| Type |
+---------------+
此结构中Type采用原始码流NALU中的Type字段,S=1表示这个RTP包为分片分组第一个分片,E=1表示为分片分组最后一个分片。除了首尾分片,中间的分片S&E都设为0。R为保留位,设为0。
RTP封装H.264码流示例程序
RTP Server示例程序
程序框架
这个示例程序是参考ffmpeg的代码,实现了读取一个Sample.h264裸流文件,(打算以后支持HEVC/H.265所以文件名有HEVC),通过ffmpeg内置的函数查找NAL单元起始码,从而获取一个完整的NALU。根据NALU长度选择RTP打包类型,然后再组装RTP头部信息,最终发送到指定IP和端口,本例发送到本机1234端口。本程序网络部分使用的是基于Linux/MacOS 的POSIX socket,Windows平台可以自己实现相应的函数进行代替。
程序文件概览
- 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.c
//
// Created by Liming Shao on 2018/5/10.
//
#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.c
//
// Created by Liming Shao on 2018/5/10.
//
#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.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.c
//
// Created by Liming Shao on 2018/5/10.
//
#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");
}
完整代码可以从Github上直接下载,地址:https://github.com/lmshao/RTP 。
RTP码流播放方法/SDP文件
本程序只是实现了发送RTP视频流的服务器端功能,可以使用第三方软件ffmpeg-ffplay/VLC进行播放。播放RTP流需要一个写有视频流信息的SDP文件(play.sdp),此程序使用的文件如下所示,具体关于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视频流获取了显示时间信息。
关于RTP聚合分组STAP-A NAL HDR问题
RTP聚合分组的STAP-A NAL头部字段,RTP协议要求:
- The RTP timestamp MUST be set to the earliest of the NALU-times of all the NAL units to be aggregated.
- The type field of the NAL unit type octet MUST be set to the appropriate value, as indicated in Table 4. STAP-A type is 24.
- The F bit MUST be cleared if all F bits of the aggregated NAL units are zero; otherwise, it MUST be set.
- The value of NRI MUST be the maximum of all the NAL units carried in the aggregation packet.
通过分析,FFmpg在封装RTP聚合分组包的时候,并没有完全采用RTP协议规定的要求就行封装。比如F位和NRI位直接使用第一包的相关信息,好像也没影响视频播放。本示例程序方便起见时间戳采用的最后一个NAL的时间戳,有空再改进。
有任何问题欢迎交流。
代码可以从Github上直接下载,地址:https://github.com/lmshao/RTP 。