hi3516a——視頻壓縮並通過RTSP播放H.264視頻流

前言

由於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()函數的主線分析完成,後續將進行其他部分的分析。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章