DTS和PTS的解释(FFMPEG、HLS相关)

转载请注明出处: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请求后,将执行以下操作。

  1. 调用avformat_seek_file(),完成文件的seek定位
  2. 清空解码前packet队列(音频、视频、字幕) 
  3. 调用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标准。后续读到的视频也有非关键帧的可能,从而导致花屏。
posted @ 2012-07-26 15:51  fpzeng  阅读(9172)  评论(0编辑  收藏  举报