下面随笔将对Hi3559AV100外接UVC/MJPEG相机实现实时采图设计的关键点-VDEC_Send_Stream线程进行分析,一两个星期前我写了有三篇系列随笔,已经实现了项目功能,大家可以参考下面随笔:
Hi3559AV100外接UVC/MJPEG相机实时采图设计(一):Linux USB摄像头驱动分析:
https://www.cnblogs.com/iFrank/p/14399421.html
Hi3559AV100外接UVC/MJPEG相机实时采图设计(二):V4L2接口的实现(以YUV422为例) :
https://www.cnblogs.com/iFrank/p/14403397.html
Hi3559AV100外接UVC/MJPEG相机实时采图设计(三):V4L2接口通过MPP平台输出 :
https://www.cnblogs.com/iFrank/p/14403620.html
虽然已经实现了实时采图的功能,但是我还是对VDEC_Send_Stream线程进行细节分析,为后面Hi3559AV100外接UVC/MJPEG相机实现目标检测做准备,同时也让大家能够更好的了解实现的机制与原理,方便大家进行移植。
1、整体代码实现
首先给出涉及的数据与函数,方便后面分析:
1 /************************************************ 2 step8: send stream to VDEC 3 *************************************************/ 4 for(i=0; i<u32VdecChnNum; i++) 5 { 6 snprintf(stVdecSend[i].cFileName, sizeof(stVdecSend[i].cFileName), "test_jpeg.jpeg"); 7 snprintf(stVdecSend[i].cFilePath, sizeof(stVdecSend[i].cFilePath), "%s", "/nfsroot"); 8 stVdecSend[i].enType = astSampleVdec[i].enType; 9 stVdecSend[i].s32StreamMode = astSampleVdec[i].enMode; 10 stVdecSend[i].s32ChnId = i; 11 stVdecSend[i].s32IntervalTime = 1000; 12 stVdecSend[i].u64PtsInit = 0; 13 stVdecSend[i].u64PtsIncrease = 0; 14 stVdecSend[i].eThreadCtrl = THREAD_CTRL_START; 15 stVdecSend[i].bCircleSend = HI_TRUE; 16 stVdecSend[i].s32MilliSec = 0; 17 stVdecSend[i].s32MinBufSize = (astSampleVdec[i].u32Width * astSampleVdec[i].u32Height * 3)>>1; 18 } 19 SAMPLE_COMM_VDEC_StartSendStream(u32VdecChnNum, &stVdecSend[0], &VdecThread[0]); 20 21 SAMPLE_COMM_VDEC_CmdCtrl(u32VdecChnNum, &stVdecSend[0], &VdecThread[0]); 22 23 SAMPLE_COMM_VDEC_StopSendStream(u32VdecChnNum, &stVdecSend[0], &VdecThread[0]);
程序中u32VdecChnNum表示你所开的VDEC通道,这个根据项目的实际需求来定,随后给出MJPEG输入的文件名cFileName和路径名cFilePath,随后的参数设置根据文档说明进行修改,也可直接默认使用。在完成参数设置后,进入关键的三个函数,分别为:
1 SAMPLE_COMM_VDEC_StartSendStream(u32VdecChnNum, &stVdecSend[0], &VdecThread[0]); 2 3 SAMPLE_COMM_VDEC_CmdCtrl(u32VdecChnNum, &stVdecSend[0], &VdecThread[0]); 4 5 SAMPLE_COMM_VDEC_StopSendStream(u32VdecChnNum, &stVdecSend[0], &VdecThread[0]);
三者传入的参数一致,处理对象相同,现在我将一一分析各个函数的实现功能:
1.1、SAMPLE_COMM_VDEC_StartSendStream实现分析
对于SAMPLE_COMM_VDEC_StartSendStream(),其最关键的是pthread_create:
1 pthread_create(&pVdecThread[i], //对应于VdecThread[i] 2 0, 3 SAMPLE_COMM_VDEC_SendStream, 4 (HI_VOID *)&pstVdecSend[i]); //对应于stVdecSend[0]
参考man pthread_create,pthread_create()函数在调用中启动一个新线程处理,给出pthread_create的特征:
1 #include <pthread.h> 2 3 int pthread_create(pthread_t *thread, 4 const pthread_attr_t *attr, 5 void *(*start_routine) (void *), 6 void *arg);
新的线程通过调用start_routine()来开始执行,在此处,start_routine()为SAMPLE_COMM_VDEC_SendStream,arg<------->pstVdecSend[i]作为start_routine()的唯一参数传递;而thread这个标识符是用来引用在后续调用其他pthread功能,成功创建线程后,返回0。
线程分两类,一类是joinable,一类是detached,对于joinable线程,需要用pthread_join()来等待线程结束并获取状态;而对于detached线程终止,其所用的资源系统会自动回收的,不需要进行操作,对于线程创建来说,默认是joinable线程,如果大家需要detached线程,则需要修改attr参数。
当start_routine()设置为SAMPLE_COMM_VDEC_SendStream,函数的关键又转移到了start_routine下,所以分析重点又转了SAMPLE_COMM_VDEC_SendSteam,此函数下,首先是设置了线程名字,fopen打开MJPEG文件,并开辟了内存空间,为后面接收数据做准备,并清空了缓存区:
1 //设置线程名字 2 prctl(PR_SET_NAME, "VideoSendStream", 0,0,0); 3 4 //cStreamFile = "/nfsroot/test_jpeg.jpeg" 5 snprintf(cStreamFile, sizeof(cStreamFile), "%s/%s", pstVdecThreadParam->cFilePath,pstVdecThreadParam->cFileName); 6 7 pu8Buf = malloc(pstVdecThreadParam->s32MinBufSize); 8 9 /* 10 清空标准输出缓冲区, 11 刷新输出缓冲区,即将缓冲区的东西输出到屏幕上 12 */ 13 fflush(stdout);
在完成准备工作后,进入了while循环,while循环主要完成下面几件事:
(1)、用pstVdecThreadParam->eThreadCtrl来实现while循环继续与否:
1 //另外线程控制 2 if (pstVdecThreadParam->eThreadCtrl == THREAD_CTRL_STOP) 3 { 4 break; 5 } 6 else if (pstVdecThreadParam->eThreadCtrl == THREAD_CTRL_PAUSE) 7 { 8 sleep(1); 9 continue; 10 }
(2)、第二就是把MJPEG文件导入,读取长度等等,并进行MJPEG数据帧头帧尾判断, 接收数据。
因为JPEG 文件的格式是分为一个一个的段来存储的,段的多少和长度并不是一定的。只要包含了足够的信息,该JPEG文件就能够被打开,呈现给人们。JPEG文件的每个段都一定包含两部分一个是段的标识,它由两个字节构成:第一个字节是十六进制0xFF,第二个字节对于不同的段,这个值是不同的。紧接着的两个字节存放的是这个段的长度(除了前面的两个字节0xFF和0xXX,X表示不确定。他们是不算到段的长度中的)。
段的一般结构:
1 ----------------------------------------------------------------- 2 名称 字节数 数据 说明 3 ----------------------------------------------------------------- 4 段标识 1 FF 每个新段的开始标识 5 段类型 1 类型编码(称作“标记码”) 6 段长度 2 包括段内容和段长度本身,不包括段标识和段类型 7 段内容 ≤65533字节
段类型:
1 --------------------------------------- 2 名称 标记码 说明 3 --------------------------------------- 4 SOI D8 文件头 5 EOI D9 文件尾 6 SOF0 C0 帧开始(标准 JPEG) 7 SOF1 C1 同上 8 DHT C4 定义 Huffman 表(霍夫曼表) 9 SOS DA 扫描行开始 10 DQT DB 定义量化表 11 DRI DD 定义重新开始间隔 12 APP0 E0 定义交换格式和图像识别信息 13 COM FE 注释 14 -----------------------------------------------------------
(3)、VDEC通道数据的发送,通过使用HI_MPI_VDEC_SendStream向视频解码通道发送码流数据,具体实现如下:
1.2、SAMPLE_COMM_VDEC_CmdCtrl实现分析
因为前面设置了VDEC_THREAD_PARAM_S参数类型pstVdecSend[i[.bCircleSend == HI_TRUE,函数直接利用goto语句跳转到WHILE标签,在WHILE标签下,首先就会终端打印
1 printf("\nSAMPLE_TEST:press 'e' to exit; 'p' to pause; 'r' to resume; 'q' to query!\n");
通过在终端输入字符'e'、'p'、'r'、'q'执行对应的操作,具体功能如上所示,实现如下:
1 if (c == 'e') 2 break; 3 else if (c == 'r') 4 { 5 if (bVoPause == HI_TRUE) 6 { 7 for (i=0; i<s32ChnNum; i++) 8 { 9 pstVdecSend[i].eThreadCtrl = THREAD_CTRL_START; 10 } 11 12 for (VoLayer=0; VoLayer<VO_MAX_LAYER_NUM; VoLayer++) 13 { 14 for (VoChn=0; VoChn<VO_MAX_CHN_NUM; VoChn++) 15 { 16 s32Ret = HI_MPI_VO_ResumeChn(VoLayer, VoChn); 17 if(HI_SUCCESS != s32Ret) 18 { 19 printf("HI_MPI_VO_ResumeChn(%d, %d) fail for 0x%x!\n", VoLayer, VoChn, s32Ret); 20 } 21 } 22 } 23 printf("VO Resume!!!"); 24 } 25 bVoPause = HI_FALSE; 26 } 27 else if (c == 'p') 28 { 29 if(bVoPause == HI_FALSE) 30 { 31 for (i=0; i<s32ChnNum; i++) 32 { 33 pstVdecSend[i].eThreadCtrl = THREAD_CTRL_PAUSE; 34 } 35 36 for (VoLayer=0; VoLayer<VO_MAX_LAYER_NUM; VoLayer++) 37 { 38 for (VoChn=0; VoChn<VO_MAX_CHN_NUM; VoChn++) 39 { 40 s32Ret = HI_MPI_VO_PauseChn(VoLayer, VoChn); 41 if(HI_SUCCESS != s32Ret) 42 { 43 printf("HI_MPI_VO_PauseChn(%d, %d) fail for 0x%x!\n", VoLayer, VoChn, s32Ret); 44 } 45 } 46 } 47 printf("VO Pause..."); 48 } 49 bVoPause = HI_TRUE; 50 } 51 else if (c == 'q') 52 { 53 for (i=0; i<s32ChnNum; i++) 54 { 55 HI_MPI_VDEC_QueryStatus(pstVdecSend[i].s32ChnId, &stStatus); 56 PRINTF_VDEC_CHN_STATUS(pstVdecSend[i].s32ChnId, stStatus); 57 } 58 }
SAMPLE_COMM_VDEC_CmdCtrl函数正常情况下只可以通过输入字符'c'退出,而字符'r'、'p'、'q'的具体实现这里就不介绍了,总的来说就是循环往VDEC通道发送数据。
1.3、SAMPLE_COMM_VDEC_StopSendStream实现分析
SAMPLE_COMM_VDEC_StopSendStream实现功能比较简单,主要完成:
(1)、解码器停止接收用户发送的码流-HI_MPI_VDEC_StopRecvStream(i);
(2)、发出THREAD_CTRL_STOP指令,退出SAMPLE_COMM_VDEC_SendStream函数(即break),随后用pthread_join()来等待线程结束并获取状态,回收系统资源;
具体实现如下所示:
1 for(i=0; i<s32ChnNum; i++) 2 { 3 pstVdecSend[i].eThreadCtrl = THREAD_CTRL_STOP; 4 HI_MPI_VDEC_StopRecvStream(i); 5 if(0 != pVdecThread[i]) 6 { 7 pthread_join(pVdecThread[i], HI_NULL); 8 pVdecThread[i] = 0; 9 } 10 }
2、总结
通过对VDEC_Send_Stream线程进行分析,比较清楚的了解了其内在的实现机理,而在之后的项目,将实现MJPEG->VDEC->VPSS->NNIE->VGS->VO数据通路,将利用其线程机制进行移植。