?1.問題描述1.1 背景

之前基于做二次開發(fā),完成常見的視頻處理功能,并用命令行做兜底。在此基礎(chǔ)上,還做一個(gè)轉(zhuǎn)碼接入和調(diào)度系統(tǒng)對外提供服務(wù)。有個(gè)功能需要是這樣的:快速從指定的視頻中裁剪某一時(shí)間范圍的子視頻, 兩個(gè)要求:1. 要快,不能像轉(zhuǎn)碼一樣耗時(shí);2.要精確,剪輯的時(shí)候能指定從哪一秒開始,到哪一秒結(jié)束。

1.2 難點(diǎn)

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

2.原因分析

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

視頻剪輯項(xiàng)目經(jīng)歷_視頻剪輯項(xiàng)目經(jīng)歷怎么寫_視頻剪輯項(xiàng)目

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

3.解決方案

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

視頻剪輯項(xiàng)目_視頻剪輯項(xiàng)目經(jīng)歷_視頻剪輯項(xiàng)目經(jīng)歷怎么寫

當(dāng)然這里面還是有一些坑的,下面開始填坑。

3.1 拼接

源視頻可能會(huì)驚訝:我憑本事編的碼,為什么你直接拷貝就能解碼?一般來說解碼依賴于SPS和PPS,而源視頻與目標(biāo)視頻的SPS和PPS會(huì)有所不同,因此直接拷貝是不能正確解碼的。對于mp4文件,SPS和PPS一般是放到文件頭。一個(gè)文件只能有一個(gè)文件頭,也就不能存放兩個(gè)不同的SPS和PPS。為了能正確解碼目標(biāo)視頻必須得有源視頻的SPS和PPS。不能放文件頭的話,那能放哪里?能不能放到拷貝的幀的前面呢?如何放?一籌莫展、無處下手,直到有一天突然想起之前為了填一個(gè)坑,追蹤到的實(shí)現(xiàn),它的作用就是將SPS和PPS拷貝到幀(準(zhǔn)確來說應(yīng)該是)的前面。來!溫習(xí)一下的具體實(shí)現(xiàn):在所有前面增加或者,在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);
}

注意:編解碼第一個(gè)GOP和原始視頻后續(xù)GOP拼接時(shí)的時(shí)間戳要小心處理,不然視頻播放時(shí)可能會(huì)出現(xiàn)抖動(dòng)現(xiàn)象。

3.2 花屏

以為就完了嗎?沒有!!你會(huì)發(fā)現(xiàn)有些視頻會(huì)在最后一秒出現(xiàn)花屏。。。。

視頻剪輯項(xiàng)目_視頻剪輯項(xiàng)目經(jīng)歷_視頻剪輯項(xiàng)目經(jīng)歷怎么寫

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

4.總結(jié)

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