某天測(cè)試反饋击敌,硬解某個(gè)hls流后面幾秒鐘無法播放费封,以為解碼錯(cuò)誤導(dǎo)致焕妙,但實(shí)際解碼正常。經(jīng)過排查(排查的過錯(cuò)有些曲折弓摘,就不細(xì)說了)焚鹊,發(fā)現(xiàn)是解碼出來的PTS異常:某個(gè)包pts異常的大,導(dǎo)致渲染模塊把后面收到的包都拋掉了韧献。
為什么會(huì)突然來一個(gè)特別大的PTS末患?真實(shí)環(huán)境下各種亂七八糟的格式見得多了,搞不好是流本身的問題锤窑,這是我第一時(shí)間能想到的璧针。只是軟解可以正常播放,這促使我進(jìn)一步去軟解一探究竟渊啰,發(fā)現(xiàn)原來decoder修改了pts探橱。ffmpeg 源代碼簡(jiǎn)單分析 : 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里面算法挺簡(jiǎn)單的,就是統(tǒng)計(jì)dts亂序的次數(shù)隧膏,然后確定最終是否用dts代替pts哗讥。
雖然從avformat得到的dts也是錯(cuò)的,decoder內(nèi)部對(duì)dts也做了修正胞枕。修正就細(xì)節(jié)比較復(fù)雜杆煞,而iOS的硬解對(duì)DTS完全無視,除了自己移植這套算法腐泻,好像沒有別的辦法决乎。
FFmpeg上來的dts也不能全信,也許它解析錯(cuò)了呢派桩。所有要拿到原始數(shù)據(jù)瑞驱,看他是不是真的有問題。
EasyICE可能是目前最強(qiáng)大的TS分析工具窄坦。用它分析了這條流,發(fā)現(xiàn)沒有PTS相關(guān)錯(cuò)誤(打臉了)凳寺,所以這只能是avformat解析的bug鸭津。
FFmpeg本身不支持EXT-X-DISCONTINUITY,我們用的是IJK的分支https://github.com/Bilibili/FFmpeg(感謝IJK貢獻(xiàn)了這么優(yōu)秀的代碼)肠缨。pts的相關(guān)計(jì)算代碼如下
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這么一個(gè)值逆趋,正是pred導(dǎo)致了那個(gè)包PTS偏大。要理解為什么加這個(gè)值晒奕,下面這幅圖可以說明闻书。
左右兩邊是兩個(gè)不同的playlist(因?yàn)橹虚g有EXT-X-DISCONTINUNITY標(biāo)記),假設(shè)兩個(gè)綠色部分是兩個(gè)不同的包脑慧,p1和p2是PTS魄眉。根據(jù)HLS的規(guī)范,p1和p2不連續(xù)闷袒,p2的真實(shí)PTS應(yīng)該是p2+duration1坑律。
IJK的代碼認(rèn)為,pts + pred 的值比這個(gè)segment最大時(shí)間心抑琛(max_ts)晃择,那么就應(yīng)該加上。而且當(dāng)一次讀取跨兩個(gè)playlist的包時(shí)也物,文件指針移到了后一個(gè)playlist宫屠,max_ts就成了后一個(gè)的最大時(shí)間。所以p1被轉(zhuǎn)換成一個(gè)很大的pts1滑蚯,而p2 < p1浪蹂,結(jié)果后面的pts變小反而被渲染丟掉了抵栈。
既然是因?yàn)閎uff跨兩個(gè)文件導(dǎo)致,那能不能不讓它跨過去乌逐?說起來簡(jiǎn)單竭讳,實(shí)施起來有很多事情要做
- avio讀文件時(shí),如果在讀另一個(gè)文件浙踢,需要中斷當(dāng)前read
- 中斷avio不是一個(gè)特別好的解決绢慢,因?yàn)樽鳛橐粋€(gè)抽象的io層,如果讀不到所需要數(shù)據(jù)洛波,會(huì)被認(rèn)為遇到了EOF
- 不中斷read胰舆,需要一個(gè)標(biāo)記,指出某段buffer是某個(gè)文件
- avpkt也需要增加標(biāo)記
- ...
重新思考了一下蹬挤,其實(shí)沒有必要這么復(fù)雜缚窿,只需要記錄前一個(gè)pts,根據(jù)pts遞增的特性就能判斷
p0 < p1
p1 > p2
如果前一個(gè)pts比當(dāng)前pts大焰扳,那么這個(gè)pts就是重新編碼的pts倦零,需要加上前一個(gè)的duration。
至此完美解決pts分段問題吨悍。