RTMP 视频数据封装

RTMP 协议

与HTTP(超文本传输协议)同样是一个基于TCP的Real Time Messaging Protocol(实时消息传输协议)。由Adobe Systems公司为Flash播放器和服务器之间音频、视频和数据传输开发的一种开放协议 。在国内被广泛的应用于直 播领域。HTTP默认端口为80,RTMP则为1935
我们通过阅读Adobe的协议规范,通过与服务器建立TCP通信,根据协议格式生成与解析数据即可使用RTMP进行直播。当然我们也可以借助一些实现了RTMP协议的开源库来完成这一过程。

RTMPDump

RTMPDump 是一个用来处理RTMP流媒体的开源工具包。它能够单独使用进行RTMP的通信,也可以集成到FFmpeg中通过FFmpeg接口来使用RTMPDump。

RTMPDump源码下载:

交叉编译

在Android中可以直接借助NDK在JNI层调用RTMPDump来完成RTMP通信。但是首先必须得进行交叉编译。 RTMPDump源码结构如下:

image-20201231140439733

在根目录下提供了一个 Makefile 与一些 .c 源文件。这里的源文件将会编译出一系列的可执行文件。然后我们需 要的并不是可执行文件,真正的对RTMP的实现都在librtmp子目录中。在这个子目录中同样包含了一个 Makefile 文件。通过阅读 Makefile 发现,它的源码并不多: OBJS=rtmp.o log.o amf.o hashswf.o parseurl.o 。因此我们 不进行预编译,即直接放入AS中借助 CMakeLists.txt 来进行编译。这么做可以让我们方便的对库本身进行调试或 修改(实际上我们确实会稍微修改这个库的源码)。

在AS中复制librtmp置于: src/main/cpp/librtmp ,并为其编写CMakeLists.txt

 #预编译宏
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DNO_CRYPTO" ) 
#所有源文件放入 rtmp_source 变量
file(GLOB rtmp_source *.c)
#编译静态库
add_library(rtmp STATIC ${rtmp_source} )

在 app/CMakeLists.txt 中导入这个CMakeLists.txt

 cmake_minimum_required(VERSION 3.4.1) 
 #导入其他目录cmakelist 
 add_subdirectory(src/main/cpp/librtmp) 
 add_library(XXX SHARED ...) 
 #XXX需要链接rtmp库 
 target_link_libraries(XXX rtmp ...)

RTMP视频数据

RTMP视频流格式与FLV很相似,通过查看FLV的格式文档,就能够知道RTMP视频数据应该怎么拼接。

RTMP中的数据就是由FLV的TAG中的数据区构成。

image-20201231140724956

FLV tags 结构

FLV tags 结构
image-20201231140826080

如上图,第一个字节0x09 表示此段数据为视频,数据大小为0x00,0x00,0x2F即47,时间戳为 0x00,0x00,0x00,时间戳扩展也为0x00。(第二行)流ID:0x00,0x00,0x00。接下来就是视频数据,通过此处的 数据大小字段得知,数据长为47字节。 则从0x17开始,一直到最后一行的0xC0,就是数据区域,而最后的 0x00,0x00,0x00,0x3A 即58,表示的是这个数据块除最后4个字节的总大小。本处为视频数据,那么从0x17 开始,数据内容则为下面的部分。

视频数据

*字段* *占位* *描述*
帧类型 4 1: 关键帧2: 普通帧 ......
编码ID 4 7: 高级视频编码 AVC ......
视频数据 n AVC则需要下面的AVCVIDEOPACKET

AVCVIDEOPACKET

*字段* *字节* *描述*
类型 1 0:AVC 序列头(指导播放器如何解码)1:其他单元(其他NALU)
合成时间 3 对于AVC序列头,全为0
数据 n 类型不同,数据不同
image-20201231141157799

视频数据中 0x17 则表示了1: 关键帧与7: 高级视频编码 AVC,如果是普通帧,则此数据为0x27。而类型为: 0x00表示这段数据为AVC序列头(avc sequence header)。最后三个字节为合成时间。而如果类型为AVC序列 头接下来的数据就是下面的内容:

AVC 序列头

在AVCVIDEOPACKET 中如果类型为0,则后续数据为:

*类型* *字节* *说明*
版本 1 0x01
编码规格 3 sps[1]+sps[2]+sps[3] (后面说明)
几个字节表示NALU 的长度 1 0xFF,包长为 (0xFF& 3)+ 1,也就是4字节表示
SPS个数 1 0xE1,个数为0xE1 & 0x1F 也就是1
SPS长度 2 整个sps的长度
sps的内容 n 整个sps
pps个数 1 0x01,不用计算就是1
pps长度 2 整个pps长度
pps内容 n 整个pps内容
image-20201231141331917

0x01为版本,后续数据按照上表记录,最后四字节上面说过:为这个数据块除最后4个字节的总大小。其中 SPS与PPS是编码器在编码H.264视频时,在关键帧前会编码出的关于这个关键帧与需要参考该关键帧的B/P 帧如何解码的内容,如:宽、高等信息。

在AVCVIDEOPACKET 中如果类型为1(非AVC 序列头),则后续数据为:

*类型* *字节* *说明*
包长 由AVC序列头中定义 后续长度
数据 n H.264数据

