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標准。后續讀到的視頻也有非關鍵幀的可能,從而導致花屏。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM