本文来源于生产环境的实际问题,值得大家一看。
鉴于背景的复杂性,将该问题的背景简述如下:客户端A将桌面采集的数据以H264的格式推送到服务器B,服务器B将数据转发到客户端C,客户端C接收数据解码渲染。客户端C渲染完的数据存在花屏。
花屏的同时,客户端C会一直收到H264解码器的报错信息,
[h264 @ 0x1690000] No start code is found.
[h264 @ 0x1690000] Error splitting the input into NAL units.
结合日志,解码器提示No start code is found.
,则说明解码器接收到的 H264 流找不到对应的 start code,解码器收到的 H264 流存在问题,所以为了验证传输的数据流的完整性和正确性,
首先想到的是使用第三方工具接收对应的视频流,然后播放。同时使用了 ffplay 、 vlc ,两者均未出现花屏,且均能正常播放。这足以证明客户端C接收到的 H264 流是正确且完整的。
但是解码器依然报错,那问题点可以锁定在 H264 流送给解码器过程存在问题。所以分析的重点便是 H264流送给解码器 这一过程。
结合读者朋友的代码逻辑,其将接收到数据的H264流作为一个packet传送给解码器,代码如下:
auto packet = av_packet_alloc();
auto pFrame = av_frame_alloc();
packet->data = (uint8_t*)data;
packet->size = dataLen;
auto ret = avcodec_send_packet(pCodecCtx, packet);
//other code
如上的代码直接将接收到的数据作为一个 packet 传送给解码器,但是我们并不能保证接收到的数据一定是一个完整的NALU,还可能是多个NALU,所以问题的根本原因为:接收到的数据并不是数据包,不能直接用来解码。
故此接收到的数据需要经过解析器解析,将数据拆分成一个个完整的 NALU ,再传送给解码器。
FFmpeg提供了解析器相关的接口av_parser_parse2(),用于将流数据解析为一个个的数据包,接口如下:
//功能:解析数据流,
//参数:s:解析器上下文,
// avctx:解码器上下文,
// poutbuf:解析后的数据包,
// poutbuf_size:解析后的数据包大小,
// buf:待解析的数据,
// buf_size:待解析的数据大小,
// pts:时间戳,
// dts:解码时间戳,
// pos:数据包在文件中的位置
//返回值:解析数据时所使用的字节数,
int av_parser_parse2(AVCodecParserContext *s,
AVCodecContext *avctx,
uint8_t **poutbuf, int *poutbuf_size,
const uint8_t *buf, int buf_size,
int64_t pts, int64_t dts,
int64_t pos);
根据上述分析,为解决如上问题需要借助解析器将接收到的数据拆分成一个个完整的 NALU ,再传送给解码器,代码如下:
// 初始化解析器
auto pParser = av_parser_init(pCodecCtx->codec_id);
if (!pParser) {
std::cerr << "Failed to initialize parser." << std::endl;
return-1;
}
// 分配AVPacket和AVFrame
auto packet = av_packet_alloc();
auto pFrame = av_frame_alloc();
if (!packet || !pFrame) {
std::cerr << "Failed to allocate packet or frame." << std::endl;
return-1;
}
// 获取数据长度和指针
auto dataLen = data.size(); // data作为std::vector<uint8_t>
auto dataPtr = data.data();
int dataRemain = dataLen;
// 循环解析数据流
while (dataRemain > 0) {
int parsedLen = av_parser_parse2(pParser, pCodecCtx, &packet->data, &packet->size,
dataPtr, dataRemain, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
if (parsedLen < 0) {
std::cerr << "Error occurred during parsing." << std::endl;
return-1;
}
dataPtr += parsedLen;
dataRemain -= parsedLen;
if (packet->size > 0) {
// 将解析后的NALU传递给解码器
int ret = avcodec_send_packet(pCodecCtx, packet);
if (ret < 0) {
std::cerr << "Error sending packet to decoder." << std::endl;
return-1;
}
// 循环接收解码后的帧
while (true) {
ret = avcodec_receive_frame(pCodecCtx, pFrame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break; // 没有更多帧可解码
} elseif (ret < 0) {
std::cerr << "Error during frame decoding." << std::endl;
return-1;
}
// 处理解码后的帧,例如保存到队列中
}
}
}
// 释放资源
av_packet_free(&packet);
av_frame_free(&pFrame);
av_parser_close(pParser);
本文记录了一个线上问题的分析和解决过程。问题的根本原因是客户端C直接将未解析的H.264流传递给解码器,而没有正确分割 NALU 。通过使用av_parser_parse2
对数据流进行解析,可以将原始数据流拆分成完整的 NALU ,从而解决解码器报错的问题。
在生产环境中,建议在数据传输和解码前,始终对数据流进行解析,确保传递给解码器的是完整的 NALU 。此外,通过日志分析和逐步调试,可以快速定位问题,避免因数据格式问题导致的花屏或解码失败。