一般情况下,组装的RTMPPacket(RTMPDump中的结构体)为:

image-20201231141432316

这里的sps与pps表示 AVC序列头

image-20201231141505364

所以对于视频的数据封装,AVC序列头为:

    int i = 0;
    //固定头
    packet->m_body[i++] = 0x17;
    //类型
    packet->m_body[i++] = 0x00;
    //composition time 0x000000
    packet->m_body[i++] = 0x00;
    packet->m_body[i++] = 0x00;
    packet->m_body[i++] = 0x00;

    //版本
    packet->m_body[i++] = 0x01;
    //编码规格
    packet->m_body[i++] = sps[1];
    packet->m_body[i++] = sps[2];
    packet->m_body[i++] = sps[3];
    packet->m_body[i++] = 0xFF;

    //整个sps
    packet->m_body[i++] = 0xE1;
    //sps长度
    packet->m_body[i++] = (spslen >> 8) & 0xff;
    packet->m_body[i++] = spslen & 0xff;
    memcpy(&packet->m_body[i], sps, spslen);
    i += spslen;

    //pps
    packet->m_body[i++] = 0x01;
    packet->m_body[i++] = (ppslen >> 8) & 0xff;
    packet->m_body[i++] = (ppslen) & 0xff;
    memcpy(&packet->m_body[i], pps, ppslen);

    packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
    packet->m_nBodySize = bodySize;
    packet->m_headerType = RTMP_PACKET_SIZE_MEDIUM;
    //时间戳  sps与pps(不是图像) 没有时间戳
    packet->m_nTimeStamp = 0;
    // 使用相对时间
    packet->m_hasAbsTimestamp = 0;

而对于非AVC序列头,关键字与非关键字,只有第一个字节0x17与0x27的区别:

 packet->m_body[0] = 0x27;
    //关键帧
    if (type == NAL_SLICE_IDR) {
        packet->m_body[0] = 0x17;
    }
    //类型
    packet->m_body[1] = 0x01;
    //时间戳
    packet->m_body[2] = 0x00;
    packet->m_body[3] = 0x00;
    packet->m_body[4] = 0x00;
    //数据长度 int 4个字节 相当于把int转成4个字节的byte数组
    packet->m_body[5] = (i_payload >> 24) & 0xff;
    packet->m_body[6] = (i_payload >> 16) & 0xff;
    packet->m_body[7] = (i_payload >> 8) & 0xff;
    packet->m_body[8] = (i_payload) & 0xff;

    //图片数据
    memcpy(&packet->m_body[9], p_payload, i_payload);

H.264数据

H.264码流在网络中传输时实际是以NALU的形式进行传输的。NALU就是NAL UNIT,NAL单元。NAL全称Network Abstract Layer, 即网络抽象层。在H.264/AVC视频编码标准中,整个系统框架被分为了两个层面:视频编码层面 (VCL)和网络抽象层面(NAL)。其中,前者负责有效表示视频数据的内容,而后者则负责格式化数据并提供头 信息,以保证数据适合各种信道和存储介质上的传输。我们平时的每帧数据就是一个NAL单元。

往RTMP包中填充的就是NAL数据,但不是直接将编码出来的数据填充进去。

一段包含了N个图像的H.264裸数据,每个NAL之间由:

  • 00 00 00 01 或者 00 00 01 进行分割。

在分割符之后的第一个字节,就是表示这个nal的类型。

  • 0x67:sps
  • 0x68: pps
  • 0x65: IDR

在将数据加入RTMPPacket的时候是需要去除分割符的。

image-20201231142049677

所以完整的封包代码为:

 int bodysize = 9 + i_payload;
    RTMPPacket_Alloc(packet, bodysize);
    RTMPPacket_Reset(packet);
//    int type = payload[0] & 0x1f;
    packet->m_body[0] = 0x27;
    //关键帧
    if (type == NAL_SLICE_IDR) {
        packet->m_body[0] = 0x17;
    }
    //类型
    packet->m_body[1] = 0x01;
    //时间戳
    packet->m_body[2] = 0x00;
    packet->m_body[3] = 0x00;
    packet->m_body[4] = 0x00;
    //数据长度 int 4个字节 相当于把int转成4个字节的byte数组
    packet->m_body[5] = (i_payload >> 24) & 0xff;
    packet->m_body[6] = (i_payload >> 16) & 0xff;
    packet->m_body[7] = (i_payload >> 8) & 0xff;
    packet->m_body[8] = (i_payload) & 0xff;

    //图片数据
    memcpy(&packet->m_body[9], p_payload, i_payload);

NALU

NALU就是NAL UNIT,nal单元。NAL全称Network Abstract Layer, 即网络抽象层,H.264在网络上传输的结构。 一帧图片经过 H.264 编码器之后,就被编码为一个或多个片(slice),而装载着这些片(slice)的载体,就是 NALU 了 。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 230,501评论 6 544
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,673评论 3 429
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 178,610评论 0 383
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,939评论 1 318
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,668评论 6 412
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 56,004评论 1 329
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 44,001评论 3 449
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 43,173评论 0 290
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,705评论 1 336
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,426评论 3 359
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,656评论 1 374
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 39,139评论 5 364
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,833评论 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 35,247评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,580评论 1 295
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,371评论 3 400
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,621评论 2 380

推荐阅读更多精彩内容