前言
由於hi3516a海思自帶的開發應用程序是通過攝像頭接口或HDMI接口獲取視頻數據後並進行存儲。然而,在實際應用中,多是獲取數據後直接通過網絡把數據發送出去。那麼本文章將開始學習hi3516a獲取數據後通過網線和RTP協議把數據實時發送出去。
背景:hi3516a開發板通過HDMI接口獲取BT1120數據後進行壓縮,並通過RTP協議進行實時的視頻直播。
硬件平臺:hi3516a
軟件平臺:Hi3516A_SDK_V1.0.5.0
視頻數據接口:HDMI
無私分享,從我做起!
源碼解析
下面首先看主程序的源碼,源碼來源於網絡,一步一步進行分析。
int main(int argc, char* argv[])
{
int s32MainFd,temp;
struct timespec ts = { 0, 200000000 }; //200000000ns=200000us=200ms=0.2s
pthread_t id;
ringmalloc(1920*1080);//分配緩衝區並進行初始化
printf("RTSP server START\n");
PrefsInit();//設置服務器信息全局變量,獲取主機name
printf("listen for client connecting...\n");
signal(SIGINT, IntHandl); //異常中止信號處理
s32MainFd = tcp_listen(SERVER_RTSP_PORT_DEFAULT); //以非阻塞方式偵聽554端口
printf("s32MainFd=%d\r\n",s32MainFd);
if (ScheduleInit() == ERR_FATAL)
//線程的結構體進行初始化,創建處理主線程schedule_do,schedule_do裏調用sched[i].play_action把數據發送出去
{
fprintf(stderr,"Fatal: Can't start scheduler %s, %i \nServer is aborting.\n", __FILE__, __LINE__);
return 0;
}
RTP_port_pool_init(RTP_DEFAULT_PORT); //554,初始化10個RTP port
pthread_create(&id,NULL,SAMPLE_VENC_1080P_CLASSIC_RTSP,NULL); //海思芯片內部獲取編碼後數據
while (!g_s32Quit)
{
nanosleep(&ts, NULL);
EventLoop(s32MainFd);//RTSP服務器連接處理函數入口
}
sleep(2);
ringfree();
printf("The Server quit!\n");
return 0;
}
其中ringmalloc(1920*1080);是分配緩衝區並進行初始化,我的hdmi數據數據分辨率是1920X1080p,下面看下ringmalloc()函數源碼。主要是分配64個1920X1080p的fifo空間並初始化一些參數。
/* 環形緩衝區的地址編號計算函數,如果到達喚醒緩衝區的尾部,將繞回到頭部。
環形緩衝區的有效地址編號爲:0到(NMAX-1)
*/
void ringmalloc(int size)
{
int i;
for(i =0; i<NMAX; i++) //64
{
ringfifo[i].buffer = malloc(size);
ringfifo[i].size = 0;
ringfifo[i].frame_type = 0;
printf("FIFO INFO:idx:%d,len:%d,ptr:%x\n",i,ringfifo[i].size,(int)(ringfifo[i].buffer));
}
iput = 0; /* 環形緩衝區的當前放入位置 */
iget = 0; /* 緩衝區的當前取出位置 */
n = 0; /* 環形緩衝區中的元素總數量 */
}
接着查看PrefsInit()函數,主要是設置服務器信息全局變量,獲取主機name。
void PrefsInit()
{
int l;
//設置服務器信息全局變量
stPrefs.port = SERVER_RTSP_PORT_DEFAULT; //554
gethostname(stPrefs.hostname,sizeof(stPrefs.hostname));
l=strlen(stPrefs.hostname);
if (getdomainname(stPrefs.hostname+l+1,sizeof(stPrefs.hostname)-l)!=0)
{
stPrefs.hostname[l]='.';
}
#ifdef RTSP_DEBUG
printf("-----------------------------------\n");
printf("\thostname is: %s\n", stPrefs.hostname);
printf("\trtsp listening port is: %d\n", stPrefs.port);
printf("\tInput rtsp://hostIP:%d/test.264 to play this\n",stPrefs.port);
printf("\n");
#endif
}
接下來,s32MainFd = tcp_listen(SERVER_RTSP_PORT_DEFAULT);//以非阻塞方式偵聽554端口,554 是rtsp的默認端口號;具體網絡編程這一塊的知識參考傳送門。
int tcp_listen(unsigned short port)
{
int f;
int on=1;
struct sockaddr_in s;
int v = 1;
/*創建套接字*/
if((f = socket(AF_INET, SOCK_STREAM, 0))<0) //AF_INET ipv4 SOCK_STREAM 流模式,tcp
{
fprintf(stderr, "socket() error in tcp_listen.\n");
return -1;
}
/*設置socket的可選參數*/
setsockopt(f, SOL_SOCKET, SO_REUSEADDR, (char *) &v, sizeof(int));
s.sin_family = AF_INET;
s.sin_addr.s_addr = htonl(INADDR_ANY);
s.sin_port = htons(port);
/*綁定socket*/
if(bind(f, (struct sockaddr *)&s, sizeof(s)))
{
fprintf(stderr, "bind() error in tcp_listen");
return -1;
}
//設置爲非阻塞方式
if(ioctl(f, FIONBIO, &on) < 0)
{
fprintf(stderr, "ioctl() error in tcp_listen.\n");
return -1;
}
/*監聽*/
if(listen(f, SOMAXCONN) < 0)
{
fprintf(stderr, "listen() error in tcp_listen.\n");
return -1;
}
return f;
}
接下來是便是調用ScheduleInit()函數,該函數對線程的結構體進行初始化,創建處理主線程schedule_do,schedule_do裏調用sched[i].play_action把數據發送出去。ScheduleInit()函數是本文章分析的重點,下面來詳細分析該函數。
int ScheduleInit()
{
int i;
pthread_t thread=0;
/*初始化數據*/
for(i=0; i<MAX_CONNECTION; ++i) //MAX_CONNECTION=10,最大允許連接10個客戶端
{
sched[i].rtp_session=NULL;
sched[i].play_action=NULL;
sched[i].valid=0;
sched[i].BeginFrame=0;
}
/*創建處理主線程*/
pthread_create(&thread,NULL,schedule_do,NULL); //創建schedule_do線程
return 0;
}
schedule_do線程的源碼如下:
void *schedule_do(void *arg)
{
int i=0;
struct timeval now;
unsigned long long mnow;
char *pDataBuf, *pFindNal;
unsigned int ringbuffer;
struct timespec ts = {0,33333}; //33333ns=33.333us
int s32FileId;
unsigned int u32NaluToken;
char *pNalStart=NULL;
int s32NalSize;
int s32FindNal = 0;
int buflen=0,ringbuflen=0,ringbuftype;
struct ringbuf ringinfo;
#ifdef RTSP_DEBUG
printf("rtsputils.c %d :The pthread %s start\n",__LINE__ , __FUNCTION__);
#endif
do
{
nanosleep(&ts, NULL); //sleep()-------以秒爲單位,usleep()----以微秒爲單位,nanosleep( )---------以納秒爲單位
s32FindNal = 0; //客戶端連接標識清零
//如果有客戶端連接,則g_s32DoPlay大於零
//if(g_s32DoPlay>0)
{
ringbuflen = ringget(&ringinfo);
if(ringbuflen ==0)
continue ;
}
printf("ringbuflen=%d\r\n",ringbuflen);
s32FindNal = 1;//有客戶端連接,置位
for(i=0; i<MAX_CONNECTION; ++i) //10
{
if(sched[i].valid) //如果有某個客戶端連接了,那麼sched[i].valid會在schedule_add()函數中被置爲1
{
if(!sched[i].rtp_session->pause)
//若sched[i].rtp_session->pause爲0 ,即表示沒有被暫停,是在播放中,即進行下面的數據處理和發送
{
//計算時間戳
gettimeofday(&now,NULL);
mnow = (now.tv_sec*1000 + now.tv_usec/1000);//毫秒
if((sched[i].rtp_session->hndRtp)&&(s32FindNal))
//若sched[i].rtp_session->hndRtp不是空 且s32FindNal有客戶端連接
{
buflen=ringbuflen;
//調用play_action函數把數據發送出去
sched[i].play_action((unsigned int)(sched[i].rtp_session->hndRtp), ringinfo.buffer, ringinfo.size, mnow);
}
}
}
}
}
while(!stop_schedule); //如果stop_schedule被置位,則跳出do循環
cleanup:
//free(pDataBuf);
//close(s32FileId);
#ifdef RTSP_DEBUG
printf("The pthread %s end\n", __FUNCTION__);
#endif
return ERR_NOERROR;
}
sched[i].play_action是執行發送數據的函數,該函數在schedule_add函數中被配置爲RtpSend。
//把RTP會話添加進schedule中,錯誤返回-1,正常返回schedule隊列號
int schedule_add(RTP_session *rtp_session)
{
int i;
for(i=0; i<MAX_CONNECTION; ++i)
{
/*需是還沒有被加入到調度隊列中的會話*/
if(!sched[i].valid)
{
sched[i].valid=1;
sched[i].rtp_session=rtp_session;
//設置播放動作
sched[i].play_action=RtpSend;
printf("rtsputils.c **adding a schedule object action %s,%d**\n", __FILE__, __LINE__);
return i;
}
}
return ERR_GENERIC;
}
下面來分析RtpSend函數。
unsigned int RtpSend(unsigned int u32Rtp, char *pData, int s32DataSize, unsigned int u32TimeStamp)
{
int s32NalSize = 0;
char *pNalBuf, *pDataEnd;
HndRtp hRtp = (HndRtp)u32Rtp;
unsigned int u32NaluToken;
hRtp->u32TimeStampCurr = u32TimeStamp;
printf("rtputils.c %d: hRtp->emPayload=%x\r\n",__LINE__,hRtp->emPayload);
if(_h264 == hRtp->emPayload) //實際打印信息,hRtp->emPayload=101,不進入這個分支
{
pDataEnd = pData + s32DataSize;
//搜尋第一個nalu起始標誌0x01000000
for(; pData < pDataEnd-5; pData ++)
{
memcpy(&u32NaluToken, pData, 4 * sizeof(char));
if(0x01000000 == u32NaluToken)
{
//標記nalu起始位置
pData += 4;
pNalBuf = pData;
break;
}
}
//發送nalu
for(; pData < pDataEnd-5; pData ++)
{
//搜尋第二個nalu起始標誌0x01000000,找到nalu起始位置,發送該nalu數據
memcpy(&u32NaluToken, pData, 4 * sizeof(char));
if(0x01000000 == u32NaluToken)
{
s32NalSize = (int)(pData - pNalBuf);//二者一相減就是第一個nalu的內容
/*
if (first_in == 1)
{
tmp_file=fopen("tmpfile.mp4","wb");
first_in = 0;
}
fwrite(pNalBuf,s32NalSize,1,tmp_file);
fflush(tmp_file);
if(SendNalu264(hRtp, pNalBuf, s32NalSize) == -1)
{
return -1;
}*/
//標記nalu起始位置
pData += 4;
pNalBuf = pData;
}
}//while
if(pData > pNalBuf)//最後一個nalu
{
s32NalSize = (int)(pData - pNalBuf);
if(SendNalu264(hRtp, pNalBuf, s32NalSize) == -1)
{
return -1;
}
}
}
else if(_h264nalu == hRtp->emPayload) ////實際打印信息,hRtp->emPayload=101,進入這個分支中
{
if(SendNalu264(hRtp, pData, s32DataSize) == -1)//直接發送NALU單元,不需要分離NALU
{
//在rtsp的setup階段時創建RTP套接字時設置了負荷類型爲_h264nalu,所以程序執行這個分支
//原因3516編碼已經幫我們分開了每一個H264的slice,即直接是一個NALU,所以直接添加東西組成RTP即可
{
return -1;
}
}
}
else if(_g711 == hRtp->emPayload)
{
if(SendNalu711(hRtp, pData, s32DataSize) == -1)
{
return -1;
}
}
else
{
return -1;
}
return 0;
}
測試的打印信息如下:
SendNalu264函數則是把原始的h264編碼數據封包爲RTP數據包,並以udp協議發送出去。詳細分析見傳送門。
至此,ScheduleInit()函數的主線分析完成,後續將進行其他部分的分析。