前言
想搭建功能完备的万能平台,rtsp的支持自然必不可少。不论是编码的h264和我们用来实时传输的rtsp,都是可以单拎出来作为研究方向的,所幸在功能支持上而言,暂时全部当做黑盒子调用,还是比较容易实现的。看到过这样一句话,知识是学不完的,选取我们有需要的即可。
RTSP
实时流传输协议(Real Time Streaming Protocol,RTSP),RFC2326(中文版),是TCP/IP协议体系中的一个应用层协议,由哥伦比亚大学、网景和RealNetworks公司提交的IETF RFC标准。该协议定义了一对多应用程序如何有效地通过IP网络传送多媒体数据。
RTSP是 TCP/IP 协议体系中的一个应用层协议,该协议定义了一对多应用程序如何有效地通过 IP 网络传送多媒体数据。RTSP在体系结构上位于RTP和RTCP之上,它使用TCP或UDP完成数据传输。HTTP与RTSP相比,HTTP传送HTML,而RTSP传送的是多媒体数据。
RTSP是基于文本的协议,采用ISO10646字符集,使用UTF-8编码方案。行以CRLF中断,包括消息类型、消息头、消息体和消息长。但接收者本身可将CR和LF解释成行终止符。基于文本的协议使其以自描述方式增加可选参数更容易,接口中采用SDP作为描述语言。
RTSP是应用级协议,控制实时数据的发送。RTSP提供了一个可扩展框架,使实时数据,如音频与视频的受控点播成为可能。数据源包括现场数据与存储在剪辑中数据。该协议目的在于控制多个数据发送连接,为选择发送通道,如UDP、组播UDP与TCP,提供途径,并为选择基于RTP上发送机制提供方法。
RTSP建立并控制一个或几个时间同步的连续流媒体。尽管连续媒体流与控制流交换是可能的,通常它本身并不发送连续流。换言之,RTSP充当多媒体服务器的网络远程控制。RTSP连接没有绑定到传输层连接,如TCP。在RTSP连接期间,RTSP用户可打开或关闭多个对服务器的可传输连接以发出RTSP请求。此外,可使用无连接传输协议,如UDP。RTSP流控制的流可能用到RTP,但RTSP操作并不依赖用于携带连续媒体的传输机制。
简单一扫百度百科,瞬间云里雾里,不考虑技术实现方式,大白话说,就是实现了将编码完成后的音视频文件进行了实时的播放而已,我们的sample已经自带了保存编码完成后的音视频文件功能,只需要调用rtsp库,完成功能的支持其实就可以了
移植过程
之前venc 的sample分析里已经讲到SAMPLE_COMM_VENC_StartGetStream是用来保存文件的函数,不幸的是,这个函数是在common目录下的,Makefile在编译时会将这一目录下的所有文件加进来,为了不必要的麻烦,除了添加注释之外,一般不要擅自更改这些地方的函数
另外,出于个人习惯,将原sample的中所有可能修改的都重新参考sample写一遍,一是可以加深印象,而来修改起来非常方便,不用估计会给其他的sample文件带来影响
编码文件保存
pthread_t gs_RtspVencPid; static SAMPLE_VENC_GETSTREAM_PARA_S gs_stPara; /* *描述 :调用RTSP静态库后,用于实时显示编码后图像 *参数 :VeChn[] 编码通道号 s32Cnt 通道数 *返回值:创建线程PLATFORM_VENC_GetVencStreamRtsp,传递结构体gs_stPara *注意 :无 */ HI_S32 PLATFORM_VENC_StartGetStreamRtsp(VENC_CHN VeChn[],HI_S32 s32Cnt) { HI_U32 i; gs_stPara.bThreadStart = HI_TRUE; gs_stPara.s32Cnt = s32Cnt; for(i=0; i<s32Cnt; i++) { gs_stPara.VeChn[i] = VeChn[i]; } return pthread_create(&gs_RtspVencPid, 0, PLATFORM_VENC_GetVencStreamRtsp, (HI_VOID*)&gs_stPara); } /****************************************************************************** *描述 :结束RTSP实时播放线程 *参数 :无 *返回值:成功返回0 *注意 :无 ******************************************************************************/ HI_S32 PLATFORM_VENC_StopGetStreamRtsp(void) { if (HI_TRUE == gs_stPara.bThreadStart) { gs_stPara.bThreadStart = HI_FALSE; pthread_join(gs_RtspVencPid, 0); } return HI_SUCCESS; } /****************************************************************************** *描述 :保存文件 *参数 :pFd 文件描述符 pstStream 帧码流类型结构体。 *返回值:成功返回0 *注意 :无 ******************************************************************************/ HI_S32 PLATFORM_VENC_SaveStream(FILE* pFd, VENC_STREAM_S* pstStream) { HI_S32 i; for (i = 0; i < pstStream->u32PackCount; i++) { fwrite(pstStream->pstPack[i].pu8Addr + pstStream->pstPack[i].u32Offset, pstStream->pstPack[i].u32Len - pstStream->pstPack[i].u32Offset, 1, pFd); fflush(pFd); } return HI_SUCCESS; } /****************************************************************************** *描述 :根据enPayload获取文件后缀 *参数 :enPayload 音视频净荷类型枚举 szFilePostfix 文件前缀 *返回值:成功返回0,失败返回-1 ******************************************************************************/ HI_S32 PLATFORM_VENC_GetFilePostfix(PAYLOAD_TYPE_E enPayload, char* szFilePostfix) { if (PT_H264 == enPayload) { strcpy(szFilePostfix, ".h264"); } else if (PT_H265 == enPayload) { strcpy(szFilePostfix, ".h265"); } else if (PT_JPEG == enPayload) { strcpy(szFilePostfix, ".jpg"); } else if (PT_MJPEG == enPayload) { strcpy(szFilePostfix, ".mjp"); } else if (PT_PRORES == enPayload) { strcpy(szFilePostfix, ".prores"); } else { SAMPLE_PRT("payload type err!\n"); return HI_FAILURE; } return HI_SUCCESS; } /* *描述 :调用RTSP静态库后,用于实时显示编码后图像 *参数 :p 线程传递进来的结构体指针原型为 SAMPLE_VENC_GETSTREAM_PARA_S *返回值:NULL *注意 :无 */ HI_VOID* PLATFORM_VENC_GetVencStreamRtsp(HI_VOID* p) { HI_S32 i; HI_S32 s32ChnTotal; VENC_CHN_ATTR_S stVencChnAttr; SAMPLE_VENC_GETSTREAM_PARA_S* pstPara; HI_S32 maxfd = 0; struct timeval TimeoutVal; fd_set read_fds; HI_U32 u32PictureCnt[VENC_MAX_CHN_NUM]={0}; HI_S32 VencFd[VENC_MAX_CHN_NUM]; HI_CHAR aszFileName[VENC_MAX_CHN_NUM][64]; FILE* pFile[VENC_MAX_CHN_NUM]; char szFilePostfix[10]; VENC_CHN_STATUS_S stStat; VENC_STREAM_S stStream; HI_S32 s32Ret; VENC_CHN VencChn; PAYLOAD_TYPE_E enPayLoadType[VENC_MAX_CHN_NUM]; VENC_STREAM_BUF_INFO_S stStreamBufInfo[VENC_MAX_CHN_NUM]; prctl(PR_SET_NAME, "GetVencStream", 0,0,0); pstPara = (SAMPLE_VENC_GETSTREAM_PARA_S*)p; s32ChnTotal = pstPara->s32Cnt; /****************************************** step 1: check & prepare save-file & venc-fd 检查并准备保存文件和venc fd ******************************************/ if (s32ChnTotal >= VENC_MAX_CHN_NUM) { SAMPLE_PRT("input count invaild\n"); return NULL; } for (i = 0; i < s32ChnTotal; i++) { /* decide the stream file name, and open file to save stream 确定视频流文件名,并打开文件以保存视频流*/ VencChn = pstPara->VeChn[i]; s32Ret = HI_MPI_VENC_GetChnAttr(VencChn, &stVencChnAttr); if (s32Ret != HI_SUCCESS) { SAMPLE_PRT("HI_MPI_VENC_GetChnAttr chn[%d] failed with %#x!\n", \ VencChn, s32Ret); return NULL; } enPayLoadType[i] = stVencChnAttr.stVencAttr.enType; s32Ret = PLATFORM_VENC_GetFilePostfix(enPayLoadType[i], szFilePostfix); if (s32Ret != HI_SUCCESS) { SAMPLE_PRT("PLATFORM_VENC_GetFilePostfix [%d] failed with %#x!\n", \ stVencChnAttr.stVencAttr.enType, s32Ret); return NULL; } if(PT_JPEG != enPayLoadType[i]) { snprintf(aszFileName[i],32, "./RTSP/RTSP_chn%d%s", i, szFilePostfix); pFile[i] = fopen(aszFileName[i], "wb"); if (!pFile[i]) { SAMPLE_PRT("open file[%s] failed!\n", aszFileName[i]); return NULL; } } /* Set Venc Fd. */ VencFd[i] = HI_MPI_VENC_GetFd(i); if (VencFd[i] < 0) { SAMPLE_PRT("HI_MPI_VENC_GetFd failed with %#x!\n", VencFd[i]); return NULL; } if (maxfd <= VencFd[i]) { maxfd = VencFd[i]; } s32Ret = HI_MPI_VENC_GetStreamBufInfo (i, &stStreamBufInfo[i]); if (HI_SUCCESS != s32Ret) { SAMPLE_PRT("HI_MPI_VENC_GetStreamBufInfo failed with %#x!\n", s32Ret); return (void *)HI_FAILURE; } } /****************************************** step 2: Start to get streams of each channel. 开始获取每个通道的视频流 ******************************************/ while (HI_TRUE == pstPara->bThreadStart) { FD_ZERO(&read_fds); for (i = 0; i < s32ChnTotal; i++) { FD_SET(VencFd[i], &read_fds); } TimeoutVal.tv_sec = 2; TimeoutVal.tv_usec = 0; s32Ret = select(maxfd + 1, &read_fds, NULL, NULL, &TimeoutVal); if (s32Ret < 0) { SAMPLE_PRT("select failed!\n"); break; } else if (s32Ret == 0) { SAMPLE_PRT("get venc stream time out, exit thread\n"); continue; } else { for (i = 0; i < s32ChnTotal; i++) { if (FD_ISSET(VencFd[i], &read_fds)) { /******************************************************* step 2.1 : query how many packs in one-frame stream. 查询每个帧流中有多少包。 *******************************************************/ memset(&stStream, 0, sizeof(stStream)); s32Ret = HI_MPI_VENC_QueryStatus(i, &stStat); if (HI_SUCCESS != s32Ret) { SAMPLE_PRT("HI_MPI_VENC_QueryStatus chn[%d] failed with %#x!\n", i, s32Ret); break; } /******************************************************* step 2.2 :suggest to check both u32CurPacks and u32LeftStreamFrames at the same time,for example: 建议同时检查u32CurPacks和u32LeftStreamFrames if(0 == stStat.u32CurPacks || 0 == stStat.u32LeftStreamFrames) { SAMPLE_PRT("NOTE: Current frame is NULL!\n"); continue; } *******************************************************/ if(0 == stStat.u32CurPacks) { SAMPLE_PRT("NOTE: Current frame is NULL!\n"); continue; } /******************************************************* step 2.3 : malloc corresponding number of pack nodes. malloc对应的包节点数。 *******************************************************/ stStream.pstPack = (VENC_PACK_S*)malloc(sizeof(VENC_PACK_S) * stStat.u32CurPacks); if (NULL == stStream.pstPack) { SAMPLE_PRT("malloc stream pack failed!\n"); break; } /******************************************************* step 2.4 : call mpi to get one-frame stream 调用mpi获取一个帧流 *******************************************************/ stStream.u32PackCount = stStat.u32CurPacks; s32Ret = HI_MPI_VENC_GetStream(i, &stStream, HI_TRUE); if (HI_SUCCESS != s32Ret) { free(stStream.pstPack); stStream.pstPack = NULL; SAMPLE_PRT("HI_MPI_VENC_GetStream failed with %#x!\n", \ s32Ret); break; } /******************************************************* step 2.5 : save frame to file 将框架保存到文件 *******************************************************/ if(PT_JPEG == enPayLoadType[i]) { snprintf(aszFileName[i],32, "stream_chn%d_%d%s", i, u32PictureCnt[i],szFilePostfix); pFile[i] = fopen(aszFileName[i], "wb"); if (!pFile[i]) { SAMPLE_PRT("open file err!\n"); return NULL; } } #ifndef __HuaweiLite__ s32Ret = PLATFORM_VENC_SaveStream(pFile[i], &stStream); #else s32Ret = SAMPLE_COMM_VENC_SaveStream_PhyAddr(pFile[i], &stStreamBufInfo[i], &stStream); #endif if (HI_SUCCESS != s32Ret) { free(stStream.pstPack); stStream.pstPack = NULL; SAMPLE_PRT("save stream failed!\n"); break; } /******************************************************* step 2.6 : release stream 释放视频流 *******************************************************/ s32Ret = HI_MPI_VENC_ReleaseStream(i, &stStream); if (HI_SUCCESS != s32Ret) { SAMPLE_PRT("HI_MPI_VENC_ReleaseStream failed!\n"); free(stStream.pstPack); stStream.pstPack = NULL; break; } /******************************************************* step 2.7 : free pack nodes 释放包节点 *******************************************************/ free(stStream.pstPack); stStream.pstPack = NULL; u32PictureCnt[i]++; if(PT_JPEG == enPayLoadType[i]) { fclose(pFile[i]); } } } } } /******************************************************* * step 3 : close save-file *******************************************************/ for (i = 0; i < s32ChnTotal; i++) { if(PT_JPEG != enPayLoadType[i]) { fclose(pFile[i]); } } return NULL; }
MakeFile修改
rtsp库我们更改交叉编译环境(修改rtsp库的Makefile),重新编译生成静态库后,修改海思的Makefile,添加:
RTSP_DIR ?= $(PWD)/../../rtsp_lib INC_FLAGS += -I$(RTSP_DIR) SENSOR_LIBS += $(REL_LIB)/librtsp.a
路径完全看自己放的位置所决定,静态库暂时放在了默认位置,后续可以改动
RTSP移植
参考demo定义加载文件结构体,后续会升级!
#define MAX_SESSION_NUM 8 //rtsp最大接口数 #define DEMO_CFG_FILE "platform.ini" /* 配置文件结构体 */ typedef struct demo_cfg_para { int session_count; struct { char path[64]; char video_file[64]; char audio_file[64]; } session_cfg[MAX_SESSION_NUM]; }demo_cfg; 1 2 3 4 5 6 7 8 9 10 11 12 13 static int flag_run = 1; static void sig_proc(int signo) { flag_run = 0; } static int get_next_video_frame (FILE *fp, uint8_t **buff, int *size) { uint8_t szbuf[1024]; int szlen = 0; int ret; if (!(*buff)) { *buff = (uint8_t*)malloc(2*1024*1024); if (!(*buff)) return -1; } *size = 0; while ((ret = fread(szbuf + szlen, 1, sizeof(szbuf) - szlen, fp)) > 0) { int i = 3; szlen += ret; while (i < szlen - 3 && !(szbuf[i] == 0 && szbuf[i+1] == 0 && (szbuf[i+2] == 1 || (szbuf[i+2] == 0 && szbuf[i+3] == 1)))) i++; memcpy(*buff + *size, szbuf, i); *size += i; memmove(szbuf, szbuf + i, szlen - i); szlen -= i; if (szlen > 3) { //printf("szlen %d\n", szlen); fseek(fp, -szlen, SEEK_CUR); break; } } if (ret > 0) return *size; return 0; } static int get_next_audio_frame (FILE *fp, uint8_t **buff, int *size) { int ret; #define AUDIO_FRAME_SIZE 320 if (!(*buff)) { *buff = (uint8_t*)malloc(AUDIO_FRAME_SIZE); if (!(*buff)) return -1; } ret = fread(*buff, 1, AUDIO_FRAME_SIZE, fp); if (ret > 0) { *size = ret; return ret; } return 0; } int load_cfg(demo_cfg *cfg, const char *cfg_file) { //cfgline: path=%s video=%s audio=%s FILE *fp = fopen(cfg_file, "r"); char line[256]; int count = 0; if (!fp) { fprintf(stderr, "open %s failed\n", cfg_file); return -1; } memset(cfg, 0, sizeof(*cfg)); while (fgets(line, sizeof(line) - 1, fp)) { const char *p; memset(&cfg->session_cfg[count], 0, sizeof(cfg->session_cfg[count])); if (line[0] == '#') continue; p = strstr(line, "path="); if (!p) continue; if (sscanf(p, "path=%s", cfg->session_cfg[count].path) != 1) continue; if ((p = strstr(line, "video="))) { if (sscanf(p, "video=%s", cfg->session_cfg[count].video_file) != 1) { fprintf(stderr, "parse video file failed %s\n", p); } } if ((p = strstr(line, "audio="))) { if (sscanf(p, "audio=%s", cfg->session_cfg[count].audio_file) != 1) { fprintf(stderr, "parse audio file failed %s\n", p); } } if (strlen(cfg->session_cfg[count].video_file) || strlen(cfg->session_cfg[count].audio_file)) { count ++; } else { fprintf(stderr, "parse line %s failed\n", line); } } cfg->session_count = count; /* path=/live/chn0 video=BarbieGirl.h264 audio=BarbieGirl.alaw path=/live/chn1 video=BarbieGirl.h264 path=/live/chn2 audio=BarbieGirl.alaw */ printf("cfg->session_count:%d\n",cfg->session_count);//3 fclose(fp); return count; } /* *描述 :用于rtsp实时播放的线程 *参数 :NULL *返回值:无 *注意 :加载文件demo.cfg path=/mnt/sample/venc/RTSP video=RTSP_chn1.h264 */ void *video_play_rtsp_task(void*arg) { const char *cfg_file = DEMO_CFG_FILE; demo_cfg cfg; FILE *fp[MAX_SESSION_NUM][2] = {{NULL}}; rtsp_demo_handle demo; rtsp_session_handle session[MAX_SESSION_NUM] = {NULL}; int session_count = 0; uint8_t *vbuf = NULL; uint8_t *abuf = NULL; uint64_t ts = 0; int vsize = 0, asize = 0; int ret, ch; ret = load_cfg(&cfg, cfg_file); demo = rtsp_new_demo(8554);//rtsp sever socket if (NULL == demo) { SAMPLE_PRT("rtsp new demo failed!\n"); return 0; } session_count = 1; for (ch = 0; ch < session_count; ch++) { if (strlen(cfg.session_cfg[ch].video_file)) { fp[ch][0] = fopen(cfg.session_cfg[ch].video_file, "rb");//打开视频文件 if (!fp[ch][0]) { fprintf(stderr, "open %s failed\n", cfg.session_cfg[ch].video_file); } } //fp[ch][1] :音频文件的句柄 // if (strlen(cfg.session_cfg[ch].audio_file)) { // fp[ch][1] = fopen(cfg.session_cfg[ch].audio_file, "rb"); // if (!fp[ch][1]) { // fprintf(stderr, "open %s failed\n", cfg.session_cfg[ch].audio_file); // } // } if (fp[ch][0] == NULL && fp[ch][1] == NULL) continue; session[ch] = rtsp_new_session(demo, cfg.session_cfg[ch].path);//对应rtsp session if (NULL == session[ch]) { printf("rtsp_new_session failed\n"); continue; } if (fp[ch][0]) {//当前请求路径存视频数据源 rtsp_set_video(session[ch], RTSP_CODEC_ID_VIDEO_H264, NULL, 0); rtsp_sync_video_ts(session[ch], rtsp_get_reltime(), rtsp_get_ntptime()); } printf("==========> rtsp://192.168.119.200:8554%s for %s <===========\n", cfg.session_cfg[ch].path, fp[ch][0] ? cfg.session_cfg[ch].video_file : ""); } ts = rtsp_get_reltime(); signal(SIGINT, sig_proc); while (flag_run) { uint8_t type = 0; for (ch = 0; ch < session_count; ch++) {//3个源 if (fp[ch][0]) { read_video_again: ret = get_next_video_frame(fp[ch][0], &vbuf, &vsize); if (ret < 0) { fprintf(stderr, "get_next_video_frame failed\n"); flag_run = 0; break; } if (ret == 0) { fseek(fp[ch][0], 0, SEEK_SET); if (fp[ch][1]) fseek(fp[ch][1], 0, SEEK_SET); goto read_video_again; } if (session[ch])//1源session 存存 rtsp_tx_video(session[ch], vbuf, vsize, ts);//2rtsp_client_connect存在 type = 0; if (vbuf[0] == 0 && vbuf[1] == 0 && vbuf[2] == 1) { type = vbuf[3] & 0x1f; } if (vbuf[0] == 0 && vbuf[1] == 0 && vbuf[2] == 0 && vbuf[3] == 1) { type = vbuf[4] & 0x1f; } if (type != 5 && type != 1) goto read_video_again; } if (fp[ch][1]) { ret = get_next_audio_frame(fp[ch][1], &abuf, &asize); if (ret < 0) { fprintf(stderr, "get_next_audio_frame failed\n"); break; } if (ret == 0) { fseek(fp[ch][1], 0, SEEK_SET); if (fp[ch][0]) fseek(fp[ch][0], 0, SEEK_SET); continue; } if (session[ch]) rtsp_tx_audio(session[ch], abuf, asize, ts); } } do { ret = rtsp_do_event(demo);// if (ret > 0) continue; if (ret < 0) break; usleep(20000); } while (rtsp_get_reltime() - ts < 1000000 / 25); if (ret < 0) break; ts += 1000000 / 25; printf(".");fflush(stdout);//立即将printf的数据输出显示 } free(vbuf); free(abuf); for (ch = 0; ch < session_count; ch++) { if (fp[ch][0]) fclose(fp[ch][0]); if (fp[ch][1]) fclose(fp[ch][1]); if (session[ch]) rtsp_del_session(session[ch]); } rtsp_del_demo(demo); printf("Exit.\n"); getchar(); return 0; }
改进
这样的应用方式会依赖码流的本地保存,编码的速率影响保存速率,从保存文件的读取在实时播放,这样的流程还是很繁琐的,所以造成了大量的延时,直接从根源处,保存之前获取码流才是更为正确的选择,具体做法参考RTSP优化篇