首页
学习
活动
专区
圈层
工具
发布
50 篇文章
1
C语言中如何实现数据帧封装与解析
2
【熟视C语言】如何快速的了解一个库函数(C语言讲解,以string.h中的部分库函数为例)
3
C语言代码封装MQTT协议报文,了解MQTT协议通信过程
4
NV12数据格式转H265编码格式实现过程
5
基于Modbus协议实现Openplc与Kingview的仿真通讯与模拟测试
6
onvif协议最新版本_接口协议测试工具
7
linux后台开发常用调试工具
8
C/C++开发人员要了解的几大著名C/C++开源库[通俗易懂]
9
适用于嵌入式环境的加速计算库
10
Linux下WebRTC框架Janus编译过程
11
探索嵌入式应用框架(EAF)
12
[C&C++]联合体union的特征及用其进行传输
13
联合体和结构体一起解析数据
14
国标GB28181协议客户端开发(四)实时视频数据传输
15
6.1 C/C++ 封装字符串操作
17
C语言进阶——自定义类型
18
干货 | 结构体、联合体嵌套使用的一些实用操作
19
C语言的面向对象编程
20
QT应用编程: 编写低功耗BLE蓝牙调试助手(Android系统APP)
21
设计模式之接口隔离原则C++实现
22
嵌入式软件开发的框架思维
23
通过面向对象设计串口协议
24
QT应用编程: 开发串口调试助手
25
一种高效的串口自定义16进制通信协议的嵌入式应用开发解决方案
26
嵌入式中状态机的几种骚操作
27
【干货】用FreeRTOS搭建Event-Driven应用框架
28
嵌入式开发基础之任务管理(线程管理)
29
SIP菜鸟如何学SIP
30
Linux下使用libuvc读取控制USB免驱摄像头
31
Linux 使用strace命令查找进程卡死原因
32
84-OOP之组合
33
如何调试多线程程序
34
GDB多线程调试分析
35
GDB多线程多进程调试
36
一个简单实用的线程基类
37
OpenThread是世界上最舒心的跨平台多线程并发库
38
OpenMiniServer是一个超迷你、 超易用的C++高并发跨平台服务器框架
39
OpenSocket是跨全平台的高性能高并发网络库
40
一个C++多线程TCP服务Demo
41
一文搞懂网络库的分层设计!
42
实现一个接收多路RTP流,输出一路RTMP流的简单MCU
43
谈谈嵌入式应用软件人机界面开发的菜单框架编写
44
union 的概念及在嵌入式编程中的应用
45
让终端支持https,移植OpenSSL和libcurl到嵌入式linux,遇到的问题总结
46
日常工作中的设计:解耦和封装
47
一种简易的嵌入式设备系统日志记录方法
48
PLC和计算机通信的数据采集方法和传输监控的实现(1)
49
C++随笔(五)三种实现串口通信的方式
50
开源一个自己写过的MQTT 客户端调试工具

国标GB28181协议客户端开发(四)实时视频数据传输

国标GB28181协议客户端开发(四)实时视频数据传输

本文是《国标GB28181协议设备端开发》系列的第四篇,介绍了实时视频数据传输的过程。通过解读INVITE报文中的SDP信息,读取和解析视频文件或图片文件,进行数据编码,以及h264封装为PS格式,最终通过RTP数据发送,实现了GB28181协议设备端的视频传输功能。本文将逐步详细介绍每个模块的实现步骤和相关技术要点,帮助读者理解和应用GB28181协议进行实时视频传输。

一、INVITE报文的SDP信息解读

在GB28181协议中,在实时音视频传输过程中,使用INVITE报文携带SDP(Session Description Protocol)信息。SDP信息描述了会话的属性和参数,包括媒体类型、传输协议、编解码器、网络地址等。下面是一个示例INVITE报文的SDP内容,并对其中的每一项进行详细解释:

