某天測試反饋,硬解某個hls流后面幾秒鐘無法播放,以為解碼錯誤導致,但實際解碼正常。經過排查(排查的過錯有些曲折,就不細說了),發(fā)現(xiàn)是解碼出來的PTS異常:某個包pts異常的大,導致渲染模塊把后面收到的包都拋掉了。
為什么會突然來一個特別大的PTS?真實環(huán)境下各種亂七八糟的格式見得多了,搞不好是流本身的問題,這是我第一時間能想到的。只是軟解可以正常播放,這促使我進一步去軟解一探究竟,發(fā)現(xiàn)原來decoder修改了pts。ffmpeg 源代碼簡單分析 : avcodec_decode_video2()
注意到這一行
av_frame_set_best_effort_timestamp(picture,
guess_correct_pts(avctx,
picture->pkt_pts,
picture->pkt_dts));
guess_correct_pts返回了正確的pts。guess_correct_pts里面算法挺簡單的,就是統(tǒng)計dts亂序的次數(shù),然后確定最終是否用dts代替pts。
雖然從avformat得到的dts也是錯的,decoder內部對dts也做了修正。修正就細節(jié)比較復雜,而iOS的硬解對DTS完全無視,除了自己移植這套算法,好像沒有別的辦法。
FFmpeg上來的dts也不能全信,也許它解析錯了呢。所有要拿到原始數(shù)據,看他是不是真的有問題。
EasyICE可能是目前最強大的TS分析工具。用它分析了這條流,發(fā)現(xiàn)沒有PTS相關錯誤(打臉了),所以這只能是avformat解析的bug。
FFmpeg本身不支持EXT-X-DISCONTINUITY,我們用的是IJK的分支https://github.com/Bilibili/FFmpeg(感謝IJK貢獻了這么優(yōu)秀的代碼)。pts的相關計算代碼如下
if (c->playlists[minplaylist]->finished) {
struct playlist *pls = c->playlists[minplaylist];
int seq_no = pls->cur_seq_no - pls->start_seq_no;
if (seq_no < pls->n_segments && s->streams[pkt->stream_index]) {
struct segment *seg = pls->segments[seq_no];
int64_t pred = av_rescale_q(seg->previous_duration,
AV_TIME_BASE_Q,
s->streams[pkt->stream_index]->time_base);
int64_t max_ts = av_rescale_q(seg->start_time + seg->duration,
AV_TIME_BASE_Q,
s->streams[pkt->stream_index]->time_base);
/* EXTINF duration is not precise enough */
max_ts += 2 * AV_TIME_BASE;
if (s->start_time > 0) {
max_ts += av_rescale_q(s->start_time,
AV_TIME_BASE_Q,
s->streams[pkt->stream_index]->time_base);
}
if (pkt->dts != AV_NOPTS_VALUE && pkt->dts + pred < max_ts) pkt->dts += pred;
if (pkt->pts != AV_NOPTS_VALUE && pkt->pts + pred < max_ts) pkt->pts += pred;
}
}
pts和dts都加上了pred這么一個值,正是pred導致了那個包PTS偏大。要理解為什么加這個值,下面這幅圖可以說明。
左右兩邊是兩個不同的playlist(因為中間有EXT-X-DISCONTINUNITY標記),假設兩個綠色部分是兩個不同的包,p1和p2是PTS。根據HLS的規(guī)范,p1和p2不連續(xù),p2的真實PTS應該是p2+duration1。
IJK的代碼認為,pts + pred 的值比這個segment最大時間?。╩ax_ts),那么就應該加上。而且當一次讀取跨兩個playlist的包時,文件指針移到了后一個playlist,max_ts就成了后一個的最大時間。所以p1被轉換成一個很大的pts1,而p2 < p1,結果后面的pts變小反而被渲染丟掉了。
既然是因為buff跨兩個文件導致,那能不能不讓它跨過去?說起來簡單,實施起來有很多事情要做
- avio讀文件時,如果在讀另一個文件,需要中斷當前read
- 中斷avio不是一個特別好的解決,因為作為一個抽象的io層,如果讀不到所需要數(shù)據,會被認為遇到了EOF
- 不中斷read,需要一個標記,指出某段buffer是某個文件
- avpkt也需要增加標記
- ...
重新思考了一下,其實沒有必要這么復雜,只需要記錄前一個pts,根據pts遞增的特性就能判斷
p0 < p1
p1 > p2
如果前一個pts比當前pts大,那么這個pts就是重新編碼的pts,需要加上前一個的duration。
至此完美解決pts分段問題。