=================================================================
音視頻入門基礎:MPEG2-PS專題系列文章:
音視頻入門基礎:MPEG2-PS專題(1)——MPEG2-PS官方文檔下載
音視頻入門基礎:MPEG2-PS專題(2)——使用FFmpeg命令生成ps文件
音視頻入門基礎:MPEG2-PS專題(3)——MPEG2-PS格式簡介
音視頻入門基礎:MPEG2-PS專題(4)——FFmpeg源碼中,判斷某文件是否為PS文件的實現
音視頻入門基礎:MPEG2-PS專題(5)——FFmpeg源碼中,解析PS流中的PES流的實現
音視頻入門基礎:MPEG2-PS專題(6)——FFmpeg源碼中,獲取PS流的視頻信息的實現
音視頻入門基礎:MPEG2-PS專題(7)——通過FFprobe顯示PS流每個packet的信息
=================================================================
一、引言
從《音視頻入門基礎: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_statereturn 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_statereturn 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流的實現》。