?1.問(wèn)題描述1.1 背景

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

1.2 難點(diǎn)

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

2.原因分析

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

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

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

3.解決方案

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

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

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

3.1 拼接

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

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 花屏

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

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

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

4.總結(jié)

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