转载请注明出处:http://www.cnblogs.com/fpzeng/archive/2012/07/26/dts_pts.html
原由:
- 近来在研究HLS(HTTP Live Streaming),以实现android上播放m3u8文件。由于TS段的切分不统一,每个视频网站给出的m3u8 playlists总有差别,在时间戳显示上有差异,所以对DTS和PTS进行了研究。
- DTS和PTS是音视频同步的关键技术,同时也是丢帧策略密切相关。
dts/pts定义 dts: decoding time stamp pts: present time stamp 在ISO/IEC13818-1中制定90k Hz 的时钟,如果编码帧频是30,那么时间戳间隔就该是90000 / 30 = 3000。 在FFMPEG中有三种时间单位:秒、微秒和dts/pts。从dts/pts转化为微秒公式:
dts* AV_TIME_BASE/ denominator
其中AV_TIME_BASE为1,000,000,denominator为90,000。 拿到m3u8播放列表后,首先进行解析。HTTP Live Streaming标准草案可以从这里http://tools.ietf.org/html/draft-pantos-http-live-streaming-08查看。 解析代码在ffmpeg/libavformat/hls.c中
parse_playlist源代码
1 static int parse_playlist(HLSContext *c, const char *url,
2 struct variant *var, AVIOContext *in)
3 { int ret = 0, duration = 0, is_segment = 0, is_variant = 0, bandwidth = 0;
4 enum KeyType key_type = KEY_NONE;
5 uint8_t iv[16] = "";
6 int has_iv = 0;
7 char key[MAX_URL_SIZE] = "";
8 char line[1024];
9 const char *ptr;
10 int close_in = 0;
11
12 if (!in) {
13 close_in = 1;
14 if ((ret = avio_open2(&in, url, AVIO_FLAG_READ,
15 c->interrupt_callback, NULL)) < 0)
16 return ret;
17 }
18 read_chomp_line(in, line, sizeof(line));
19 if (strcmp(line, "#EXTM3U")) {
20 ret = AVERROR_INVALIDDATA;
21 goto fail;
22 }
23 if (var) {
24 free_segment_list(var);
25 var->finished = 0;
26 }
27 while (!url_feof(in)) {
28 read_chomp_line(in, line, sizeof(line));
29 if (av_strstart(line, "#EXT-X-STREAM-INF:", &ptr)) {
30 struct variant_info info = {{0}};
31 is_variant = 1;
32 ff_parse_key_value(ptr, (ff_parse_key_val_cb) handle_variant_args,
33 &info);
34 bandwidth = atoi(info.bandwidth);
35 } else if (av_strstart(line, "#EXT-X-KEY:", &ptr)) {
36 struct key_info info = {{0}};
37 ff_parse_key_value(ptr, (ff_parse_key_val_cb) handle_key_args,
38 &info);
39 key_type = KEY_NONE;
40 has_iv = 0;
41 if (!strcmp(info.method, "AES-128"))
42 key_type = KEY_AES_128;
43 if (!strncmp(info.iv, "0x", 2) || !strncmp(info.iv, "0X", 2)) {
44 ff_hex_to_data(iv, info.iv + 2);
45 has_iv = 1;
46 }
47 av_strlcpy(key, info.uri, sizeof(key));
48 } else if (av_strstart(line, "#EXT-X-TARGETDURATION:", &ptr)) {
49 if (!var) {
50 var = new_variant(c, 0, url, NULL);
51 if (!var) {
52 ret = AVERROR(ENOMEM);
53 goto fail;
54 }
55 }
56 var->target_duration = atoi(ptr);
57 } else if (av_strstart(line, "#EXT-X-MEDIA-SEQUENCE:", &ptr)) {
58 if (!var) {
59 var = new_variant(c, 0, url, NULL);
60 if (!var) {
61 ret = AVERROR(ENOMEM);
62 goto fail;
63 }
64 }
65 var->start_seq_no = atoi(ptr);
66 } else if (av_strstart(line, "#EXT-X-ENDLIST", &ptr)) {
67 if (var)
68 var->finished = 1;
69 } else if (av_strstart(line, "#EXTINF:", &ptr)) {
70 is_segment = 1;
71 duration = atoi(ptr);
72 } else if (av_strstart(line, "#", NULL)) {
73 continue;
74 } else if (line[0]) {
75 if (is_variant) {
76 if (!new_variant(c, bandwidth, line, url)) {
77 ret = AVERROR(ENOMEM);
78 goto fail;
79 }
80 is_variant = 0;
81 bandwidth = 0;
82 }
83 if (is_segment) {
84 struct segment *seg;
85 if (!var) {
86 var = new_variant(c, 0, url, NULL);
87 if (!var) {
88 ret = AVERROR(ENOMEM);
89 goto fail;
90 }
91 }
92 seg = av_malloc(sizeof(struct segment));
93 if (!seg) {
94 ret = AVERROR(ENOMEM);
95 goto fail;
96 }
97 seg->duration = duration;
98 seg->key_type = key_type;
99 if (has_iv) {
100 memcpy(seg->iv, iv, sizeof(iv));
101 } else {
102 int seq = var->start_seq_no + var->n_segments;
103 memset(seg->iv, 0, sizeof(seg->iv));
104 AV_WB32(seg->iv + 12, seq);
105 }
106 ff_make_absolute_url(seg->key, sizeof(seg->key), url, key);
107 ff_make_absolute_url(seg->url, sizeof(seg->url), url, line);
108 dynarray_add(&var->segments, &var->n_segments, seg);
109 is_segment = 0;
110 }
111 }
112 }
113 if (var)
114 var->last_load_time = av_gettime();
115 fail:
116 if (close_in)
117 avio_close(in);
118 return ret;
119 }
解析播放列表的问题:
当解析到#EXT-X-TARGETDURATION标签时,后面紧跟着的是TS段的最大时长,当前没有什么用。#EXTINF标签后紧跟的是当前TS段的时长,当EXT-X-VERSION标签大于等于3时,TS段的时长可以为小数,当前(2012-07-26)的FFMPEG代码还不支持EXT-X-VERSION标签的判断,TS段的时长也为整数。seg->duration保存了当前段的时长,单位为秒。当前草案中还有EXT-X-DISCONTINUITY标签,它表征其后面的视频段文件和之前的不连续,这意味着文件格式、时间戳顺序、编码参数等的变化。但是很遗憾,当前FFMPEG仍然不支持,这意味着该标签出现后,后续的PES中携带的dts和pts将重新从零开始计数。
HLS上下文结构体
1 typedef struct HLSContext {
2 int n_variants;
3 struct variant **variants;
4 int cur_seq_no;
5 int end_of_segment;
6 int first_packet;
7 int64_t first_timestamp;
8 int64_t seek_timestamp;
9 int seek_flags;
10 AVIOInterruptCB *interrupt_callback;
11 } HLSContext;
HLS上下文中存在当前的段序号,在HLS.c文件中,hls_read()函数根据判断得到当前段读取完毕后,将cur_seq_no加一,从而读取下一个TS段。在hls_read_packet()函数读取一个packet,该packet包含一帧可被解码的图像,或者一帧或多帧音频。
hls_read_packet源代码
1 static int hls_read_packet(AVFormatContext *s, AVPacket *pkt)
2 {
3 HLSContext *c = s->priv_data;
4 int ret, i, minvariant = -1;
5
6 if (c->first_packet) {
7 recheck_discard_flags(s, 1);
8 c->first_packet = 0;
9 }
10
11 start:
12 c->end_of_segment = 0;
13 for (i = 0; i < c->n_variants; i++) {
14 struct variant *var = c->variants[i];
15 /* Make sure we've got one buffered packet from each open variant
16 * stream */
17 if (var->needed && !var->pkt.data) {
18 while (1) {
19 int64_t ts_diff;
20 AVStream *st;
21 ret = av_read_frame(var->ctx, &var->pkt);
22 if (ret < 0) {
23 if (!url_feof(&var->pb))
24 return ret;
25 reset_packet(&var->pkt);
26 break;
27 } else {
28 if (c->first_timestamp == AV_NOPTS_VALUE)
29 c->first_timestamp = var->pkt.dts;
30 }
31
32 if (c->seek_timestamp == AV_NOPTS_VALUE)
33 break;
34
35 if (var->pkt.dts == AV_NOPTS_VALUE) {
36 c->seek_timestamp = AV_NOPTS_VALUE;
37 break;
38 }
39
40 st = var->ctx->streams[var->pkt.stream_index];
41 ts_diff = av_rescale_rnd(var->pkt.dts, AV_TIME_BASE,
42 st->time_base.den, AV_ROUND_DOWN) -
43 c->seek_timestamp;
44 if (ts_diff >= 0 && (c->seek_flags & AVSEEK_FLAG_ANY ||
45 var->pkt.flags & AV_PKT_FLAG_KEY)) {
46 c->seek_timestamp = AV_NOPTS_VALUE;
47 break;
48 }
49 }
50 }
51 /* Check if this stream has the packet with the lowest dts */
52 if (var->pkt.data) {
53 if (minvariant < 0 ||
54 var->pkt.dts < c->variants[minvariant]->pkt.dts)
55 minvariant = i;
56 }
57 }
58 if (c->end_of_segment) {
59 if (recheck_discard_flags(s, 0))
60 goto start;
61 }
62 /* If we got a packet, return it */
63 if (minvariant >= 0) {
64 *pkt = c->variants[minvariant]->pkt;
65 pkt->stream_index += c->variants[minvariant]->stream_offset;
66 reset_packet(&c->variants[minvariant]->pkt);
67 return 0;
68 }
69 return AVERROR_EOF;
70 }
这里c->seek_timestamp为标志位,它表征当前视频发生了SEEK事件,当发生SEEK事件后首先调用hls_read_seek()函数定位到应该读取的TS段,更新HLS上下文中的段序号。当读取到该段的packet,有两种判断。 在ffplay中,当外界发起seek请求后,将执行以下操作。
- 调用avformat_seek_file(),完成文件的seek定位
- 清空解码前packet队列(音频、视频、字幕)
- 调用avcodec_flush_buffers(),清空解码buffer和相关状态
在第一个步骤中,将在HLS层进行seek操作,seek流程图如下图所示:

