一、引言
从《音视频入门基础:MPEG2-PS专题(3)——MPEG2-PS格式简介》中可以知道,PS流由一个个pack(包装)组成。一个pack = 一个pack_header + 一个或多个PES_packet。pack_header中还可能存在system header。
但是pack_header和system header中并没有什么重要信息,所以FFmpeg源码在解析PS流时会跳过pack_header和system header,直接解析PES packet。
FFmpeg源码中通过mpegps_read_pes_header函数解析PS流中的PES packet。
二、mpegps_read_pes_header函数的定义
mpegps_read_pes_header函数定义在FFmpeg源码(本文演示用的FFmpeg源码版本为7.0.1)的源文件libavformat/mpeg.c中:
/* read the next PES header. Return its position in ppos
* (if not NULL), and its start code, pts and dts.
*/
static int mpegps_read_pes_header(AVFormatContext *s,
int64_t *ppos, int *pstart_code,
int64_t *ppts, int64_t *pdts)
{
MpegDemuxContext *m = s->priv_data;
int len, size, startcode, c, flags, header_len;
int pes_ext, ext2_len, id_ext, skip;
int64_t pts, dts;
int64_t last_sync = avio_tell(s->pb);
error_redo:
avio_seek(s->pb, last_sync, SEEK_SET);
redo:
/* next start code (should be immediately after) */
m->header_state = 0xff;
size = MAX_SYNC_SIZE;
startcode = find_next_start_code(s->pb, &size, &m->header_state);
last_sync = avio_tell(s->pb);
if (startcode < 0) {
if (avio_feof(s->pb))
return AVERROR_EOF;
// FIXME we should remember header_state
return FFERROR_REDO;
}
if (startcode == PACK_START_CODE)
goto redo;
if (startcode == SYSTEM_HEADER_START_CODE)
goto redo;
if (startcode == PADDING_STREAM) {
avio_skip(s->pb, avio_rb16(s->pb));
goto redo;
}
if (startcode == PRIVATE_STREAM_2) {
if (!m->sofdec) {
/* Need to detect whether this from a DVD or a 'Sofdec' stream */
int len = avio_rb16(s->pb);
int bytesread = 0;
uint8_t *ps2buf = av_malloc(len);
if (ps2buf) {
bytesread = avio_read(s->pb, ps2buf, len);
if (bytesread != len) {
avio_skip(s->pb, len - bytesread);
} else {
uint8_t *p = 0;
if (len >= 6)
p = memchr(ps2buf, 'S', len - 5);
if (p)
m->sofdec = !memcmp(p+1, "ofdec", 5);
m->sofdec -= !m->sofdec;
if (m->sofdec < 0) {
if (len == 980 && ps2buf[0] == 0) {
/* PCI structure? */
uint32_t startpts = AV_RB32(ps2buf + 0x0d);
uint32_t endpts = AV_RB32(ps2buf + 0x11);
uint8_t hours = ((ps2buf[0x19] >> 4) * 10) + (ps2buf[0x19] & 0x0f);
uint8_t mins = ((ps2buf[0x1a] >> 4) * 10) + (ps2buf[0x1a] & 0x0f);
uint8_t secs = ((ps2buf[0x1b] >> 4) * 10) + (ps2buf[0x1b] & 0x0f);
m->dvd = (hours <= 23 &&
mins <= 59 &&
secs <= 59 &&
(ps2buf[0x19] & 0x0f) < 10 &&
(ps2buf[0x1a] & 0x0f) < 10 &&
(ps2buf[0x1b] & 0x0f) < 10 &&
endpts >= startpts);
} else if (len == 1018 && ps2buf[0] == 1) {
/* DSI structure? */
uint8_t hours = ((ps2buf[0x1d] >> 4) * 10) + (ps2buf[0x1d] & 0x0f);
uint8_t mins = ((ps2buf[0x1e] >> 4) * 10) + (ps2buf[0x1e] & 0x0f);
uint8_t secs = ((ps2buf[0x1f] >> 4) * 10) + (ps2buf[0x1f] & 0x0f);
m->dvd = (hours <= 23 &&
mins <= 59 &&
secs <= 59 &&
(ps2buf[0x1d] & 0x0f) < 10 &&
(ps2buf[0x1e] & 0x0f) < 10 &&
(ps2buf[0x1f] & 0x0f) < 10);
}
}
}
av_free(ps2buf);
/* If this isn't a DVD packet or no memory
* could be allocated, just ignore it.
* If we did, move back to the start of the
* packet (plus 'length' field) */
if (!m->dvd || avio_skip(s->pb, -(len + 2)) < 0) {
/* Skip back failed.
* This packet will be lost but that can't be helped
* if we can't skip back
*/
goto redo;
}
} else {
/* No memory */
avio_skip(s->pb, len);
goto redo;
}
} else if (!m->dvd) {
int len = avio_rb16(s->pb);
avio_skip(s->pb, len);
goto redo;
}
}
if (startcode == PROGRAM_STREAM_MAP) {
mpegps_psm_parse(m, s->pb);
goto redo;
}
/* find matching stream */
if (!((startcode >= 0x1c0 && startcode <= 0x1df) ||
(startcode >= 0x1e0 && startcode <= 0x1ef) ||
(startcode == 0x1bd) ||
(startcode == PRIVATE_STREAM_2) ||
(startcode == 0x1fd)))
goto redo;
if (ppos) {
*ppos = avio_tell(s->pb) - 4;
}
len = avio_rb16(s->pb);
pts =
dts = AV_NOPTS_VALUE;
if (startcode != PRIVATE_STREAM_2)
{
/* stuffing */
for (;;) {
if (len < 1)
goto error_redo;
c = avio_r8(s->pb);
len--;
/* XXX: for MPEG-1, should test only bit 7 */
if (c != 0xff)
break;
}
if ((c & 0xc0) == 0x40) {
/* buffer scale & size */
avio_r8(s->pb);
c = avio_r8(s->pb);
len -= 2;
}
if ((c & 0xe0) == 0x20) {
dts =
pts = get_pts(s->pb, c);
len -= 4;
if (c & 0x10) {
dts = get_pts(s->pb, -1);
len -= 5;
}
} else if ((c & 0xc0) == 0x80) {
/* mpeg 2 PES */
flags = avio_r8(s->pb);
header_len = avio_r8(s->pb);
len -= 2;
if (header_len > len)
goto error_redo;
len -= header_len;
if (flags & 0x80) {
dts = pts = get_pts(s->pb, -1);
header_len -= 5;
if (flags & 0x40) {
dts = get_pts(s->pb, -1);
header_len -= 5;
}
}
if (flags & 0x3f && header_len == 0) {
flags &= 0xC0;
av_log(s, AV_LOG_WARNING, "Further flags set but no bytes left\n");
}
if (flags & 0x01) { /* PES extension */
pes_ext = avio_r8(s->pb);
header_len--;
/* Skip PES private data, program packet sequence counter
* and P-STD buffer */
skip = (pes_ext >> 4) & 0xb;
skip += skip & 0x9;
if (pes_ext & 0x40 || skip > header_len) {
av_log(s, AV_LOG_WARNING, "pes_ext %X is invalid\n", pes_ext);
pes_ext = skip = 0;
}
avio_skip(s->pb, skip);
header_len -= skip;
if (pes_ext & 0x01) { /* PES extension 2 */
ext2_len = avio_r8(s->pb);
header_len--;
if ((ext2_len & 0x7f) > 0) {
id_ext = avio_r8(s->pb);
if ((id_ext & 0x80) == 0)
startcode = ((startcode & 0xff) << 8) | id_ext;
header_len--;
}
}
}
if (header_len < 0)
goto error_redo;
avio_skip(s->pb, header_len);
} else if (c != 0xf)
goto redo;
}
if (startcode == PRIVATE_STREAM_1) {
int ret = ffio_ensure_seekback(s->pb, 2);
if (ret < 0)
return ret;
startcode = avio_r8(s->pb);
m->raw_ac3 = 0;
if (startcode == 0x0b) {
if (avio_r8(s->pb) == 0x77) {
startcode = 0x80;
m->raw_ac3 = 1;
avio_skip(s->pb, -2);
} else {
avio_skip(s->pb, -1);
}
} else {
len--;
}
}
if (len < 0)
goto error_redo;
if (dts != AV_NOPTS_VALUE && ppos) {
int i;
for (i = 0; i < s->nb_streams; i++) {
if (startcode == s->streams[i]->id &&
(s->pb->seekable & AVIO_SEEKABLE_NORMAL) /* index useless on streams anyway */) {
ff_reduce_index(s, i);
av_add_index_entry(s->streams[i], *ppos, dts, 0, 0,
AVINDEX_KEYFRAME /* FIXME keyframe? */);
}
}
}
*pstart_code = startcode;
*ppts = pts;
*pdts = dts;
return len;
}
该函数的作用就是:解析PS流中的一个PES packet,将其PES packet header里面的信息解析出来。
形参s:既是输入型参数也是输出型参数,指向一个AVFormatContext类型的变量。s->pb包含需要被解析的PS流的二进制数据。mpegps_read_pes_header函数解析的是s->pb->buf_ptr指向的PS流数据中的下一个PES packet。
形参ppos:输出型参数,*ppos为读取到的PES packet相对于文件首的偏移字节数。
形参pstart_code:输出型参数。如果读取到了PES packet,且该PES packet里面包含的不是的private_stream_1,*pstart_code为其PES packet header中的stream_id属性的值加0x100。
形参ppts:输出型参数。*ppts为从该PES packet的PES packet header中读取到的pts。
形参pdts:输出型参数。*pdts为从该PES packet的PES packet header中读取到的dts。
返回值:解析成功,返回该PES packet去掉PES packet header后的大小(即基本码流ES数据的大小)。解析失败,返回一个负数。
三、mpegps_read_pes_header函数的内部实现分析
mpegps_read_pes_header函数中,首先通过find_next_start_code函数找到s->pb->buf_ptr指向的PS流数据中的下一个pack header的起始码 或 下一个system header的起始码 或 下一个PES packet header的起始码:
redo:
/* next start code (should be immediately after) */
m->header_state = 0xff;
size = MAX_SYNC_SIZE;
startcode = find_next_start_code(s->pb, &size, &m->header_state);
last_sync = avio_tell(s->pb);
if (startcode < 0) {
if (avio_feof(s->pb))
return AVERROR_EOF;
// FIXME we should remember header_state
return FFERROR_REDO;
}
如果找到的是pack header的起始码 或 system header的起始码 或 找到的PES packet中包含padding_stream数据,通过goto语句跳转,然后重新通过find_next_start_code函数查找下一个起始码:
if (startcode == PACK_START_CODE)
goto redo;
if (startcode == SYSTEM_HEADER_START_CODE)
goto redo;
if (startcode == PADDING_STREAM) {
avio_skip(s->pb, avio_rb16(s->pb));
goto redo;
}
如果找到了符合要求的PES packet header的起始码,通过avio_tell函数读取该PES packet相对于文件首的偏移字节数,赋值给*ppos。关于avio_tell函数的用法可以参考:《FFmpeg源码:avio_tell函数分析》:
/* find matching stream */
if (!((startcode >= 0x1c0 && startcode <= 0x1df) ||
(startcode >= 0x1e0 && startcode <= 0x1ef) ||
(startcode == 0x1bd) ||
(startcode == PRIVATE_STREAM_2) ||
(startcode == 0x1fd)))
goto redo;
if (ppos) {
*ppos = avio_tell(s->pb) - 4;
}
读取PES packet header中的PES_packet_length属性,赋值给变量len。关于avio_rb16函数的用法可以参考:《FFmpeg源码:avio_r8、avio_rl16、avio_rl24、avio_rl32、avio_rl64函数分析》:
len = avio_rb16(s->pb);
读取PES packet header中的PES_scrambling_control、PES_priority、data_alignment_indicator、copyright、original_or_copy属性,赋值给变量c:
c = avio_r8(s->pb);
如果上述读取到的PES_packet_length属性后面的值为"10((c & 0xc0) == 0x80为真),表示读取到的PES packet header的格式正确:
执行大括号内的内容:
else if ((c & 0xc0) == 0x80) {
//...
}
读取PTS_DTS_flags、ESCR_flag、ES_rate_flag、DSM_trick_mode_flag、additional_copy_info_flag、PES_CRC_flag、PES_extension_flag这7个属性,赋值给变量flags:
flags = avio_r8(s->pb);
读取PES_header_data_length属性,赋值给变量header_len:
header_len = avio_r8(s->pb);
如果PTS_DTS_flags属性的值为'10',表示PES packet header中会存在PTS,读取PTS;值为'11'时,表示PES packet header中会同时存在PTS和DTS,读取PTS和DTS,分别赋值给变量dts和pts:
if (flags & 0x80) {
dts = pts = get_pts(s->pb, -1);
header_len -= 5;
if (flags & 0x40) {
dts = get_pts(s->pb, -1);
header_len -= 5;
}
}
如果PES_extension_flag属性的值为1,表示PES packet header有PES_extension域,读取PES_extension域:
if (flags & 0x01) { /* PES extension */
pes_ext = avio_r8(s->pb);
header_len--;
/* Skip PES private data, program packet sequence counter
* and P-STD buffer */
skip = (pes_ext >> 4) & 0xb;
skip += skip & 0x9;
if (pes_ext & 0x40 || skip > header_len) {
av_log(s, AV_LOG_WARNING, "pes_ext %X is invalid\n", pes_ext);
pes_ext = skip = 0;
}
avio_skip(s->pb, skip);
header_len -= skip;
if (pes_ext & 0x01) { /* PES extension 2 */
ext2_len = avio_r8(s->pb);
header_len--;
if ((ext2_len & 0x7f) > 0) {
id_ext = avio_r8(s->pb);
if ((id_ext & 0x80) == 0)
startcode = ((startcode & 0xff) << 8) | id_ext;
header_len--;
}
}
}
最后返回从该PES packet的PES packet header中读取到的pts、dts、该PES packet去掉PES packet header后的大小等信息:
*pstart_code = startcode;
*ppts = pts;
*pdts = dts;
return len;
四、FFmpeg源码中,解析TS流中的PES流的实现
FFmpeg源码中,解析TS流中的PES流所使用的函数不一样,是通过mpegts_push_data函数进行解析的,具体可以参考:《音视频入门基础:MPEG2-TS专题(19)——FFmpeg源码中,解析TS流中的PES流的实现》。