基于librtmp的直播推流方案

最近在做关于rtmp直播推流的项目,本身对rtmp也是一窍不通的,关于连接封装方面的资料也是看了很多,软件实现还是没有什么头绪,在看了雷神的基于librtmp的例子后(https://blog.csdn.net/leixiaohua1020/article/details/42105049) 在大神的*上修改,增加了一些东西,简单实现了一个摄像头rtmp实时推流的功能。

在看这篇文章之前,我还是建议大家先去上文给的雷神的博文中看看,里面还有大神写的源代码,本文实现的推流功能在关于librtmp库(rtmp的初始化,连接及发送接口)的运用方面都是参照雷神的来写的,没有什么区别,关于这部分代码的详解我会另起一篇或几篇文章来写(有关RTMP的东西实在太多),这篇文章主要介绍的是我跟雷神写的不同的地方,看过雷神的例子应该都知道,雷神的例子是推一个H264的视频文件,而在直播中,码流数据大多时候是不会存在本地(摄像头本身可用内存就少)而是直接推送到流媒体服务器上,推完之后缓冲就会被清除,所以在关于码流数据的循环读取和拆分部分需要重写,主要是三个函数ReadFirstNaluFromBuf(),ReadNaluFromBuf(),FindNaluFromNextPacket()。

在分析了海思,RealTek等厂家的编码芯片出来的码流可以发现,视频开始的第一个数据包通常都由SPS,PPS,I帧构成,接下来的几个数据包大多都是P帧,然后又会有一个包含了SPS,PPS,I帧的数据包出现,这样往复循环。而RTMP是不能直接推h264码流的,必须把264码流封装成FLV格式才能推出去。所以,这个264的RTMP的主要实现思想就是,把每包数据以NALU为单位拆分开来,并对每一个NALU进行FLV格式封装,封装好后调用librtmp的发送接口发送出去。

  1. 循环读取,拆分码流数据示意图(三个函数运作流程图)
    基于librtmp的直播推流方案

现在上代码
1.ReadFirstNaluFromBuf()分离码流包第一个Nalu,当头下标nalhead_pos为0时调用

int ReadFirstNaluFromBuf(NaluUnit *nalu,unsigned char *buf, unsigned int  buf_size) 
{
    
	int naltail_pos=nalhead_pos;
	if(buf_tmp != NULL){
       free(buf_tmp);
	}
	buf_tmp=(unsigned char*)malloc(buf_size);
	memset(buf_tmp,0,buf_size);
	//Nalu头标志为00000001,头下标找到第一个Nalu头标志
	while(nalhead_pos<buf_size)  
	{  
	    
		if(buf[nalhead_pos++] == 0x00 && 
			buf[nalhead_pos++] == 0x00) 
		{ 	
				if(buf[nalhead_pos++] == 0x00 && 
					buf[nalhead_pos++] == 0x01 ){
				   goto gotnal_head;
				}
				
				else
					continue;
		 }
		else 
			continue;

		
gotnal_head:
		//尾下标找到下一个Nalu头标志
		naltail_pos = nalhead_pos;  
		while (naltail_pos<buf_size)  
		{  
			if(buf[naltail_pos++] == 0x00 && 
				buf[naltail_pos++] == 0x00 )
			{  
					if(buf[naltail_pos++] == 0x00 &&
						buf[naltail_pos++] == 0x01)
					{	
						nalu->size = (naltail_pos-4)-nalhead_pos;
						break;
					}
			}  
		}
        //在此码流包找不到下一个Nalu头标志,说明在下一个包还有此Nalu数据
		if(nalhead_pos<buf_size && naltail_pos>=buf_size ){
			nalu->size = buf_size - nalhead_pos;
			nalu->type = buf[nalhead_pos]&0x1f; 
		    memcpy(buf_tmp,buf+nalhead_pos,nalu->size+1);//先把这个包的Nalu数据保存起来
			nalu->size = FindNaluFromNextPacket(nalu->size,buf_size);//不断将这个包的NALU剩余数据放到buf_tmp中,以便一起发送
			nalhead_pos = 0;//FindNaluFromNextPacket()函数运行后读指针必定指向新的码流包,所以置0重新调用本函数。
			if(nalu->size == FALSE){
              printf("FindNaluFromNextPacket file \n");
			  return FALSE;
			}
		}
        //在此码流找到下一个Nalu头标志
		 if(nalhead_pos<buf_size && naltail_pos<buf_size){
		 nalu->type = buf[nalhead_pos]&0x1f;
		 memcpy(buf_tmp,buf+nalhead_pos,nalu->size+1);
         nalhead_pos=naltail_pos;
		}

		nalu->data=buf_tmp;
		return TRUE;   		
	}
	return TRUE;
}

2.ReadNaluFromBuf()分离码流包中Nalu,当头下标nalhead_pos不为0时调用

int ReadNaluFromBuf(NaluUnit *nalu,unsigned char *buf, unsigned int  buf_size) 
{
	int naltail_pos=nalhead_pos;
	int nalustart;
	if(buf_tmp != NULL){
       free(buf_tmp);
	}
	buf_tmp=(unsigned char*)malloc(buf_size);
	memset(buf_tmp,0,buf_size);
	nalu->size=0;
				
		while(naltail_pos<buf_size)  
		{  
				
			if(buf[naltail_pos++] == 0x00 && 
				buf[naltail_pos++] == 0x00) 
			{
							
				if(buf[naltail_pos++] == 0x00 && 
					buf[naltail_pos++] == 0x01)
				{
					nalustart=4;
					goto gotnal;
				}
				else
				continue;
				
			}
			else
              continue;
			
				

			gotnal:	
 				
					nalu->type = buf[nalhead_pos]&0x1f; 
					nalu->size=naltail_pos-nalhead_pos-nalustart;
					if(nalu->type==0x06)
					{
						nalhead_pos=naltail_pos;
						continue;
					}
					memcpy(buf_tmp,buf+nalhead_pos,nalu->size+1);
					nalu->data=buf_tmp;
					nalhead_pos=naltail_pos;
					return TRUE;    
								
		}

		
		//此包找不到下一NALU
		if(naltail_pos>=buf_size && nalhead_pos<buf_size)
		{ 
            nalu->size = buf_size - nalhead_pos;
			nalu->type = buf[nalhead_pos]&0x1f; 
		    memcpy(buf_tmp,buf+nalhead_pos,nalu->size+1);
			nalu->size = FindNaluFromNextPacket(nalu->size,buf_size);
			nalhead_pos = 0; 
			if(nalu->size == FALSE){
              printf("FindNaluFromNextPacket file \n");
			  return FALSE;
			}
			return TRUE;
		}
      
		if(naltail_pos>=buf_size && nalhead_pos>=buf_size){
			nalhead_pos = 0;
            return FALSE;
		}
		
	return FALSE;  
}

3.FindNaluFromNextPacket()更新数据包直至找到下一NALU为止

int FindNaluFromNextPacket(int size,unsigned int buf_size){
	int nal_pos = 0;
	int nalustate = TRUE;
	
	
  do{//找到下一NALU头为止
	nal_pos = 0;
	struct rts_av_buffer *pdata=NULL;
    memset(&stData,0,sizeof (stData));
   
    //usleep(1000);
   if (rts_av_poll(h264flag))//检查 Channel 中是否有数据可读(获取码流数据底层接口)
			continue;
   if (rts_av_recv(h264flag, &pdata))//接收数据放入buffer(获取码流数据底层接口)
			continue;
   
		 if(stDataPacket != NULL){
              free(stDataPacket);
	          //printf("free buffer \n");
         }
	 	stData.pSize = pdata->bytesused;
        stDataPacket=(unsigned char*)malloc(pdata->bytesused);
		if(stDataPacket)
		//printf("malloc buffer success \n");
	    memset(stDataPacket,0,pdata->bytesused);
		memcpy(stDataPacket, (unsigned char *)pdata->vm_addr,pdata->bytesused);
		stData.pData = stDataPacket;
	    rts_av_put_buffer(pdata);//减少 buffer 的引用计数
	    
   	

	 /*if (stDataPacket) {
			fwrite(stData.pData, 1,
				stData.pSize, pfile);	
	 }*/
	
   
	 while(nal_pos<stData.pSize) {  //跳出此循环说明新数据包遍历完毕或找到nalu头
	    
		if(stData.pData[nal_pos++] == 0x00 && 
			stData.pData[nal_pos++] == 0x00) { 
			
				if(stData.pData[nal_pos++] == 0x00 && 
					stData.pData[nal_pos++] == 0x01 ){
				   nalustate = FALSE;
				   break;
				}
				
				else
                  continue;	
		}
		else
           continue;
		
	 }
	 
	//未找到下一nalu头但到了此包尾,将buf_tmp扩大容量以放入上一NALU剩余数据
	 if(nalustate && nal_pos>=stData.pSize){
      if((buf_tmp = (unsigned char*)realloc(buf_tmp,nal_pos + buf_size)) == NULL){
         return FALSE;
      }
	  memcpy(buf_tmp+size,stData.pData,nal_pos+1);
	  size = size + nal_pos;//记录NALU长度
	  buf_size = nal_pos + buf_size;//记录BUFFER长度用以扩大容量
	 }
	 
	
    }while(nalustate);

	return size;
}

这三个函数在成功取到一个NALU的相关数据后就会将这些数据存到定义的NALU结构体里面,将这个结构体里面的数据进行封装并发送就可以实现实时的rtmp推流了。这里把完整的demo放上来https://download.csdn.net/download/weixin_36983723/10753424 ,有兴趣的可以看一下。