首先读取packet,判断是否有seek操作,没有则直接将该packet返回,送人后续的解码操作。如果是seek情况,则读取dts时间戳,如果dts没有值,则直接清除seek标志并返回packet(问题一)。如果dts时间戳有值,则将该值转化为微秒并与seek传入的时间进行比较,看是否大于seek时间,如果大于则表明读取的packet达到了seek要求(问题二),否则继续读packet。如果seek时间已经满足,则看该packet的flags是否是关键帧,如果是则返回该packet(问题三),否则继续读packet。
该流程很简单,但是带来了三个问题。分别解释
- 问题一,如果dts没有值,返回回去后,解码状态全部进行了reset,则送入的第一帧信息应该为关键帧,否则该帧需要参考其他帧,产生花屏。
- 问题二,如果dts时间戳有误,将出现dts转化为微秒后永远小于seek传入时间问题,则永远无法返回packet,导致seek僵死。
- 问题三,判断packet是否为关键帧,忽略了该packet是否为视频,如果该packet为音频并且flag & AV_PKT_FLAG_KEY的结果为真,则将返回该packet并清空seek标准。后续读到的视频也有非关键帧的可能,从而导致花屏。
来源:https://www.cnblogs.com/fpzeng/archive/2012/07/26/dts_pts.html
