音视频入门基础:MPEG2-TS专题(7)——FFmpeg源码中,读取出一个transport packet数据的实现
一、引言
从《音视频入门基础:MPEG2-TS专题(3)——TS Header简介》可以知道,TS格式有三种:分别为transport packet长度固定为188、192和204字节。而FFmpeg源码中是通过read_packet函数从一段MPEG2-TS传输流/TS文件中读取出一个transport packet的。
二、read_packet函数
(一)read_packet函数的定义
read_packet函数定义在FFmpeg源码(本文演示用的FFmpeg源码版本为7.0.1)的源文件libavformat/mpegts.c中:
/* return AVERROR_something if error or EOF. Return 0 if OK. */
static int read_packet(AVFormatContext *s, uint8_t *buf, int raw_packet_size,
const uint8_t **data)
{
AVIOContext *pb = s->pb;
int len;
for (;;) {
len = ffio_read_indirect(pb, buf, TS_PACKET_SIZE, data);
if (len != TS_PACKET_SIZE)
return len < 0 ? len : AVERROR_EOF;
/* check packet sync byte */
if ((*data)[0] != 0x47) {
/* find a new packet start */
if (mpegts_resync(s, raw_packet_size, *data) < 0)
return AVERROR(EAGAIN);
else
continue;
} else {
break;
}
}
return 0;
}
该函数的作用是:从一段MPEG2-TS传输流/TS文件或内存中读取出接下来的一个transport packet的前188个字节。transport packet长度为192和204字节的TS格式实际上是在188字节的Packet后部加上额外的字段,所以read_packet函数只会读取出transport packet的前188字节。对于transport packet长度固定为188字节的TS格式,使用read_packet函数可以读取出一个transport packet的全部数据。
形参s:既是输入型参数也是输出型参数。指向一个AVFormatContext类型变量。
形参buf:输出型参数。仅当“AVIOContext输入缓冲区中还未被读取的数据量不小于计划要读取的字节数(TS_PACKET_SIZE,188个字节)”,并且“该MPEG2-TS传输流/TS文件被打开不是为了写入的”时有意义。保存读上来的数据的缓冲区。
形参data:输出型参数。“*data”指向保存读上来的数据的缓冲区。执行read_packet函数后,“*data”指向缓冲区会存贮被读取到的这个transport packet的前188字节。
返回值:返回0表示成功,返回一个负数表示出错。
(二)read_packet函数的内部实现分析
TS_PACKET_SIZE为宏定义,等价于188,表示一个普通transport packet的长度:
#define TS_PACKET_SIZE 188
read_packet函数中,首先通过ffio_read_indirect函数从内存或TS文件或socket中读取188个字节数据,如果实际读取到的数据小于188字节,表示文件读完了或出错了,read_packet函数直接返回。关于ffio_read_indirect函数的用法可以参考:《FFmpeg源码:ffio_read_indirect函数分析》:
len = ffio_read_indirect(pb, buf, TS_PACKET_SIZE, data);
if (len != TS_PACKET_SIZE)
return len < 0 ? len : AVERROR_EOF;
如果上述读取到的数据的第一个字节不是同步字节(0x47),执行mpegts_resync函数进行重新同步操作,让AVIOContext文件位置指针(s->pb->buf_ptr)指向MPEG2-TS传输流/TS文件中的值为“0x47”的同步字节。再通过continue关键字在for循环中重新执行ffio_read_indirect函数,重新读取188个字节数据,保证读取到的数据的第一个字节为同步字节,从而保证通过read_packet函数读取的是一个transport packet的前188个字节数据:
/* check packet sync byte */
if ((*data)[0] != 0x47) {
/* find a new packet start */
if (mpegts_resync(s, raw_packet_size, *data) < 0)
return AVERROR(EAGAIN);
else
continue;
} else {
break;
}
三、mpegts_resync函数
(一)mpegts_resync函数的定义
mpegts_resync函数定义在源文件libavformat/mpegts.c中:
static int mpegts_resync(AVFormatContext *s, int seekback, const uint8_t *current_packet)
{
MpegTSContext *ts = s->priv_data;
AVIOContext *pb = s->pb;
int c, i;
uint64_t pos = avio_tell(pb);
int64_t back = FFMIN(seekback, pos);
//Special case for files like 01c56b0dc1.ts
if (current_packet[0] == 0x80 && current_packet[12] == 0x47 && pos >= TS_PACKET_SIZE) {
avio_seek(pb, 12 - TS_PACKET_SIZE, SEEK_CUR);
return 0;
}
avio_seek(pb, -back, SEEK_CUR);
for (i = 0; i < ts->resync_size; i++) {
c = avio_r8(pb);
if (avio_feof(pb))
return AVERROR_EOF;
if (c == 0x47) {
int new_packet_size, ret;
avio_seek(pb, -1, SEEK_CUR);
pos = avio_tell(pb);
ret = ffio_ensure_seekback(pb, PROBE_PACKET_MAX_BUF);
if (ret < 0)
return ret;
new_packet_size = get_packet_size(s);
if (new_packet_size > 0 && new_packet_size != ts->raw_packet_size) {
av_log(ts->stream, AV_LOG_WARNING, "changing packet size to %d\n", new_packet_size);
ts->raw_packet_size = new_packet_size;
}
avio_seek(pb, pos, SEEK_SET);
return 0;
}
}
av_log(s, AV_LOG_ERROR,
"max resync size reached, could not find sync byte\n");
/* no sync found */
return AVERROR_INVALIDDATA;
}
该函数的作用是:进行重新同步操作,让AVIOContext文件位置指针(s->pb->buf_ptr)指向MPEG2-TS传输流/TS文件中的值为“0x47”的同步字节。
形参s:既是输入型参数也是输出型参数。指向一个AVFormatContext类型变量。
形参seekback:输入型参数。需要回退的大小,值一般为该MPEG2-TS传输流/TS文件中的一个transport packet的长度,以字节为单位。
形参current_packet:输入型参数,保存一个transport packet的数据。
返回值:返回0表示成功,返回一个负数表示出错。
(二)mpegts_resync函数的内部实现分析
mpegts_resync函数中,首先通过avio_tell函数,得到文件位置指针当前位置(s->buf_ptr)相对于TS文件的文件首(s->buffer)的偏移字节数。关于avio_tell函数用法可以参考:《FFmpeg源码:avio_tell函数分析》:
uint64_t pos = avio_tell(pb);
得到需要回退的最小值:
int64_t back = FFMIN(seekback, pos);
判断该媒体文件/流是不是属于TS文件的特殊例子(非标的TS文件),如果是,把AVIOContext的文件位置指针回退到离当前位置(12 - TS_PACKET_SIZE)字节处。关于avio_seek函数的有用法可以参考:《FFmpeg源码:avio_seek函数分析》:
//Special case for files like 01c56b0dc1.ts
if (current_packet[0] == 0x80 && current_packet[12] == 0x47 && pos >= TS_PACKET_SIZE) {
avio_seek(pb, 12 - TS_PACKET_SIZE, SEEK_CUR);
return 0;
}
不断通过avio_r8函数读取一个字节数据(关于avio_r8函数的有用法可以参考:FFmpeg源码:avio_r8、avio_rl16、avio_rl24、avio_rl32、avio_rl64函数分析),如果读取到TS文件的末尾了(avio_feof(pb)为真),返回AVERROR_EOF(关于avio_feof函数的有用法可以参考:FFmpeg源码:avio_feof函数分析)。如果还没读取到末尾,并且读取到的数据是“0x47”,表示是同步字节,执行if (c == 0x47)为真时大括号里的操作:
for (i = 0; i < ts->resync_size; i++) {
c = avio_r8(pb);
if (avio_feof(pb))
return AVERROR_EOF;
if (c == 0x47) {
//...
return 0;
}
}
c == 0x47为真时,首先通过avio_seek函数让AVIOContext的文件位置指针回退一个字节,这样pb->buf_ptr就会指向值为“0x47”的同步字节:
int new_packet_size, ret;
avio_seek(pb, -1, SEEK_CUR);
重新得到此时文件位置指针当前位置(s->buf_ptr)相对于TS文件的文件首(s->buffer)的偏移字节数:
pos = avio_tell(pb);
确保请求的seekback缓冲区大小可用:
ret = ffio_ensure_seekback(pb, PROBE_PACKET_MAX_BUF);
if (ret < 0)
return ret;
得到这段MPEG2-TS传输流/TS文件中每个transport packet的长度,赋值给变量new_packet_size。关于get_packet_size函数的用法可以参考:《音视频入门基础:MPEG2-TS专题(6)——FFmpeg源码中,获取MPEG2-TS传输流每个transport packet长度的实现》:
new_packet_size = get_packet_size(s);
如果上述获取到的transport packet的长度跟原来内存中保存的transport packet长度不一致,打印日志:"changing packet size to XXX",调整内存中保存的transport packet长度(ts->raw_packet_size)为新的长度:
if (new_packet_size > 0 && new_packet_size != ts->raw_packet_size) {
av_log(ts->stream, AV_LOG_WARNING, "changing packet size to %d\n", new_packet_size);
ts->raw_packet_size = new_packet_size;
}
由于执行上述get_packet_size函数后,AVIOContext的文件位置指针(pb->buf_ptr)会改变,所以重新执行一次avio_seek函数,确保pb->buf_ptr指向值为“0x47”的同步字节:
avio_seek(pb, pos, SEEK_SET);
return 0;