代码语言:txt
复制
v=0
o=34020000002000000001 0 0 IN IP4 192.168.1.10
s=Play
c=IN IP4 192.168.1.10
t=0 0
m=video 40052 RTP/AVP 96
a=recvonly
a=rtpmap:96 PS/90000
y=0358902090
f=
  1. v=0

表示SDP协议版本号,此处为0。

  1. o=34020000002000000001 0 0 IN 192.168.1.10

o字段标识了会话的发起者和会话的唯一标识。

"34020000002000000001" 表示该会话会话发起者的SIP ID。

0 0 表示会话的起始和结束时间戳。

IN IP4 192.168.1.10 表示会话的网络地址,这里为IPv4地址。

  1. s字段为会话的名称或描述,此处为"Play"表面是实时音视频
  2. c=IN IP4 192.168.1.10

c字段指定了会话的连接信息。

IN 表示网络类型为Internet。

IP4 192.168.1.10 表示会话的IPv4地址。

  1. t=0 0

t字段指定了会话的时间信息。

0 0 表示会话的起始和结束时间都为0,即持续时间未定义。

  1. m=video 40052 RTP/AVP 96

m字段定义了会话中的媒体类型和相关参数。

video 表示媒体类型为视频。

40052 表示媒体流的传输端口号。

RTP/AVP 表示传输协议为RTP,使用AVP(Audio-Visual Profile)配置。

96 表示媒体流使用编号96表示。

  1. a=rtpmap:96 PS/90000

a字段包含了媒体流的属性。

rtpmap:96 表示将编号为96的负载类型。

PS 表示使用MPEG-PS格式进行数据封装。

90000 表示时钟速率,即每秒的时钟滴答数。

  1. y=0358902090

y字段为十进制整数字符串,表示SSRC值

  1. f=

f字段:f= v/编码格式/分辨率/帧率/码率类型/码率大小a/编码格式/码率大小/采样率

这里并没有设置f字段,由数据发送端来填充

二、视频文件或图片文件的读取、解析和编码

为了进行视频数据传输,我们首先需要读取和解析视频文件或图片文件。我们需要使用相应的库或工具,从文件中读取视频或图片数据,并进行解析,以获取关键的视频帧或图像数据,为后续的编码和封装做准备。

三、h264封装PS

在GB28181协议中,视频数据通常以MPEG-PS(MPEG Program Stream)格式进行封装。需要将经过编码的视频数据进行PS格式的封装,包括添加包装头和起始码,然后再进一步封装RTP。

以下是使用C++将H.264的NALU封装为MPEG-PS格式的主要过程(仅展示部分代码):

代码语言:txt
复制
// 将H.264的NALU列表封装为MPEG-PS格式
void MakeMPEGPS(unsigned char* h264Data, int h264Length,
    unsigned char* psData)
{
    int totalPES = (h264Length + MAX_PES_LENGTH - 1) / MAX_PES_LENGTH; // 计算总的PES包数
    int remainingBytes = h264Length; // 剩余待处理的字节数

    // MPEG-PS包头
    unsigned char mpegPSHeader[] = {0x00, 0x00, 0x01, 0xBA};

    // 分割并封装H.264数据
    for (int i = 0; i < totalPES; i++)
    {
        unsigned char* pbuf = psData;

        int pesLength = (remainingBytes > MAX_PES_LENGTH) ? MAX_PES_LENGTH : remainingBytes; // 当前PES包的长度
        remainingBytes -= pesLength; // 更新剩余待处理的字节数

        // PES包头
        unsigned char pesHeader[] = {0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x00};

        // 设置PES包长度
        pesHeader[4] = (pesLength + 8) >> 8; // 高8位
        pesHeader[5] = (pesLength + 8) & 0xFF; // 低8位

        // 输出MPEG-PS包头和当前PES包头
        memcpy(pbuf, mpegPSHeader, sizeof(mpegPSHeader));
        pbuf += sizeof(mpegPSHeader);

        memcpy(pbuf, pesHeader, sizeof(pesHeader));
        pbuf += sizeof(pesHeader);

        // 输出当前PES包的H.264数据
        memcpy(pbuf, h264Data + (i * MAX_PES_LENGTH), pesLength);
        pbuf += pesLength;

        int payload_len = (pbuf - psData);

        // 封装RTP包并发送
        MakeAndSendRTP(psData, payload_len);
    }
}

