关键词:H20码流、花屏、解码报错,移除SEI帧
这篇文章聊聊OSDK解码H20花屏严重的问题。基于camera_stream_callback_sample 尝试解码播放功能,本人PC虚拟机上的效果还是比较明显的,先放上我测试用的代码,因主要还是想记录一下问题的排查和解决思路,放上的代码也仅供参考,工程就不放出来了。主要提供两个主要的文件
camera-stream-callback-sample.cpp
dji_camera_stream_decoder.cpp
文件功能不熟的可以再参考前一篇代码跟读,阅读下官方的demo。
直接替换进工程可能编译不过,因为也是在动态调测中,也不记得具体修改过哪些地方,编译不过也可以根据编译报错对应修改一下,问题应该也不大。
开始发现花屏问题时,理解是解码性能问题基本是没有异议的,因为根据官方提供的参数,H20的码流看起来确实大了很多,另外直接使用官方的demo代码保存为H264文件再播放是没有问题的,也就验证了OSDK从飞机端取流没有问题,问题就在于获取到裸流后解码跟不上导致花屏。但是,再次过这部分代码时貌似忽略了一个问题,在理解保存为H264文件时是独立的代码和camera_stream_callback_sample有关系但不能完全等同。简单的回溯一下camera_stream_callback_sample取流代码,
代码实现函数名为:
decodeBuffer
2021-04-22 修正,上次贴的代码有误(帖成了M210读取USB数据部分,M300是下面这个),更新如下:
这个解码函数注册到了取流的底层触发(如下(handlerMap[pos].cb)):
E_OsdkStat LiveViewImpl::RecordStreamHandler(struct _CommandHandle *cmdHandle,
const T_CmdInfo *cmdInfo,
const uint8_t *cmdData,
void *userData) {
if ((!cmdInfo) || (!userData)) {
DERROR("Recv Info is a null value");
return OSDK_STAT_ERR;
}
std::map<LiveView::LiveViewCameraPosition, H264CallbackHandler> handlerMap =
*(std::map<LiveView::LiveViewCameraPosition, H264CallbackHandler> *)userData;
LiveView::LiveViewCameraPosition pos;
switch (cmdInfo->cmdId) {
case LIVEVIEW_FPV_CAM_TEMP_CMD_ID:
pos = LiveView::OSDK_CAMERA_POSITION_FPV;
break;
case LIVEVIEW_MAIN_CAM_TEMP_CMD_ID:
pos = LiveView::OSDK_CAMERA_POSITION_NO_1;
break;
case LIVEVIEW_VICE_CAM_TEMP_CMD_ID:
pos = LiveView::OSDK_CAMERA_POSITION_NO_2;
break;
case LIVEVIEW_TOP_CAM_TEMP_CMD_ID:
pos = LiveView::OSDK_CAMERA_POSITION_NO_3;
break;
default:
return OSDK_STAT_ERR_OUT_OF_RANGE;
}
if((handlerMap.find(pos) != handlerMap.end()) && (handlerMap[pos].cb != NULL)) {
handlerMap[pos].cb((uint8_t *)cmdData, cmdInfo->dataLen, handlerMap[pos].userData);
} else {
//DERROR("Can't find valid cb in handlerMap, pos = %d", pos);
}
return OSDK_STAT_OK;
}
这部分逻辑是将解码部分以回调的方式注入到取流回调中的,先假设存在解码性能问题,按照回调触发的原理,解码性能是可能会影响到底层的回调触发的。简单理解一下,要保证实时性,回调正常情况下一定是按帧频来触发回调,如果外部的解码延时过高,数据量大且有一定实时要求的数据处理就可能存在相互干扰,就可能会出现外部解码导致底层抛上来的H264裸流数据不全,很简单,为了验证这个问题直接在decodeBuffer中加一个写H264文件就可以了。代码文件中
还保留了这个注释:
//writeStreamData1("tmp1.h264", pBuf, remainingLen);
实测验证了上述想法是存在的,为了进一步验证,我把后面解码和播放的代码都注释了再看保存的H264文件就是正常的。所以很明显上层应用和底层部分存在干扰,导致接收到的裸流数据被破坏了。裸流文件就不放出来了,放个统计帧数的打印,可以看到回调吐出来的帧不太稳定,而且丢了的帧是没有补回来的。
既然发现了问题,就再想想办法看是不是可以解决或者优化一下这个问题。论坛帖还是想多分享一下思维过程,考虑到OSDK解码播放的实际使用用途,实操过程中应该不会直接连接OSDK机载端进行实时查看,所以这部分更多的就是一个demo过程,在调测代码时更多还是在官方demo中进行修改和调试。如果需要直接可用的版本或比较好的方案的,建议还是参考官方方案。
简单聊聊我的想法,既然回调中解码会有影响,那我们是否可以在回调中仅接收数据不处理,另外播放的时候来取数据解码,这样应用端和底层接收就是独立的。所以实时缓存似乎在这可以起到点作用,即取数据只写缓存,解码播放时读缓存。因为到这里不确定平台的解码性能是否会有影响,或者会产生多大的影响,毕竟不同的平台效果区别还是挺大的,所以提供的测试代码中使用了一个简单的循环队列(先进先出)来做缓存,有个好处就是可以通过队列缓存的状态来判定数据处理效果,简单的进出水原理。而且可以通过控制缓存大小,我就可以调测到底是不是解码性能的影响或者影响效果。
又考虑到数据帧已经是单独帧就没有必要再放一块大内存,在读取的时候再识别帧了。所以在网上找了下并参考大牛们的优秀设计,结合这个需求简单的使用了个数组做循环队列:
直接在类DJICameraStreamDecoder中添加如下部分:
typedef struct ST_QUEUE_NODE
{
uint8_t uQueueIndex;
uint32_t uFrameNum;
bool bIsKeyFrame;
uint8_t *pBuf;
uint32_t uFrameLen;
}ST_QUEUE_NODE;
ST_QUEUE_NODE *pQueueArr;
pthread_mutex_t QueueMutex;
int front;
int rear;
int maxSize;
decodeBuffer修改后如下。
void DJICameraStreamDecoder::decodeBuffer(uint8_t* buf, int bufLen)
{
//uint8_t* pData = buf;
int remainingLen = bufLen;
int processedLen = 0;
unsigned char pBuf[1024*1024] = {0};
uint8_t* pData = pBuf;
//writeStreamData1("tmp1.h264", pBuf, remainingLen);
long int curtimestamp = getTimeStamp();
if(curtimestamp - timestamp >= 1 * 1000)
{
printf("test: decodeBuffer FPS[%d], curtimestamp[%ld], last[%ld]\n", num, curtimestamp, timestamp);
timestamp = curtimestamp;
num = 0;
}
num++;
bool IsKeyFrame = false;
remainingLen = RemoveSEINal(pBuf, buf, bufLen, &IsKeyFrame);
//printf("test RemoveSEINal:IsKeyFrame[%d]\n",IsKeyFrame);
if(IsKeyFrame && (size() >= 20))
{
flushQuene();
printf("test: flushQuene\n");
}
bufLen = AddQueue(pBuf, remainingLen, IsKeyFrame);
//printf("test: decodeBuffer bufLen[%d],QueneSize[%d], timestamp[%ld]\n", bufLen, size(), getTimeStamp());
}
回调中写完buf就完事了,保险一点可以测一下写H264是不是正常,其实在前面已经验证过了。
再就是从缓存取数据实时解码部分,本着不修改demo结构的原则,最开始直接将解码部分移动到读取的线程中,这部分在上面提供的代码文件中貌似还有点痕迹,这个直接修改使用上还是有点问题,大致介绍下这部分处理遇到的问题。
调试的时候发现,缓存中明显的出现了进 > 出的情况,也就是解码线程存在延时导致buf很快就会溢出的情况,开始设置队列大小为5,基本一跑就满。然后设30,我想法是设置30也就是帧率大小,最大也就延时1s。如果实在处理不过来,就可以应用端处理数据,这个在实时播放处理中应该是比较常见的。如果buf设置过大,解码不过来数据倒是没丢,但是会出现累积延时的现象。
现在问题似乎又回到了解码性能问题。之所以用这个缓存还是为这部分考虑,先说下我个人对实时解码缓存的理解,因为这个可以引导后续的优化,要保证数据处理正常就需要保证有限大小实时缓存的动态平衡,也就是要保证写缓存和读缓存处于一个动态平衡状态,也即一定时间内写入的数据帧数量和读走帧数量要基本相同。本着这个原则,优化的方向应该就在于解码效率能跟上视频流的帧率。
通过代码在OSDK获取数据流的回调中先压了下底层吐数据帧率,保证给我数据是正常的。测试可以验证到OSDK读取视频流的帧率是基本稳定在30帧的,与官方提供的参数基本保持一致,所以这部分问题应该是不大。那读数据如果也按30帧算,也就是每帧解码和播放的时间大致估算应该在30ms左右,不然就会可能存在读数据跟不上写数据。
继续压下解码每个步骤的耗时,发现sws_scale渲染耗时非常高,最大能达到400ms,这个我暂时无法解释,在修改前这部分实际是没有这么高的耗时,按理来说这部分开的线程读缓存再通过SDLpushevent给SDL播放应该会更好,在网上开始了解了下该接口也是有在提sws_scale性能问题的,也调了下参数,基本没有看到什么效果。
然后开始尝试不渲染成RGB看看效果怎么样,直接用SDL播放YUV,此时延时很奇怪的就来到了UpdateYUVTexture,看起来就像是解码播放处理不过来,但问题耗时太高,及时清缓存都来不及,想在后期处理估计都困难。另外还有一个比较费解的是ffplay播放H264文件同样采用ffmpeg解码和SDL渲染播放,严格来说只有一个实时处理的差异,如果播放缓存处理不过来,播放文件也应该处理不过来才对。
大概看了下ffplay的源码,毕竟OSDK的视频播放功能也相对简单很多,就开始尝试修改现有代码逻辑,直接在SDL线程中读取缓存,就是现在代码中提供的搞法,将读取线程改成了读取缓存解码的函数,在主线程中调用,解码加播放处理延时减到了40 ~ 50ms. 再调整了下代码,将不要的步骤和打印处理掉后,效果看起来还是比较明显的得到了改善。
说明一下:播放的效果视频就不放了,放一个解码帧与获取帧的对比,decodebuffer是取流回调中的帧率,也就每秒写缓存的的帧数,stepSDL3中对应的是每秒解码显示的帧数,基本是保持一个动态平衡的状态。到这里基本目的应该算是达到了。
遗留问题:
*调试过程中解码H20 SEI帧报错比较影响调试,直接加了RemoveSEINal部分将SEI帧去掉了。这部分暂时没有SEI帧的相关说明,使用上应该是没有问题的。
*另外考虑到每帧处理还是有点高,代码中加了个关键帧和清缓存的部分,即到缓冲到25帧左右的时候如果来了关键帧直接将缓存清掉,人为的丢数据处理。如果处理不过来应该还是会有轻微的花屏,这部分暂没考虑平滑处理。
*提供的代码在我的虚拟环境中,CPU占应该在60~70左右,猜测应该还有优化空间。
评论
2 条评论
你好,貌似H20的相机获取到的流不是性能导致的花屏吧;是GDR编码的原因?
你好!你的这篇文章应该提供了相关的代码吧?在哪儿可以找到?谢谢!
请登录写评论。