前面介绍了HI3518作为服务器实现RTSP传输视频,但HI3518毕竟只是个嵌入式CPU,处理并发能力有限,如果多个客户端同时观看视频就会有性能上的问题,而且实现一些直播功能如暂停,回放功能就有些捉襟见肘,这时就需要把HI3518的视频数据通过一个性能更强大服务器作为中转处理更大的并发数并实现暂停,回放等功能,这时RTMP协议就派上用场了。
Hi3518+RTMP工程文件下载链接:Hi3518+RTMP
我们先将源码编译一遍然后操作一次再作解释。将源代码文件夹"rtmp"放到ubuntu里,按照"编译说明"先编译zlib库、openssl库、librtmp库,然后编译rtmp推流程序生成名字为"rtmp"的可执行程序,将这个程序通过tftp传到开发板上。然后在服务器上打开命令行进入nginx-rtmp-win32-master目录(注意这个程序的路径一定不要包含中文,如果有什么问题可查看日志),用命令start nginx启动,用tasklist /fi "imagename eq nginx.exe" 查看nginx服务是否启动成功,若启动成功会看到PID会话名,如果启动失败可以查看logs目录下 error.log 文件里的错误信息,根据信息百度查询(我碰到过程序路径含中文导致nginx服务启动不成功)。然后再在开发板上输入./rtmp 192.168.1.101(192.168.1.101是nginx服务器的IP地址)运行推流程序(推流程序一定要在nginx服务启动后才运行)。然后在电脑上打开浏览器输入地址http://192.168.1.101/vod.html启动RTMP网页播放器,在下方的拉流地址输入rtmp://192.168.1.101/live/stream,点击右边播放按钮,等个1秒钟左右就可以看到视频了。
作个比较贴切的比喻,RTSP就像记者自己去抓新闻,这新闻只能在通过自己传播给附近几个人了解。而RTMP就是记者去抓新闻,然后将新闻上交给电视台,电视台播放,然后人民打开电视机转到这个电视台频道就可以收看新闻,这样可以有很多个知道这个新闻。在比喻中,HI3518就是记者,专门采集视频资源,服务器就是电视台,浏览器播放器客户端就是普通民众。在这次使用中,我们用普通电脑搭建nginx服务器,对服务器而言,HI3518就是个客户端,专门推送资源的客户端,所以叫推流端。而浏览器播放视频的客户端就是从服务器拉视频,所以叫拉流端,也叫收流器。
同样,运行这个程序时也生成了一些文件有便于我们调试理解,主要有hi3518的调试输出记录文件及wireshark抓包文件。
以下就是个人自画的RTMP协议
可以发现HI3518作为客户端主动发起请求,不像RTSP协议,HI3518作为服务器等待客户端发起请求。按照网上解释的标准RTMP协议有点复杂,一般我们为了通信方便,可以简化成如上图那样,客户端先发C0+C1,服务器回复S0+S1+S2,然后客户端发送C2完成握手。接下来就是控制信息的通信,客户端请求连接直播,服务器回复消息大小和连接成功的消息。然后客户端请求释放控制信息的流并重新创建一条推送流,服务器回复结果,然后客户端申请推流,服务器回复推流开始,客户端开始推送视频数据。
使用librtmp时,解析RTMP地址、握手、建立流媒体链接和AMF编码这块我们都不需要关心,但是数据是如何打包并通过int RTMP_SendPacket(RTMP *r, RTMPPacket *packet, int queue) 函数推送的还是得学习一下。
解析RTMP地址主要是用strstr,strchr函数分离字符串,然后字符串指针移位这样就能得到字符串内容,具体见代码注释,而且里面还举例说明了执行某条语句后结果会是怎么样的。类似如下:
-
p+=
3;
//p="://192.168.1.100/live/stream",移3个,所以p="192.168.1.100/live/stream"
-
printf(
"in RTMP_ParseURL,p2=%s\n",p);
//in RTMP_ParseURL,p2=192.168.1.101/live/stream
RTMPPacket类型的结构体定义如下,一个RTMPPacket对应RTMP协议规范里面的一个块(Chunk)。
-
typedef
struct RTMPPacket
-
{
-
uint8_t m_headerType;
//块消息头的类型(4种)
-
uint8_t m_packetType;
//消息类型ID(1-7协议控制;8,9音视频;10以后为AMF编码消息)
-
uint8_t m_hasAbsTimestamp;
//时间戳是绝对值还是相对值
-
int m_nChannel;
//块流ID
-
uint32_t m_nTimeStamp;
//时间戳
-
int32_t m_nInfoField2;
//last 4 bytes in a long header,消息流ID
-
uint32_t m_nBodySize;
//消息载荷大小
-
uint32_t m_nBytesRead;
//暂时没用到
-
RTMPChunk *m_chunk;
//暂时没用到
-
char *m_body;
//消息载荷,可分割为多个块载荷
-
} RTMPPacket;
RTMP_SendPacket函数
-
//queue:TRUE为放进发送队列,FALSE是不放进发送队列,直接发送
-
int RTMP_SendPacket(RTMP *r, RTMPPacket *packet, int queue)
-
{
-
const RTMPPacket *prevPacket;
-
uint32_t last =
0;
//上一个块的时间戳
-
int nSize;
//消息载荷大小,可分割为多个块载荷大小
-
int hSize, cSize;
//块头大小,块基本头大小增量
-
char *header, *hptr, *hend, hbuf[RTMP_MAX_HEADER_SIZE], c;
-
//header:指向块头起始位置,hend:指向块头结束位置
-
uint32_t t;
//相对时间戳
-
char *buffer, *tbuf =
NULL, *toff =
NULL;
//buffer:指向消息载荷
-
int nChunkSize;
//块载荷大小
-
int tlen;
-
-
if (packet->m_nChannel >= r->m_channelsAllocatedOut)
-
{
-
int n = packet->m_nChannel +
10;
-
RTMPPacket **packets =
realloc(r->m_vecChannelsOut,
sizeof(RTMPPacket*) * n);
-
if (!packets)
-
{
-
free(r->m_vecChannelsOut);
-
r->m_vecChannelsOut =
NULL;
-
r->m_channelsAllocatedOut =
0;
-
return FALSE;
-
}
-
r->m_vecChannelsOut = packets;
-
memset(r->m_vecChannelsOut + r->m_channelsAllocatedOut,
0,
sizeof(RTMPPacket*) * (n - r->m_channelsAllocatedOut));
-
r->m_channelsAllocatedOut = n;
-
}
-
-
prevPacket = r->m_vecChannelsOut[packet->m_nChannel];
-
//不是完整块消息头(即不是11字节的块消息头)
-
if (prevPacket && packet->m_headerType != RTMP_PACKET_SIZE_LARGE)
-
{
-
/* compress a bit by using the prev packet's attributes */
-
if (prevPacket->m_nBodySize == packet->m_nBodySize
-
&& prevPacket->m_packetType == packet->m_packetType
-
&& packet->m_headerType == RTMP_PACKET_SIZE_MEDIUM)
-
packet->m_headerType = RTMP_PACKET_SIZE_SMALL;
-
-
if (prevPacket->m_nTimeStamp == packet->m_nTimeStamp
-
&& packet->m_headerType == RTMP_PACKET_SIZE_SMALL)
-
packet->m_headerType = RTMP_PACKET_SIZE_MINIMUM;
-
last = prevPacket->m_nTimeStamp;
-
}
-
//非法
-
if (packet->m_headerType >
3)
/* sanity */
-
{
-
RTMP_Log(RTMP_LOGERROR,
"sanity failed!! trying to send header of type: 0x%02x.",
-
(
unsigned
char)packet->m_headerType);
-
return FALSE;
-
}
-
-
//nSize暂时设置为块头大小;packetSize[] = { 12, 8, 4, 1 }
-
nSize = packetSize[packet->m_headerType];
-
hSize = nSize;
//块头大小初始化
-
cSize =
0;
-
//相对时间戳,当块时间戳与上一个块时间戳的差值
-
t = packet->m_nTimeStamp - last;
-
-
if (packet->m_body)
-
{
-
//m_body是指向载荷数据首地址的指针,“-”号用于指针前移
-
//header:块头起始位置
-
header = packet->m_body - nSize;
-
hend = packet->m_body;
//hend:块头结束位置
-
}
-
else
-
{
-
header = hbuf +
6;
-
hend = hbuf +
sizeof(hbuf);
-
}
-
-
if (packet->m_nChannel >
319)
//当块流ID大于319时 ,块基本头是3个字节
-
cSize =
2;
-
else
if (packet->m_nChannel >
63)
//当块流ID大于63时,块基本头是2个字节
-
cSize =
1;
-
if (cSize)
-
{
-
header -= cSize;
//header指针指块头起始位置,“-”号用于指针前移
-
hSize += cSize;
//当cSize不为0时,块头需要进行扩展,默认的块基本头为1字节,但是也可能是2字节或3字节
-
}
-
//相对时间戳大于0xffffff,此时需要使用ExtendTimeStamp
-
if (t >=
0xffffff)
-
{
-
header -=
4;
-
hSize +=
4;
-
RTMP_Log(RTMP_LOGWARNING,
"Larger timestamp than 24-bit: 0x%x", t);
-
}
-
-
hptr = header;
-
c = packet->m_headerType <<
6;
//把块基本头的fmt类型左移6位。
-
switch (cSize)
-
{
-
case
0:
//把块基本头的低6位设置成块流ID
-
c |= packet->m_nChannel;
-
break;
-
case
1:
//同理,但低6位设置成000000
-
break;
-
case
2:
//同理,但低6位设置成000001
-
c |=
1;
-
break;
-
}
-
*hptr++ = c;
//可以拆分成两句*hptr=c;hptr++,此时hptr指向第2个字节
-
if (cSize)
//cSize>0,即块基本头大于1字节
-
{
-
int tmp = packet->m_nChannel -
64;
//将要放到第2字节的内容tmp
-
*hptr++ = tmp &
0xff;
//获取低位存储于第2字节
-
if (cSize ==
2)
//块基本头是最大的3字节时
-
*hptr++ = tmp >>
8;
//获取高位存储于第三个字节(注意:排序使用大端序列,和主机相反)
-
}
-
//块消息头一共有4种,包含的字段数不同,nSize>1,块消息头存在。
-
if (nSize >
1)
-
{
-
//块消息头的最开始三个字节为时间戳,返回值hptr=hptr+3
-
hptr = AMF_EncodeInt24(hptr, hend, t >
0xffffff ?
0xffffff : t);
-
}
-
//如果块消息头包括MessageLength+MessageTypeID(4字节)
-
if (nSize >
4)
-
{
-
hptr = AMF_EncodeInt24(hptr, hend, packet->m_nBodySize);
//消息长度,为消息载荷AMF编码后的长度
-
*hptr++ = packet->m_packetType;
//消息类型ID
-
}
-
-
if (nSize >
8)
-
hptr += EncodeInt32LE(hptr, packet->m_nInfoField2);
-
//相对时间戳大于0xffffff,此时需要使用ExtendTimeStamp
-
if (t >=
0xffffff)
-
hptr = AMF_EncodeInt32(hptr, hend, t);
-
-
nSize = packet->m_nBodySize;
//消息载荷大小
-
buffer = packet->m_body;
//消息载荷指针
-
nChunkSize = r->m_outChunkSize;
//块大小,默认128字节
-
-
RTMP_Log(RTMP_LOGDEBUG2,
"%s: fd=%d, size=%d", __FUNCTION__, r->m_sb.sb_socket,nSize);
-
/* send all chunks in one HTTP request */
//使用HTTP
-
if (r->Link.protocol & RTMP_FEATURE_HTTP)
-
{
-
//nSize:消息载荷大小;nChunkSize:块载荷大小
-
//例nSize:307,nChunkSize:128;
-
//可分为(307+128-1)/128=3个
-
//为什么减1?因为除法会只取整数部分!
-
int chunks = (nSize+nChunkSize
-1) / nChunkSize;
-
if (chunks >
1)
-
{
-
//消息分n块后总的开销:
-
//n个块基本头,1个块消息头,1个消息载荷,这里是没有扩展时间戳的情况
-
//实际中只有第一个块是完整的,剩下的只有块基本头
-
tlen = chunks * (cSize +
1) + nSize + hSize;
-
tbuf =
malloc(tlen);
-
if (!tbuf)
-
return FALSE;
-
toff = tbuf;
-
}
-
}
-
while (nSize + hSize)
-
{
-
int wrote;
-
//消息载荷小于块载荷(不用分块)
-
if (nSize < nChunkSize)
-
nChunkSize = nSize;
-
-
RTMP_LogHexString(RTMP_LOGDEBUG2, (
uint8_t *)header, hSize);
-
RTMP_LogHexString(RTMP_LOGDEBUG2, (
uint8_t *)buffer, nChunkSize);
-
if (tbuf)
-
{
-
memcpy(toff, header, nChunkSize + hSize);
-
toff += nChunkSize + hSize;
-
}
-
else
-
{
-
wrote = WriteN(r, header, nChunkSize + hSize);
-
if (!wrote)
-
return FALSE;
-
}
-
nSize -= nChunkSize;
-
buffer += nChunkSize;
-
hSize =
0;
-
//如果消息没有发完
-
if (nSize >
0)
-
{
-
header = buffer -
1;
-
hSize =
1;
-
if (cSize)
-
{
-
header -= cSize;
-
hSize += cSize;
-
}
-
if (t >=
0xffffff)
-
{
-
header -=
4;
-
hSize +=
4;
-
}
-
*header = (
0xc0 | c);
-
if (cSize)
-
{
-
int tmp = packet->m_nChannel -
64;
-
header[
1] = tmp &
0xff;
-
if (cSize ==
2)
-
header[
2] = tmp >>
8;
-
}
-
if (t >=
0xffffff)
-
{
-
char* extendedTimestamp = header +
1 + cSize;
-
AMF_EncodeInt32(extendedTimestamp, extendedTimestamp +
4, t);
-
}
-
}
-
}
-
if (tbuf)
-
{
-
int wrote = WriteN(r, tbuf, toff-tbuf);
-
free(tbuf);
-
tbuf =
NULL;
-
if (!wrote)
-
return FALSE;
-
}
-
-
/* we invoked a remote method */
-
if (packet->m_packetType == RTMP_PACKET_TYPE_INVOKE)
-
{
-
AVal method;
-
char *ptr;
-
ptr = packet->m_body +
1;
-
AMF_DecodeString(ptr, &method);
-
RTMP_Log(RTMP_LOGDEBUG,
"Invoking %s", method.av_val);
-
/* keep it in call queue till result arrives */
-
if (
queue)
-
{
-
int txn;
-
ptr +=
3 + method.av_len;
-
txn = (
int)AMF_DecodeNumber(ptr);
-
AV_queue(&r->m_methodCalls, &r->m_numCalls, &method, txn);
-
}
-
}
-
-
if (!r->m_vecChannelsOut[packet->m_nChannel])
-
r->m_vecChannelsOut[packet->m_nChannel] =
malloc(
sizeof(RTMPPacket));
-
memcpy(r->m_vecChannelsOut[packet->m_nChannel], packet,
sizeof(RTMPPacket));
-
return TRUE;
-
}
一路跟踪RTMP_SendPacket函数下去会发现RTMP_SendPacket-->WriteN-->RTMPSockBuf_Send-->send,最终会调用网络编程的send函数。
整个程序也是生产者消费者模式,SAMPLE_VENC_1080P_CLASSIC线程产生数据放到环形缓冲区,主线程从环形缓冲区取出数据进入rtmp_sender_write_video_frame函数将H264的NALU打包进消息载荷.
rtmp_sender_write_video_frame函数如下:
-
// @brief send video frame, now only H264 supported
-
// @param [in] rtmp_sender handler
-
// @param [in] data : video data, (Full frames are required)
-
// @param [in] size : video data size
-
// @param [in] dts_us : decode timestamp of frame
-
// @param [in] key : key frame indicate, [0: non key] [1: key]
-
//rtmp_sender_write_video_frame(prtmp,ringinfo.buffer, ringinfo.size,timeCount, 0,start_time);
-
/*
-
功能:按FLV格式填充内容并发送出去
-
参数:
-
handle: in/our,rtmp结构体,根据rtmp结构体里的配置来进行不同的处理
-
data: in,要发送的视频数据
-
size: in,要发送的视频数据的大小
-
dts_us: in,时间戳
-
key: 是否是关键帧,这里不需要,因为在函数里通过读取nalu[0]来判断是否是关键帧
-
start_time: 时间戳的起始时间
-
返回值:成功返回0,失败返回1
-
*/
-
int rtmp_sender_write_video_frame(void *handle,uint8_t *data,int size,uint64_t dts_us,int key,uint32_t start_time)
-
{
-
uint8_t * buf;
-
uint8_t * buf_offset;
-
int total;
-
uint32_t ts;
-
uint32_t nal_len;
-
uint32_t nal_len_n;
-
uint8_t *nal;
-
uint8_t *nal_n;
-
char *output ;
-
uint32_t offset =
0;
-
uint32_t body_len;
-
uint32_t output_len;
-
RTMP_XIECC *rtmp_xiecc;
-
RTMP *rtmp;
-
-
-
buf = data;
-
buf_offset = data;
-
total = size;
-
ts = (
uint32_t)dts_us;
-
rtmp_xiecc = (RTMP_XIECC *)handle;
-
if ((data ==
NULL) || (rtmp_xiecc ==
NULL))
-
{
-
return
1;
-
}
-
rtmp = rtmp_xiecc->rtmp;
-
//printf("ts is %d\n",ts);
-
-
while (
1)
-
{
-
//ts = RTMP_GetTime() - start_time; //
-
//by ssy
-
offset =
0;
-
nal = get_nal(&nal_len, &buf_offset, buf, total);
-
if (nal ==
NULL)
-
break;
-
if (nal[
0] ==
0x67)
//打包sps,pps这里没有if (nal[0] == 0x68),但这里直接处理了两个nal即把nal[0] == 0x68也处理了
-
{
-
if (rtmp_xiecc->video_config_ok >
0)
-
{
-
continue;
//only send video sequence set one time
-
}
-
nal_n = get_nal(&nal_len_n, &buf_offset, buf, total);
//get pps//sps后面一般是紧跟pps 0x68
-
if (nal_n ==
NULL)
-
{
-
RTMP_Log(RTMP_LOGERROR,
"No Nal after SPS");
-
break;
-
}
-
/*
-
FLVtag:其实就是消息=消息头flvtag header+载荷
-
*/
-
body_len = nal_len + nal_len_n +
16;
//h264封装成rtmp包:16字节信息头+数据
-
output_len = body_len + FLV_TAG_HEAD_LEN + FLV_PRE_TAG_LEN;
-
//0x67+0x68两帧的长度+16字节+FLV_TAG_HEAD_LEN+FLV_PRE_TAG_LEN
-
output =
malloc(output_len);
-
/// flv tag header
-
//消息头=标志消息类型的Message Type ID(1byte)+标志载荷长度的Payload Length(3byte)+标识时间戳的Timestamp(4byte)+标识消息所属媒体流的Stream ID(3byte)
-
output[offset++] =
0x09;
//tagtype video
-
//output[0]=0x09消息类型,9代表视频,1字节
-
output[offset++] = (
uint8_t)(body_len >>
16);
//data len
-
output[offset++] = (
uint8_t)(body_len >>
8);
//data len
-
output[offset++] = (
uint8_t)(body_len);
//data len
-
//output[1-3]有效数据长度,3字节
-
output[offset++] = (
uint8_t)(ts >>
16);
//time stamp
-
output[offset++] = (
uint8_t)(ts >>
8);
//time stamp
-
output[offset++] = (
uint8_t)(ts);
//time stamp
-
output[offset++] = (
uint8_t)(ts >>
24);
//time stamp
-
//output[4-7]时间戳,4字节
-
output[offset++] =
0x00;
//stream id 0
-
output[offset++] =
0x00;
//stream id 0
-
output[offset++] =
0x00;
//stream id 0
-
//output[8-10]媒体流ID,3字节
-
//
-
-
///flv VideoTagHeader///
-
///flvvideotagheader/
-
//sps,pps填0x17 00,composit time无效填0
-
output[offset++] =
0x17;
//key frame, AVC
-
//output[11]高4位表示帧类型,这里是关键帧 1,低4位表示编码模式,这里是AVC,7
-
output[offset++] =
0x00;
//avc sequence header
-
//output[12]
-
output[offset++] =
0x00;
//composit time ??????????
-
output[offset++] =
0x00;
// composit time
-
output[offset++] =
0x00;
//composit time
-
//output[13-15]AVC时,composit time没有意义,所以全填为0
-
/
-
-
/flvvideotagbody/
-
//flv VideoTagBody --AVCDecoderCOnfigurationRecord
-
output[offset++] =
0x01;
//configurationversion//sps,pps专用,填版本号1
-
output[offset++] = nal[
1];
//avcprofileindication
-
output[offset++] = nal[
2];
//profilecompatibilty
-
output[offset++] = nal[
3];
//avclevelindication
-
output[offset++] =
0xff;
//reserved + lengthsizeminusone//一般填ff
-
output[offset++] =
0xe1;
//numofsequenceset//一般填e1
-
//sps len
-
output[offset++] = (
uint8_t)(nal_len >>
8);
//sequence parameter set length high 8 bits
-
output[offset++] = (
uint8_t)(nal_len);
//sequence parameter set length low 8 bits
-
//sps data
-
memcpy(output + offset, nal, nal_len);
//H264 sequence parameter set
-
offset += nal_len;
-
//pps一个数,一般为1
-
output[offset++] =
0x01;
//numofpictureset
-
//pps len
-
output[offset++] = (
uint8_t)(nal_len_n >>
8);
//picture parameter set length high 8 bits
-
output[offset++] = (
uint8_t)(nal_len_n);
//picture parameter set length low 8 bits
-
//pps data
-
memcpy(output + offset, nal_n, nal_len_n);
//H264 picture parameter set
-
/
-
//
-
//no need set pre_tag_size ,RTMP NO NEED
-
// flv test
-
/*
-
offset += nal_len_n;
-
uint32_t fff = body_len + FLV_TAG_HEAD_LEN;
-
output[offset++] = (uint8_t)(fff >> 24); //data len
-
output[offset++] = (uint8_t)(fff >> 16); //data len
-
output[offset++] = (uint8_t)(fff >> 8); //data len
-
output[offset++] = (uint8_t)(fff); //data len
-
*/
-
RTMP_Write(rtmp, output, output_len);
-
//RTMP Send out
-
free(output);
-
rtmp_xiecc->video_config_ok =
1;
-
continue;
-
}
-
-
if (nal[
0] ==
0x65)
-
{
-
body_len = nal_len +
5 +
4;
//flv VideoTagHeader + NALU length
-
output_len = body_len + FLV_TAG_HEAD_LEN + FLV_PRE_TAG_LEN;
-
output =
malloc(output_len);
-
// flv tag header
-
output[offset++] =
0x09;
//tagtype video
-
output[offset++] = (
uint8_t)(body_len >>
16);
//data len
-
output[offset++] = (
uint8_t)(body_len >>
8);
//data len
-
output[offset++] = (
uint8_t)(body_len);
//data len
-
output[offset++] = (
uint8_t)(ts >>
16);
//time stamp
-
output[offset++] = (
uint8_t)(ts >>
8);
//time stamp
-
output[offset++] = (
uint8_t)(ts);
//time stamp
-
output[offset++] = (
uint8_t)(ts >>
24);
//time stamp
-
output[offset++] =
0x00;
//stream id 0
-
output[offset++] =
0x00;
//stream id 0
-
output[offset++] =
0x00;
//stream id 0
-
-
//flv VideoTagHeader
-
output[offset++] =
0x17;
//key frame, AVC
-
output[offset++] =
0x01;
//avc NALU unit
-
output[offset++] =
0x00;
//composit time ??????????
-
output[offset++] =
0x00;
// composit time
-
output[offset++] =
0x00;
//composit time
-
-
output[offset++] = (
uint8_t)(nal_len >>
24);
//nal length
-
output[offset++] = (
uint8_t)(nal_len >>
16);
//nal length
-
output[offset++] = (
uint8_t)(nal_len >>
8);
//nal length
-
output[offset++] = (
uint8_t)(nal_len);
//nal length
-
memcpy(output + offset, nal, nal_len);
-
-
//no need set pre_tag_size ,RTMP NO NEED
-
/*
-
offset += nal_len;
-
uint32_t fff = body_len + FLV_TAG_HEAD_LEN;
-
output[offset++] = (uint8_t)(fff >> 24); //data len
-
output[offset++] = (uint8_t)(fff >> 16); //data len
-
output[offset++] = (uint8_t)(fff >> 8); //data len
-
output[offset++] = (uint8_t)(fff); //data len
-
*/
-
RTMP_Write(rtmp, output, output_len);
-
//RTMP Send out
-
free(output);
-
continue;
-
}
-
-
if ((nal[
0] &
0x1f) ==
0x01)
-
{
-
body_len = nal_len +
5 +
4;
//flv VideoTagHeader + NALU length
-
output_len = body_len + FLV_TAG_HEAD_LEN + FLV_PRE_TAG_LEN;
-
output =
malloc(output_len);
-
// flv tag header
-
output[offset++] =
0x09;
//tagtype video
-
output[offset++] = (
uint8_t)(body_len >>
16);
//data len
-
output[offset++] = (
uint8_t)(body_len >>
8);
//data len
-
output[offset++] = (
uint8_t)(body_len);
//data len
-
output[offset++] = (
uint8_t)(ts >>
16);
//time stamp
-
output[offset++] = (
uint8_t)(ts >>
8);
//time stamp
-
output[offset++] = (
uint8_t)(ts);
//time stamp
-
output[offset++] = (
uint8_t)(ts >>
24);
//time stamp
-
output[offset++] =
0x00;
//stream id 0
-
output[offset++] =
0x00;
//stream id 0
-
output[offset++] =
0x00;
//stream id 0
-
-
//flv VideoTagHeader
-
output[offset++] =
0x27;
//not key frame, AVC
-
output[offset++] =
0x01;
//avc NALU unit
-
output[offset++] =
0x00;
//composit time ??????????
-
output[offset++] =
0x00;
// composit time
-
output[offset++] =
0x00;
//composit time
-
-
output[offset++] = (
uint8_t)(nal_len >>
24);
//nal length
-
output[offset++] = (
uint8_t)(nal_len >>
16);
//nal length
-
output[offset++] = (
uint8_t)(nal_len >>
8);
//nal length
-
output[offset++] = (
uint8_t)(nal_len);
//nal length
-
memcpy(output + offset, nal, nal_len);
-
-
//no need set pre_tag_size ,RTMP NO NEED
-
/*
-
offset += nal_len;
-
uint32_t fff = body_len + FLV_TAG_HEAD_LEN;
-
output[offset++] = (uint8_t)(fff >> 24); //data len
-
output[offset++] = (uint8_t)(fff >> 16); //data len
-
output[offset++] = (uint8_t)(fff >> 8); //data len
-
output[offset++] = (uint8_t)(fff); //data len
-
*/
-
RTMP_Write(rtmp, output, output_len);
-
-
//RTMP Send out
-
free(output);
-
continue;
-
}
-
}
-
return
0;
-
}
rtmp_sender_write_video_frame这个函数主要是先get_nal取出H264的nal单元,然后根据nal单元是sps/pps、0x65的关键帧还是0x61的其它帧对output数组作不同的填充,最后RTMP_Write函数发送出去。RTMP_Write函数会对output数组进行AMF编码然后利用上面提到的RTMP_SendPacket函数(最终会调用网络编程send函数)将数据发送出去。
现在要解决的是如何给结构体RTMPPacket中的消息载荷m_body赋值,即如何将H264的NALU打包进消息载荷。为了说明打包过程,大家请看下面3张图,结合代码分析:
图1
图2
表1
由于RTMP推送的音视频流的封装形式和FLV格式相似,向FMS等流媒体服务器推送H264和AAC直播流时,需要首先发送"AVC sequence header"和"AAC sequence header"这两项数据包含的是重要的编码信息,没有它们,解码器将无法解码),因此这里的"AVC sequence header"就是用来打包sps和pps的。所以要按FLV格式来把H264内容封装成FLV包。而FLV格式如下:
FLV=FLVheader(9字节)+FLVbody
FLVheader(9字节):是“FLV”开头的9个字节,一般固定
FLVbody=pre_tag_size(4字节,上一个tag的大小)+FLVtag
pre_tag_size(4字节,上一个tag的大小):程序好像没用
FLVtag:其实就是消息=消息头flvtag header+载荷flvxxxtag///程序里填的就是FLVtag
消息头flvtag header=消息类型1byte+载荷长度3byte+时间戳4byte+媒体流ID3byte///sps,pps、关键帧,非关键帧填的都是一样
载荷flvxxxtag:分为flvaudiotag,flvvideotag等,对视频而言就是flvvideotag///sps,pps、关键帧,非关键帧填的都是不同
flvvideotag=flvvideotagheader+flvvideotagbody具体因H264帧不同而不同,见如下分析:
flvvideotag如下:
flvvideotag=flvvideotagheader+flvvideotagbody
载荷flvvideotag
看图1的红框1,对sps,pps而言,帧类型为关键帧=1,编码类型为AVC=7,所以output[offset++] = 0x17; //key frame, AVC
看图1的红框2,因为CodecID=7,所以要看AVCVIDEOPACKET即图2
看图2的红框A,因为现在是sps,pps,所以是AVCPacketType=AVC sequence header=0,output[offset++] = 0x00; //avc sequence header
看图2的红框B,因为AVCPacketType=AVC sequence header=0,所以CompositionTime=0,所以output接下来三个都为0
看图2的红框C,因为AVCPacketType=AVC sequence header=0,所以要找AVCDecoderConfigurationRecord,即图3
看图3,是个表格,看第一行是版本号,填1,所以填output[offset++]=0x01
看表格第2-4行填sps,所以output[offset++] = nal[1]; output[offset++] = nal[2]; output[offset++] = nal[3];
看表格第5行为保留,5位全是1
看表格第6行,sps长度,一般为3(11),与第5行的5位凑成0xff,所以output[offset++] = 0xff;
看表格第7行为保留,3位为1
看表格第8行为pps个数,一般为1(00001),与第7行凑成0xe1,所以output[offset++] = 0xe1;
看表格第9行为sps大小和sps的内容,所以output[offset++] = (uint8_t)(nal_len >> 8);output[offset++] = (uint8_t)(nal_len);
sps内容为memcpy(output + offset, nal, nal_len);
然后偏移sps帧的大小offset += nal_len;
看表格第10行填pps个数,一般为1,output[offset++] = 0x01;
看表格第11行填pps大小和pps内容,所以output[offset++] = (uint8_t)(nal_len_n >> 8);output[offset++] = (uint8_t)(nal_len_n);
填pps内容memcpy(output + offset, nal_n, nal_len_n);
最后发送出去RTMP_Write(rtmp, output, output_len);并标记配置完成rtmp_xiecc->video_config_ok = 1;
关键帧0x65封装:
看图1的红框1,对关键帧0x65而言,帧类型为关键帧=1,编码类型为AVC=7,所以output[offset++] = 0x17; //key frame, AVC
看图1的红框2,因为CodecID=7,所以要看AVCVIDEOPACKET即图2
看图2的红框A,因为现在是关键帧0x65,所以是AVCPacketType=AVC NALU=1,output[offset++] = 0x01; //avc NALU unit
看图2的红框B,因为AVCPacketType=AVC NALU=1,我们不想填时间,所以CompositionTime=0,所以output接下来三个都为0
看图2的红框C,因为AVCPacketType=AVC NALU=1,所以要找NALU,而NALU就是4个字节表示NALU长度,后面字节表示NALU内容
所以output[offset++] = (uint8_t)(nal_len >> 24); //nal length
output[offset++] = (uint8_t)(nal_len >> 16); //nal length
output[offset++] = (uint8_t)(nal_len >> 8); //nal length
output[offset++] = (uint8_t)(nal_len); //nal length
表示NALU长度
memcpy(output + offset, nal, nal_len);表示NALU内容
最后发送出去RTMP_Write(rtmp, output, output_len);
非关键帧nal[0] & 0x1f) == 0x01封装:
看图1的红框1,对关键帧0x01而言,帧类型为非关键帧=2,编码类型为AVC=7,所以output[offset++] = 0x27; //not key frame, AVC
看图1的红框2,因为CodecID=7,所以要看AVCVIDEOPACKET即图2
看图2的红框A,因为现在是非关键帧0x01,还是AVCPacketType=AVC NALU=1,output[offset++] = 0x01; //avc NALU unit
看图2的红框B,因为AVCPacketType=AVC NALU=1,我们不想填时间,所以CompositionTime=0,所以output接下来三个都为0
看图2的红框C,因为AVCPacketType=AVC NALU=1,所以要找NALU,而NALU就是4个字节表示NALU长度,后面字节表示NALU内容
所以output[offset++] = (uint8_t)(nal_len >> 24); //nal length
output[offset++] = (uint8_t)(nal_len >> 16); //nal length
output[offset++] = (uint8_t)(nal_len >> 8); //nal length
output[offset++] = (uint8_t)(nal_len); //nal length
表示NALU长度
memcpy(output + offset, nal, nal_len);表示NALU内容
最后发送出去RTMP_Write(rtmp, output, output_len);
Hi3518+RTMP工程文件:Hi3518+RTMP
<li class="tool-item tool-active is-like "><a href="javascript:;"><svg class="icon" aria-hidden="true"> <use xlink:href="#csdnc-thumbsup"></use> </svg><span class="name">点赞</span> <span class="count">1</span> </a></li> <li class="tool-item tool-active is-collection "><a href="javascript:;" data-report-click="{"mod":"popu_824"}"><svg class="icon" aria-hidden="true"> <use xlink:href="#icon-csdnc-Collection-G"></use> </svg><span class="name">收藏</span></a></li> <li class="tool-item tool-active is-share"><a href="javascript:;" data-report-click="{"mod":"1582594662_002"}"><svg class="icon" aria-hidden="true"> <use xlink:href="#icon-csdnc-fenxiang"></use> </svg>分享</a></li> <!--打赏开始--> <!--打赏结束--> <li class="tool-item tool-more"> <a> <svg t="1575545411852" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5717" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M179.176 499.222m-113.245 0a113.245 113.245 0 1 0 226.49 0 113.245 113.245 0 1 0-226.49 0Z" p-id="5718"></path><path d="M509.684 499.222m-113.245 0a113.245 113.245 0 1 0 226.49 0 113.245 113.245 0 1 0-226.49 0Z" p-id="5719"></path><path d="M846.175 499.222m-113.245 0a113.245 113.245 0 1 0 226.49 0 113.245 113.245 0 1 0-226.49 0Z" p-id="5720"></path></svg> </a> <ul class="more-box"> <li class="item"><a class="article-report">文章举报</a></li> </ul> </li> </ul> </div> </div> <div class="person-messagebox"> <div class="left-message"><a href="https://blog.csdn.net/zhanshenrui"> <img src="https://profile.csdnimg.cn/0/C/B/3_zhanshenrui" class="avatar_pic" username="zhanshenrui"> <img src="https://g.csdnimg.cn/static/user-reg-year/1x/12.png" class="user-years"> </a></div> <div class="middle-message"> <div class="title"><span class="tit"><a href="https://blog.csdn.net/zhanshenrui" data-report-click="{"mod":"popu_379"}" target="_blank">zhanshenrui</a></span> </div> <div class="text"><span>发布了6 篇原创文章</span> · <span>获赞 15</span> · <span>访问量 1万+</span></div> </div> <div class="right-message"> <a href="https://im.csdn.net/im/main.html?userName=zhanshenrui" target="_blank" class="btn btn-sm btn-red-hollow bt-button personal-letter">私信 </a> <a class="btn btn-sm bt-button personal-watch" data-report-click="{"mod":"popu_379"}">关注</a> </div> </div> </div>