需要注意到,当h264帧比较大的时候,会超出PES可表述的长度大小,这个时候必须对h264帧进行切分,封装成多个PES,再合成到PS包中。

四、RTP数据发送

RTP数据发送的逻辑比较简单,以下为程序中的代码示意图

以下为RTP封装的演示代码(仅展示部分代码):

代码语言:txt
复制
struct RTPHeader
{
    uint8_t version; // RTP协议版本号,固定为2
    uint8_t padding: 1; // 填充位
    uint8_t extension: 1; // 扩展位
    uint8_t csrcCount: 4; // CSRC计数器,指示CSRC标识符的个数
    uint8_t marker: 1; // 标记位
    uint8_t payloadType: 7; // 负载类型
    uint16_t sequenceNumber; // 序列号
    uint32_t timestamp; // 时间戳
    uint32_t ssrc; // 同步信源标识符
};

void MakeRTPHeader(struct RTPHeader* header, uint16_t sequenceNumber, uint32_t timestamp, uint32_t ssrc, bool isMark)
{
    // 设置RTP协议版本号为2
    header->version = 2;
    // 填充位、扩展位、CSRC计数器等字段根据具体需求进行设置
    header->padding = 0;
    header->extension = 0;
    header->csrcCount = 0;
    // 设置标记位为0(如果需要设置为1,则在需要设置的地方进行修改)
    header->marker = isMark ? 1 : 0;
    // 设置负载类型(payload type),根据具体需求进行设置
    header->payloadType = 96;
    // 设置序列号和时间戳
    header->sequenceNumber = htons(sequenceNumber); // 需要进行字节序转换(网络字节序)
    header->timestamp = htonl(timestamp); // 需要进行字节序转换(网络字节序)

    // 设置同步信源标识符
    header->ssrc = htonl(ssrc); // 需要进行字节序转换(网络字节序)
}

void sendRTPPacket(const uint8_t* mpegPSData, int mpegPSLength, uint16_t sequenceNumber, uint32_t timestamp, uint32_t ssrc)
{
    int offset = 0; // 偏移量,用于遍历MPEG-PS包数据
    int remainingLength = mpegPSLength; // 剩余长度,用于判断是否需要分割RTP报文
    uint8_t rtpbuf[RTP_PAYLOAD_MAX_SIZE]; // RTP负载数据缓冲区
    struct RTPHeader rtpHeader; // RTP报文头部

    while (remainingLength > 0)
    {
        // 计算当前RTP负载数据长度(不超过RTP负载最大大小)
        bool is_mark = false;
        int data_len = RTP_PAYLOAD_MAX_SIZE;
        if (remainingLength <= RTP_PAYLOAD_MAX_SIZE)
        {
            data_len = remainingLength;
            is_mark = true;
        }

        // 填写RTP报文头部
        MakeRTPHeader(&rtpHeader, sequenceNumber, timestamp, ssrc);

        // 复制RTP头部到RTP负载缓冲区
        memcpy(rtpbuf, &rtpHeader, sizeof(RTPHeader));

        // 复制MPEG-PS数据到RTP负载缓冲区
        memcpy(rtpbuf + RTP_HEADER_LEN, mpegPSData + offset, data_len);

        // 将完整RTP包发送出去
        if (udp_channel_)
        {
            udp_channel_->PostSendBuf(rtpbuf, RTP_HEADER_LEN + data_len);
        }

        // 更新偏移量、剩余长度、序列号等信息
        offset += data_len;
        remainingLength -= data_len;
        sequenceNumber++;
    }
}

合作请加作者hbstream(http://haibindev.cnblogs.com),转载请注明作者和出处

下一篇
举报
领券