?1.問題描述1.1 背景

之前基于做二次開發,完成常見的視頻處理功能,并用命令行做兜底。在此基礎上,還做一個轉碼接入和調度系統對外提供服務。有個功能需要是這樣的:快速從指定的視頻中裁剪某一時間范圍的子視頻, 兩個要求:1. 要快,不能像轉碼一樣耗時;2.要精確,剪輯的時候能指定從哪一秒開始,到哪一秒結束。

1.2 難點

用很容易從一個長視頻剪輯出一段小視頻。比如命令 -i .mp4 -ss 00:10:03 -t 00:03:00 - copy - copy .mp4就是從.mp4的第10分鐘03秒開始剪輯出一個3分鐘的視頻并且保存為.mp4文件。參數- copy - copy就是直接拷貝原始視頻的音視頻流,不進行編解碼。雖然上面的方法很方便,但有一個致命的缺陷:畫面在一開始會卡住(但聲音一直是正常的),幾秒后畫面才正常滾動。下面視頻是一個例子。

2.原因分析

究其原因,剪輯的開始時間落在視頻GOP的中間位置而不是第一個I幀。稍微了解過視頻編碼的同學應該都聽過I、B、P幀。簡單來說,I幀是一張完整的圖像,P幀則根據I幀做差分編碼,B幀根據前后的I、P、B幀作差分編碼。也就是說I幀具有完整的內容,而P和B幀不具有,所以如果缺少I幀,那么P和B幀是不能正常解碼的。通常來說,一個GOP里面第一幀是I幀,后面是若干個P和B幀。一個GOP長達10秒都是有可能的。下圖是一個真實視頻的I、B、P幀信息圖,紅色的表示I幀,可以看到兩個I幀相隔深遠(實際是隔了10秒)。

視頻剪輯項目經歷_視頻剪輯項目經歷怎么寫_視頻剪輯項目

從上面分析可知:剪輯的開始時間很大可能不是落在I幀,由于缺少I幀會使得后面的P和B幀無法解碼導致畫面卡住。上面的分析都是基于不編解碼的直接拷貝視頻內容的,如果考慮先解碼成一張張的圖像,然后再對符合時間要求的圖像編碼,那么剪輯時間可以做到非常精準。但這樣做的就是耗時過長:需求花費大量的CPU完成編解碼操作。

3.解決方案

解決的辦法還是有的:對前面第一個符合時間要求的GOP編解碼,而之后的GOP內容則直接拷貝到目標視頻。一來,第一個GOP的幀由于是重新編碼所以會重新分配I幀從而能播放,二來,之后的GOP內容是直接拷貝的所以基本不消耗CPU,性能杠桿的。如下圖所示:

視頻剪輯項目_視頻剪輯項目經歷_視頻剪輯項目經歷怎么寫

當然這里面還是有一些坑的,下面開始填坑。

3.1 拼接

源視頻可能會驚訝:我憑本事編的碼,為什么你直接拷貝就能解碼?一般來說解碼依賴于SPS和PPS,而源視頻與目標視頻的SPS和PPS會有所不同,因此直接拷貝是不能正確解碼的。對于mp4文件,SPS和PPS一般是放到文件頭。一個文件只能有一個文件頭,也就不能存放兩個不同的SPS和PPS。為了能正確解碼目標視頻必須得有源視頻的SPS和PPS。不能放文件頭的話,那能放哪里?能不能放到拷貝的幀的前面呢?如何放?一籌莫展、無處下手,直到有一天突然想起之前為了填一個坑,追蹤到的實現,它的作用就是將SPS和PPS拷貝到幀(準確來說應該是)的前面。來!溫習一下的具體實現:在所有前面增加或者,在I幀的前面插入SPS和PPS。也就是通過就能把解碼所需的SPS和PPS正確插入到視頻中。使用起來也比較簡單,代碼如下:

AVBSFContext* initBSF(const std::string &filter_name, const AVCodecParameters *codec_par, AVRational tb)
{
    const AVBitStreamFilter *filter = av_bsf_get_by_name(m_filter_name.c_str());
?
    AVBSFContext *bsf_ctx = nullptr;
    av_bsf_alloc(filter, &bsf_ctx);
?
    avcodec_parameters_copy(bsf_ctx->par_in, codec_par);
    bsf_ctx->time_base_in = tb;
?
    av_bsf_init(bsf_ctx);
    return bsf_ctx;
}
?
AVPacket* feedPacket(AVBSFContext *bsf_ctx, AVPacket &packet)
{
    av_bsf_send_packet(bsf_ctx, packet);
?
    AVPacket *dst_packet = av_packet_alloc();
    av_bsf_receive_packet(bsf_ctx, dst_packet);
?
    return dst_packet;
}
?
void test()
{
    AVBSFContext *bsf_ctx = initBSF("h264_mp4toannexb", video_stream->codecpar, video_stream->time_base);
    AVPacket *packet = readVideoPacket();
    AVPacket *dst_packet = feedPacket(bsf_ctx, packet);
}

注意:編解碼第一個GOP和原始視頻后續GOP拼接時的時間戳要小心處理,不然視頻播放時可能會出現抖動現象。

3.2 花屏

以為就完了嗎?沒有!!你會發現有些視頻會在最后一秒出現花屏。。。。

視頻剪輯項目_視頻剪輯項目經歷_視頻剪輯項目經歷怎么寫

出現花屏的原因其實也不難猜到:最后一幀是B幀。由于不是所有剪輯的視頻最后一幀都是B幀,所以花屏也不是必現的。知道是B幀引起的,那解決方案也就明確了:保證最后一幀是P幀。即使時間上稍微超一點(音頻流也應該跟著視頻流稍微超一下時間)。不過呢,由于不能直接從判斷一個幀是否為P幀,所以最后一個GOP也得解碼(無需編碼)。記錄超出時間范圍后的第一個P幀的pts,后面拷貝GOP的時候,拷貝到這個pts就可以停止了。

4.總結

起初覺得問題很難解決,畢竟命令行都裁剪出來的都有問題。而萬變不離其宗,從問題的原因出發,一步步尋找解決方案,并將一路上碰到的問題逐一擊破。記住,明白原理才能解決問題。