本文首發(fā)公眾號 音視頻開發(fā)進階 料扰,鏈接:https://mp.weixin.qq.com/s/qPBSe0itF40ek1laIZRQnw
昨天周六,群里面還有人在技術交流1翰洹晒杈!。
默默吐槽一下:這些人真卷啊孔厉,大周末還搞技術拯钻,是游戲不好玩還是電影不好看。
一開始是討論剪映的 100 變速是如何實現撰豺,群主作為相關人士肯定就不方便透露這些了说庭,不過也有其他大佬給出了思路。
討論焦點還是圍繞如何丟幀展開的郑趁。
百倍變速,比如正常速度下一幀是播放第 1s 時刻的內容姿搜,而變速后要播放 100s 時刻了寡润。
此時的邏輯有以下幾種情況:
- 如果下一個播放時刻要超過目前 GOP 大小了,那么就及時 seek 到離目標 pts 最近的關鍵幀舅柜,比如從 1s 變速后到了 100s 梭纹,那就 seek 到第 98s 。
- 如果下一個播放時刻在同一 GOP 內了致份,那就繼續(xù)往下解碼变抽,判斷解碼后的幀 pts 不需要顯示就直接丟棄再接著往后解 ,直到接近了目標時間點就顯示氮块。
- seek 后的時間點沒達到目標時間點的情況绍载,需要繼續(xù)解碼的可以重復第二步。
以上是針對群內大佬的總結滔蝉,拿著小本本趕緊記下來击儡。
此時,還有大佬對解碼丟幀給出了其他意見:
主要是針對非參考幀的丟幀處理蝠引,也是文章的重點內容阳谍。
當我們通過 av_read_frame 得到一個 AVPacket 之后,可以判斷它的 nal_ref_idc 值來決定是否要丟棄螃概。
如果為 0 矫夯,說明其他幀不需要參考該幀,可以直接丟棄不發(fā)送給解碼器吊洼,而不是解碼后再丟幀训貌。
如果你不清楚 nal_ref_idc 是什么意思 ? 那么可以了解一下 H.264 碼流 NALU 的概念融蹂。
H.264 碼流傳輸時以 NALU 的形式進行旺订,NALU 主要由一個字節(jié)的 HAL Header 和 RBSP 兩部分組成弄企。
HAL Header 的組成形式如下圖所示:
HAL Header 的計算如下:
forbidden_zero_bit(1bit) + nal_ref_idc(2bit) + nal_unit_type(5bit)
nal_unit_type 不同的值代表不同類型的幀,解析 AVPacket 完全可以得到如上的信息区拳,后面在公眾號音視頻開發(fā)進階繼續(xù)更新文章詳解如何計算拘领。
所以,在解碼時完全是可以丟棄這些非參考幀的樱调,放心大膽地操作吧约素。
而且丟非參考幀的操作也是經過了產品億級檢測的,這一點我確實可以作證0柿琛Jチ浴!
FFmpeg 中的丟幀
以上的丟幀邏輯是根據 H.264 規(guī)范來的乞而,那么在 FFmpeg 的源碼中有沒有針對這一邏輯做處理呢送悔?
那必然是有的啊Wδ!G菲 !
如果仔細看 ffplay 的源碼屋灌,在源碼中有如下的調用方式:
/* this thread gets the stream from the disk or the network */
static int read_thread(void *arg)
{
// 省略部分代碼
for (i = 0; i < ic->nb_streams; i++) {
AVStream *st = ic->streams[i];
enum AVMediaType type = st->codecpar->codec_type;
// AVDISCARD_ALL 拋棄所有的幀
st->discard = AVDISCARD_ALL;
if (type >= 0 && wanted_stream_spec[type] && st_index[type] == -1)
if (avformat_match_stream_specifier(ic, st, wanted_stream_spec[type]) > 0)
st_index[type] = i;
}
// 省略部分代碼
if (!video_disable)
st_index[AVMEDIA_TYPE_VIDEO] =
av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO,
st_index[AVMEDIA_TYPE_VIDEO], -1, NULL, 0);
if (!audio_disable)
st_index[AVMEDIA_TYPE_AUDIO] =
av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO,
st_index[AVMEDIA_TYPE_AUDIO],
st_index[AVMEDIA_TYPE_VIDEO],
NULL, 0);
/* open the streams */
if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {
// 開啟解碼線程
stream_component_open(is, st_index[AVMEDIA_TYPE_AUDIO]);
}
ret = -1;
if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
// 開啟解碼線程
ret = stream_component_open(is, st_index[AVMEDIA_TYPE_VIDEO]);
}
// 省略代碼
}
read_thread 方法運行在單獨線程上洁段,該方法首先進行解封裝操作,然后開啟一個線程進行解碼共郭,接下來調用 av_read_frame 方法讀取 AVPacket 放到隊列中供解碼線程使用祠丝。
在 av_find_best_stream 方法之前先將 discard 置為 AVDISCARD_ALL ,過濾掉 AVStream 中的數據除嘹,接下來就是 stream_component_open 操作写半。
/* open a given stream. Return 0 if OK */
static int stream_component_open(VideoState *is, int stream_index)
{
// 省略部分代碼
// AVDISCARD_DEFAULT 默認模式,過濾無效數據
ic->streams[stream_index]->discard = AVDISCARD_DEFAULT;
switch (avctx->codec_type) {
case AVMEDIA_TYPE_AUDIO:
// 省略部分代碼
if ((ret = decoder_start(&is->auddec, audio_thread, "audio_decoder", is)) < 0)
goto out;
// 省略部分代碼
case AVMEDIA_TYPE_VIDEO:
// 省略部分代碼
if ((ret = decoder_start(&is->viddec, video_thread, "video_decoder", is)) < 0)
goto out;
// 省略部分代碼
}
stream_component_open 方法又將 discard 置為了 AVDISCARD_DEFAULT 憾赁,僅過濾掉無效數據污朽。
繼續(xù)跟進這個 discard 字段,就會有新發(fā)現了龙考!
關于 discard 的所有類型值和使用方式蟆肆,FFmpeg 中有如下定義:
/**
* @ingroup lavc_decoding
*/
enum AVDiscard{
/* We leave some space between them for extensions (drop some
* keyframes for intra-only or drop just some bidir frames). */
// 不拋棄,不放棄任何數據
AVDISCARD_NONE =-16, ///< discard nothing
// 丟掉無用的數據晦款,比如 size 為 0 這種
AVDISCARD_DEFAULT = 0, ///< discard useless packets like 0 size packets in avi
// 丟掉所有的非參考幀
AVDISCARD_NONREF = 8, ///< discard all non reference
// 丟掉所有的雙向幀
AVDISCARD_BIDIR = 16, ///< discard all bidirectional frames
// 丟掉所有的非內幀
AVDISCARD_NONINTRA= 24, ///< discard all non intra frames
// 丟掉所有的非關鍵幀
AVDISCARD_NONKEY = 32, ///< discard all frames except keyframes
// 丟掉所有幀
AVDISCARD_ALL = 48, ///< discard all
};
所以炎功,完全可以使用 discard 字段來標識解碼時丟棄哪些幀。
另外缓溅,在 avcodec_send_packet 方法源碼注釋中也提示了可以通過 AVCodecContext.skip_frame 字段來決定丟棄哪些幀蛇损。
/**
* Internally, this call will copy relevant AVCodecContext fields, which can
* influence decoding per-packet, and apply them when the packet is actually
* decoded. (For example AVCodecContext.skip_frame, which might direct the
* decoder to drop the frame contained by the packet sent with this function.)
* 省略部分注釋
*/
int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);
摘錄了部分注釋內容,寫的就很清楚了。
所以淤齐,在解碼時也可以不用自己解析 AVPacket 的 nal_ref_idc 字段值股囊,直接通過 AVCodecContext.skip_frame 實現同樣的目的。
親測有效更啄,過濾非關鍵幀之后稚疹,解碼出來的全是關鍵幀了。
以上祭务,就是關于丟幀的一些分享内狗,技術交流探討歡迎加我微信 ezglumes 交流!R遄丁柳沙!