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中
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将重新从零开始计数。
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包含一帧可被解码的图像,或者一帧或多帧音频。
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标准。后续读到的视频也有非关键帧的可能,从而导致花屏。