前面介绍了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 = hb