1. 问题
被安卓的小伙伴告知我们iOS拉服务端某ts流,出现一直黑屏的问题,发现是在VideoToolbox解码时提示解码失败,返回值-12909(kVTVideoDecoderBadDataErr),奇怪之处还在于,有的ts流文件播放正常,于是使用ijk播放这个ts流,没有问题,将ijk拉到的流与我们的sdk拉到的流分别写264文件进行比对,找到问题啦,如下图
左边是ijk的正常播放数据,右边是我们sdk拉下来的数据,发现我们的流里每一帧后都有14字节(右图红色部分)未知数据,于是猜测是ffmpeg读出来的数据问题。
2. 定位
老规矩,源码调试看看到底哪里增加的这项数据
- av_read_frame->read_frame_internal,发现是该函数的尾部调用av_packet_merge_side_data加入了这些多余的数据,以下是该函数源码实现
备注1:这里的FF_MERGE_MARKER宏定义就是我们多余数据的后8字节 8C 4D 9D 10 8E 25 E9 FE
#define FF_MERGE_MARKER 0x8c4d9d108e25e9feULL
int av_packet_merge_side_data(AVPacket *pkt){
if(pkt->side_data_elems){
AVBufferRef *buf;
int i;
uint8_t *p;
uint64_t size= pkt->size + 8LL + AV_INPUT_BUFFER_PADDING_SIZE;
AVPacket old= *pkt;
for (i=0; i<old.side_data_elems; i++) {
size += old.side_data[i].size + 5LL;
}
if (size > INT_MAX)
return AVERROR(EINVAL);
buf = av_buffer_alloc(size);
if (!buf)
return AVERROR(ENOMEM);
pkt->buf = buf;
pkt->data = p = buf->data;
pkt->size = size - AV_INPUT_BUFFER_PADDING_SIZE;
bytestream_put_buffer(&p, old.data, old.size);
for (i=old.side_data_elems-1; i>=0; i--) {
备注2:将side_data的data与size与type写到pkt->data的后面
bytestream_put_buffer(&p, old.side_data[i].data, old.side_data[i].size);
bytestream_put_be32(&p, old.side_data[i].size);
*p++ = old.side_data[i].type | ((i==old.side_data_elems-1)*128);
}
备注3:将FF_MERGE_MARKER8字节的多余数据放到了pkt->data的尾部
bytestream_put_be64(&p, FF_MERGE_MARKER);
av_assert0(p-pkt->data == pkt->size);
memset(p, 0, AV_INPUT_BUFFER_PADDING_SIZE);
av_packet_unref(&old);
pkt->side_data_elems = 0;
pkt->side_data = NULL;
return 1;
}
return 0;
}
- 如源码内的备注1,备注2,备注3
- 接下来尝试寻找side_data_elems在哪里进行的赋值,从read_frame_internal->ff_read_packet->s->iformat->read_packet,这里的iformat->read_packet是函数指针,指向的是mpegs.c里的mpegts_read_packet,接着它调用new_pes_packet->av_packet_new_side_data->av_packet_add_side_data将pkt->side_data_elems++,源代码如下:
int av_packet_add_side_data(AVPacket *pkt, enum AVPacketSideDataType type,
uint8_t *data, size_t size)
{
AVPacketSideData *tmp;
int i, elems = pkt->side_data_elems;
for (i = 0; i < elems; i++) {
AVPacketSideData *sd = &pkt->side_data[i];
if (sd->type == type) {
av_free(sd->data);
sd->data = data;
sd->size = size;
return 0;
}
}
if ((unsigned)elems + 1 > AV_PKT_DATA_NB)
return AVERROR(ERANGE);
tmp = av_realloc(pkt->side_data, (elems + 1) * sizeof(*tmp));
if (!tmp)
return AVERROR(ENOMEM);
备注4: 这里的data暂时是一组0x00数据,size为1,type为AV_PKT_DATA_MPEGTS_STREAM_ID(0x4E)
pkt->side_data = tmp;
pkt->side_data[elems].data = data;
pkt->side_data[elems].size = size;
pkt->side_data[elems].type = type;
pkt->side_data_elems++;
return 0;
}
- 上图源码的备注4
- new_pes_packet最后一行*sd = pes->stream_id; 此时这个sd指向的是pkt->side_data[elems].data,于是这里我们可以知道pkt->side_data[0].data为pes包的stream_id(这里为视频流,因此它为0xE0),pkt->side_data[elems].size = 1,pkt->side_data[elems].type = 0x4E,再来到备注2的位置将这些值替换,发现写入的数据就是 E0 00 00 00 01 CE,正好与我们多余数据的前6字节完全对应上
- 14字节的多余数据已全部定位到,那么要如何去除呢,我们来看看ijk内部的实现,发现ijk在解码前会调用av_packet_split_side_data,这里就不贴该函数源码了,因为已经看到该函数内部将尾部的多余数据删除,于是在av_read_frame后也调用av_packet_split_side_data,解码成功
3. 扩展
调试源码的过程中发现了nal_type = 0x09的数据,之前没怎么留意过,查资料得知,0x09是AUD(Access Unit Delimiter),访问分隔单元,标志着一帧的结束。
4. 总结
有ijk爸爸真香。