1. 程式人生 > >ffmpeg編解碼詳細過程

ffmpeg編解碼詳細過程

1. 註冊所有容器格式和CODEC:av_register_all()

2. 開啟檔案:av_open_input_file()

3. 從檔案中提取流資訊:av_find_stream_info()

4. 窮舉所有的流,查詢其中種類為CODEC_TYPE_VIDEO

5. 查詢對應的解碼器:avcodec_find_decoder()

6. 開啟編解碼器:avcodec_open()

7. 為解碼幀分配記憶體:avcodec_alloc_frame()

8. 不停地從碼流中提取出幀資料:av_read_frame()

9. 判斷幀的型別,對於視訊幀呼叫:avcodec_decode_video()

10. 解碼完後,釋放解碼器:avcodec_close()

11. 關閉輸入檔案:av_close_input_file()

首先第一件事情就是開一個視訊檔案並從中得到流。我們要做的第一件事情就是使用av_register_all();來初始化libavformat/libavcodec:

這一步註冊庫中含有的所有可用的檔案格式和編碼器,這樣當開啟一個檔案時,它們才能夠自動選擇相應的檔案格式和編碼器。av_register_all()只需呼叫一次,所以,要放在初始化程式碼中。也可以僅僅註冊個人的檔案格式和編碼。

下一步,開啟檔案:

AVFormatContext *pFormatCtx;
const char     *filename="myvideo.mpg";
av_open_input_file(&pFormatCtx, filename, NULL, 0, NULL);  // 開啟視訊檔案
最後三個引數描述了檔案格式,緩衝區大小(size)和格式引數;我們通過簡單地指明NULL或0告訴 libavformat 去自動探測檔案格式並且使用預設的緩衝區大小。這裡的格式引數指的是視訊輸出引數,比如寬高的座標。

下一步,我們需要取出包含在檔案中的流資訊:
av_find_stream_info(pFormatCtx); // 取出流資訊

AVFormatContext 結構體

dump_format(pFormatCtx, 0, filename, false);//我們可以使用這個函式把獲取到得引數全部輸出。

for(i=0; i<pFormatCtx->nb_streams; i++) //區分視訊流和音訊流
if(pFormatCtx->streams->codec.codec_type==CODEC_TYPE_VIDEO) //找到視訊流,這裡也可以換成音訊
  {
    videoStream=i;
    break;
  }

接下來就需要尋找解碼器

AVCodec *pCodec;
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);

avcodec_open(pCodecCtx, pCodec); // 開啟解碼器
給視訊幀分配空間以便儲存解碼後的圖片:

AVFrame *pFrame;
pFrame=avcodec_alloc_frame();

/////////////////////////////////////////開始解碼///////////////////////////////////////////

第一步當然是讀資料:

我們將要做的是通過讀取包來讀取整個視訊流,然後把它解碼成幀,最後轉換格式並且儲存。

while(av_read_frame(pFormatCtx, &packet)>=0) {  //讀資料

if(packet.stream_index==videoStream){  //判斷是否視訊流

avcodec_decode_video(pCodecCtx,pFrame, &frameFinished,

packet.data, packet.size);  //解碼

if(frameFinished) {

img_convert((AVPicture *)pFrameRGB, PIX_FMT_RGB24,(AVPicture*)pFrame, pCodecCtx->pix_fmt, pCodecCtx->width,pCodecCtx->height);//轉換  }

SaveFrame(pFrameRGB, pCodecCtx->width,pCodecCtx->height, i); //儲存資料

av_free_packet(&packet);  //釋放

av_read_frame()讀取一個包並且把它儲存到AVPacket結構體中。這些資料可以在後面通過av_free_packet()來釋放。函式avcodec_decode_video()把包轉換為幀。然而當解碼一個包的時候,我們可能沒有得到我們需要的關於幀的資訊。因此,當我們得到下一幀的時候,avcodec_decode_video()為我們設定了幀結束標誌frameFinished。最後,我們使用 img_convert()函式來把幀從原始格式(pCodecCtx->pix_fmt)轉換成為RGB格式。要記住,你可以把一個 AVFrame結構體的指標轉換為AVPicture結構體的指標。最後,我們把幀和高度寬度資訊傳遞給我們的SaveFrame函式。

到此解碼完畢,顯示過程使用SDL完成考慮到我們以後會使用firmware進行顯示操作,SDL忽略不講。

音視訊同步

DTS(解碼時間戳)和PTS(顯示時間戳)

當我們呼叫av_read_frame()得到一個包的時候,PTS和DTS的資訊也會儲存在包中。但是我們真正想要的PTS是我們剛剛解碼出來的原始幀的PTS,這樣我們才能知道什麼時候來顯示它。然而,我們從avcodec_decode_video()函式中得到的幀只是一個AVFrame,其中並沒有包含有用的PTS值(注意:AVFrame並沒有包含時間戳資訊,但當我們等到幀的時候並不是我們想要的樣子)。。我們儲存一幀的第一個包的PTS:這將作為整個這一幀的PTS。我們可以通過函式avcodec_decode_video()來計算出哪個包是一幀的第一個包。怎樣實現呢?任何時候當一個包開始一幀的時候,avcodec_decode_video()將呼叫一個函式來為一幀申請一個緩衝。當然,ffmpeg允許我們重新定義那個分配記憶體的函式。計算前一幀和現在這一幀的時間戳來預測出下一個時間戳的時間。同時,我們需要同步視訊到音訊。我們將設定一個音訊時間audioclock;一個內部值記錄了我們正在播放的音訊的位置。就像從任意的mp3播放器中讀出來的數字一樣。既然我們把視訊同步到音訊,視訊執行緒使用這個值來算出是否太快還是太慢。

用FFMPEG SDK進行視訊轉碼壓縮時解決音視訊不同步問題的方法(轉)

ffmpeg 2010-07-21 19:54:16 閱讀163 評論0

用FFMPEG SDK進行視訊轉碼壓縮的時候,轉碼成功後去看視訊的內容,發現音視訊是不同步的。這個的確是一個惱火的事情。我在用FFMPEG SDK做h264格式的FLV檔案編碼Filter的時候就碰到了這個問題。

經過研究發現,FFMPEG SDK寫入視訊的時候有兩個地方用來控制寫入的時間戳,一個是AvPacket, 一個是AvFrame。在呼叫avcodec_encode_video的時候需要傳入AvFrame的物件指標,也就是傳入一幀未壓縮的視訊進行壓縮處理,AvFrame包含一個pts的引數,這個引數就是當前幀將來在還原播放的時候的時間戳。而AvPacket裡面也有pts,還有dts。說起這個就必須要說明一下 I,P,B三種視訊壓縮幀。I幀就是關鍵幀,不依賴於其他視訊幀,P幀是向前預測的幀,只依賴於前面的視訊幀,而B幀是雙向預測視訊幀,依賴於前後視訊幀。由於B幀的存在,因為它是雙向的,必須知道前面的視訊幀和後面的視訊幀的詳細內容後,才能知道本B幀最終該呈現什麼影象。而pts和dts兩個引數就是用來控制視訊幀的顯示和解碼的順序。

pts就是幀顯示的順序。

dts就是幀被讀取進行解碼的順序。

如果沒有B幀存在,dts和pts是相同的。反之,則是不相同的。關於這個的詳細介紹可以參考一下mpeg的原理。

再說說AvPacket中包含的pts和dts兩個到底該設定什麼值?

pts和dts需要設定的就是視訊幀解碼和顯示的順序。每增加一幀就加一,並不是播放視訊的時間戳。

但是實踐證明經過rmvb解碼的視訊有時候並不是固定幀率的,而是變幀率的,這樣,如果每壓縮一幀,pts和dts加一的方案為導致音視訊不同步。

那怎麼來解決音視訊同步的問題呢?

請看如下程式碼段。

lTimeStamp 是通過directshow 獲取的當前的視訊幀的時間戳。

m_llframe_index為當前已經經過壓縮處理的幀的數量。

首先av_rescale計算得到當前壓縮處理已經需要處理什麼時間戳的視訊幀,如果該時間戳尚未到達directshow當前提供的視訊幀的時間戳,則將該幀丟棄掉。

否則進行壓縮操作。並設定AVPacket的pts和dts。這裡假設B幀不存在。

因為在將來播放的時候視訊以我們設定的固定播放幀率進行播放,所以需要根據設定的播放幀率計算得到的視訊幀時間戳和directshow提供的當前視訊幀的時間戳進行比較,設定是否需要進行實施延緩播放的策略。如果需要延緩播放,則將pts增加步長2,否則以普通速度播放,則設定為1.dts與之相同。
__int64 x =av_rescale(m_llframe_index,AV_TIME_BASE*(int64_t)c->time_base.num,c->time_base.den);

if( x > lTimeStamp )
{
return TRUE;
}
m_pVideoFrame2->pts = lTimeStamp;
m_pVideoFrame2->pict_type = 0;

int out_size = avcodec_encode_video( c, m_pvideo_outbuf, video_outbuf_size,m_pVideoFrame2 );

if (out_size > 0)
{
AVPacket pkt;
av_init_packet(&pkt);

if( x > lTimeStamp )
{
pkt.pts = pkt.dts = m_llframe_index;
pkt.duration = 0;
}
else
{
pkt.duration = (lTimeStamp - x)*c->time_base.den/1000000 + 1;
pkt.pts = m_llframe_index;
pkt.dts = pkt.pts;
m_llframe_index += pkt.duration;
}

//pkt.pts = lTimeStamp * (__int64)frame_rate.den / 1000;
if( c->coded_frame && c->coded_frame->key_frame )
{
pkt.flags |= PKT_FLAG_KEY;
}

pkt.stream_index= m_pVideoStream->index;
pkt.data= m_pvideo_outbuf;
pkt.size= out_size;


ret = av_interleaved_write_frame( m_pAvFormatContext, &pkt );
}
else
{
ret = 0;
}

請問avcodec_decode_video解碼的幀為什麼後面的比前面的pts小呢?

請問如下程式碼:
while( av_read_frame(pFormatCtxSource,&packet)>=0 )
{
  if( packet.stream_index==videoStream )
  {
    int out_size = avcodec_decode_video(pCodecCtxSource,pFrameSource, &bFrameFinished, packet.data, packet.size); // Decode fromsource frame

    if( bFrameFinished )
    {
        pFrameSource->pts =av_rescale_q(packet.pts, pCodecCtxSource->time_base,pStCodec->time_base);
        int out_size =avcodec_encode_video(pStCodec, video_buffer, 200000, pFrameSource); // Encodeto output
        if( out_size>0 )
        {
          // ...
        }
    }
  }

  av_free_packet(&packet);

}

在我Decode的時候,第一幀得到的 pFrameSource->pts 是96,再解第二幀的時候,pFrameSource->pts 計算完後就成了80幾,後幾幀也是比96小,過一會又會解出來一個100多的,接下來又是比100多小的,這是為什麼?在Encode的時候,先 Encode一個pts=96的,再去Encode比96小的幀就返回-1了,直到找到一個比96大的。

另外,我計算pts的方法正確嗎?

答覆:

Because you have B - Frame

for example:

the Inputsequence for video encoder
1 2 3   4   5   6   7
I   B   B   P B   B   I

Let's take1,2,3.. as PTS for simplification

the out sequencefor video encoder ( this equals the decoder sequence)
1 4 2   3   7   5   6
I P   B   B   I   B   B

you will get aPTS sequence as following:

1 4 2 3 7 5 6

7 5 6sequence will be same as your question

問:

哦,那是不是我的pts不能這麼算呢?而是要每次+1,對嗎?那麼,packet中的pts和dts要用在什麼地方呢?我這樣按儲存順序進行解碼的話,顯示之前是不是要自己進行快取呢?謝謝!

另外,還有個問題,既然解碼的時候,不一定是按照pts遞增的順序得到的解碼後的畫面,那我在編碼影象的時候,是應該按照解碼出來的幀順序進行編碼嗎?還是把幀先快取起來,最後嚴格接照影象的顯示順序來編碼呢?用程式碼來表示,就是:
方法一:
while(av_read_frame )
{  
  解碼;
  pts+1;  
  編碼;
  輸出;
}

方法二:
while(av_read_frame )
{
  解碼;
  if( pts<previous )
  {
  快取;
  }
  else
  {
  編碼快取的幀並寫入檔案;
  }
}

這兩個方法,哪個是正確的呢?因為我看到網上的程式碼都用的是方法一,但是我覺得方法二是對的呀?

答:

the output of decoderis the right order for display because I/P frames will be cacheduntil next I/P

理解:

Decoder 後output的pts 是按正常的順序,即顯示的順序輸出的,如果有B幀,decoder會快取。

但encoder後,輸出的是按dts輸出的。

Pts,dts並不是時間戳,而更應該理解為frame的順序序列號。由於每幀frame的幀率並不一定是一致的,可能會變化的。轉換為時間戳的話,應該是(pts*幀率)。為加深理解

可以將pts比做是第pts幀frame,假設每幀的幀率不變的話,則顯示的時間戳為(pts*幀率),如果考慮幀率變化的,則要想辦法將(pts*當前的幀率)累加到後面。

在tutorial5中在decode 下增加trace後列印情況:

len1 = avcodec_decode_video(is->video_st->codec,pFrame, &frameFinished,

packet->data,packet->size);

printf("-----------------------------------------------------------------------------n");

printf("avcodec_decode_videopacket->pts:%x,packet->dts:%xn",packet->pts,packet->dts);

printf("avcodec_decode_videopFrame->pkt_pts:%x,pFrame->pkt_dts:%x,pFrame->pts:%xn",pFrame->pkt_pts,pFrame->pkt_dts,pFrame->pts);

if(pFrame->opaque)

printf("avcodec_decode_video*(uint64_t *)pFrame->opaque:%xn",*(uint64_t *)pFrame->opaque);

其中播一個mp4檔案的列印情況:

-----------------------------------------------------------------------------

avcodec_decode_video packet->pts:1ae,packet->dts:0

avcodec_decode_videopFrame->pkt_pts:0,pFrame->pkt_dts:80000000,pFrame->pts:0

avcodec_decode_video *(uint64_t *)pFrame->opaque:1ae

-----------------------------------------------------------------------------

avcodec_decode_video packet->pts:1af,packet->dts:0

avcodec_decode_videopFrame->pkt_pts:0,pFrame->pkt_dts:80000000,pFrame->pts:0

avcodec_decode_video *(uint64_t *)pFrame->opaque:1af

-----------------------------------------------------------------------------

avcodec_decode_video packet->pts:24c,packet->dts:0

avcodec_decode_videopFrame->pkt_pts:0,pFrame->pkt_dts:80000000,pFrame->pts:0

avcodec_decode_video *(uint64_t *)pFrame->opaque:1ac

-----------------------------------------------------------------------------

avcodec_decode_video packet->pts:24d,packet->dts:0

avcodec_decode_videopFrame->pkt_pts:0,pFrame->pkt_dts:80000000,pFrame->pts:0

avcodec_decode_video *(uint64_t *)pFrame->opaque:24d

-----------------------------------------------------------------------------

avcodec_decode_video packet->pts:24e,packet->dts:0

avcodec_decode_videopFrame->pkt_pts:0,pFrame->pkt_dts:80000000,pFrame->pts:0

avcodec_decode_video*(uint64_t *)pFrame->opaque:24e

以下為播放rm檔案的情況:

-----------------------------------------------------------------------------

avcodec_decode_videopacket->pts:1831b,packet->dts:0

avcodec_decode_videopFrame->pkt_pts:0,pFrame->pkt_dts:80000000,pFrame->pts:0

avcodec_decode_video *(uint64_t *)pFrame->opaque:1831b

-----------------------------------------------------------------------------

avcodec_decode_videopacket->pts:18704,packet->dts:0

avcodec_decode_videopFrame->pkt_pts:0,pFrame->pkt_dts:80000000,pFrame->pts:0

avcodec_decode_video *(uint64_t *)pFrame->opaque:18704

-----------------------------------------------------------------------------

avcodec_decode_videopacket->pts:18aed,packet->dts:0

avcodec_decode_videopFrame->pkt_pts:0,pFrame->pkt_dts:80000000,pFrame->pts:0

avcodec_decode_video *(uint64_t *)pFrame->opaque:18aed

-----------------------------------------------------------------------------

avcodec_decode_videopacket->pts:18ed6,packet->dts:0

avcodec_decode_videopFrame->pkt_pts:0,pFrame->pkt_dts:80000000,pFrame->pts:0

avcodec_decode_video *(uint64_t *)pFrame->opaque:18ed6

-----------------------------------------------------------------------------

avcodec_decode_videopacket->pts:192bf,packet->dts:0

avcodec_decode_videopFrame->pkt_pts:0,pFrame->pkt_dts:80000000,pFrame->pts:0

avcodec_decode_video *(uint64_t *)pFrame->opaque:192bf

-----------------------------------------------------------------------------

avcodec_decode_videopacket->pts:196a8,packet->dts:0

avcodec_decode_videopFrame->pkt_pts:0,pFrame->pkt_dts:80000000,pFrame->pts:0

avcodec_decode_video *(uint64_t *)pFrame->opaque:196a8

可以看出有的pts是+1 累加,有的是加了很多,但都是按順序累加的。當傳人decoder前的packet有pts時,則decoder後獲取的frame將會賦值packet 的pts;當傳人的packet 只是一幀的部分資料或是B幀,由於decoder出來的frame要按正常的pts順序輸出,有可能decoder不會獲取到frame ,或decoder內部會快取也不會輸出frame,即frame的pts會為空。Frame pts(即opaque)為空的話則會看frame->dts,dts都沒有的話才認為frame->pts為0.

對於:

pts *= av_q2d(is->video_st->time_base);////即pts*幀率

// Did we get avideo frame?

if(frameFinished) {

pts =synchronize_video(is, pFrame, pts);

///// synchronize_video考慮了3中情況:

1. pts拿到的話就用該pts

2. pts沒有拿到的話就用前一幀的pts時間

3. 如果該幀要重複顯示,則將顯示的數量*幀率,再加到前面的pts中。

if(queue_picture(is, pFrame, pts) < 0) {/////傳人decoder後的幀佇列中,以便後續去獲取show。

static double synchronize_video(VideoState *is, AVFrame*src_frame, double pts) {

doubleframe_delay;

if(pts != 0) {

  is->video_clock = pts;

} else {

pts =is->video_clock;

}

/////很關鍵:前面傳進來的pts已經是時間戳了,是當前frame開始播放的時間戳,

/////下面frame_delay是該幀顯示完將要花費的時間,(pts+frame_delay)也即是/////預測的下一幀將要播放的時間戳。

frame_delay =av_q2d(is->video_st->codec->time_base);

//////重複多幀的話要累加上

frame_delay +=src_frame->repeat_pict * (frame_delay * 0.5);

is->video_clock += frame_delay;

return pts;/////此時返回的值即為下一幀將要開始顯示的時間戳。

}

///////開定時器去顯示幀佇列中的已經decode過的資料,按前面的分析我們已經知道幀佇列中的資料已經是按pts順序插入到佇列中的。 Timer的作用就是有幀率不一致及重複幀的情況造成時間戳不是線性的,有快有慢,從而tutorial5才有timer的方式來播放:追趕

以下是一個網友很直觀淺顯的例子解釋:

ccq(183892517) 17:05:21 if(packet->dts ==AV_NOPTS_VALUE 是不是就是沒有獲取到dts的情況?

David Cen(3727567) 17:06:44 就是有一把尺子一隻螞蟻跟著一個標杆走 David Cen(3727567) 17:06:58 標杆是勻速的螞蟻或快或慢 DavidCen(3727567) 17:07:18 慢了你就抽它 讓他跑起來快了就拽它 David Cen(3727567) 17:07:38 這樣音(標杆)視訊(螞蟻)就能同步了 DavidCen(3727567) 17:08:00 這裡最大的問題就是音訊是勻速的視訊是非線性的

另外:此時vp–>pts獲取到的pts已經轉化為時間戳了,這個時間戳為就是當前幀顯示結束的時間戳,也即是下一幀將顯示的預測時間戳。

static void video_refresh_timer(void *userdata) {

VideoState *is = (VideoState*)userdata;

VideoPicture *vp;

double actual_delay, delay,sync_threshold, ref_clock, diff;

if(is->video_st) {

if(is->pictq_size == 0) {

schedule_refresh(is, 1);

} else {

vp =&is->pictq[is->pictq_rindex];

delay = vp->pts -is->frame_last_pts;  ////這是當前要顯示的frame和下一副 //////將要顯示的 frame的間隔時間

if(delay <= 0 || delay>= 1.0) {

delay =is->frame_last_delay;

}

is->frame_last_delay =delay;

is->frame_last_pts =vp->pts;

ref_clock = get_audio_clock(is);/////獲取到聲音當前播放的時間戳。

diff = vp->pts -ref_clock;////// vp->pts實際上是預測的下一幀將要播放的開始時間,

//////////也就是說在diff這段時間中聲音是勻速發生的,但是在delay這段時間frame的顯示可能就會有快//////////慢的區別。

sync_threshold = (delay >AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;

if(fabs(diff) < AV_NOSYNC_THRESHOLD) {

if(diff <=-sync_threshold) {

  delay = 0;//////下一幀畫面顯示的時間和當前的聲音很近的話加快顯示下一幀(即後面video_display顯示完當前幀後開啟定時器很快去顯示下一幀)

} else if(diff >=sync_threshold) {

  delay = 2 * delay;//////下一幀開始顯示的時間和當前聲音的時間隔的比較長則延緩,即兩幀畫面間話的顯示的時間長度大於兩幀畫面間的聲音播放的時間,則我們將兩幀畫顯示的時候加倍拖長點,比如幀1和幀2的時間顯示間隔為40ms,但幀1和幀2的聲音播放時間為55ms,怎麼辦呢?我們不可能去打亂聲音的質量的,則我們採用的方法是:將兩幀畫面的播放間隔加大,本來是過30ms就要開始播下一幀的,我們改成60ms後才播下一幀。

}

}/////

////當然如果diff大於AV_NOSYNC_THRESHOLD,即快進的模式了,畫面跳動太大,不存在音視訊同步的問題了。

is->frame_timer += delay;

actual_delay =is->frame_timer - (av_gettime() / 1000000.0);

if(actual_delay < 0.010){

actual_delay = 0.010;

}

schedule_refresh(is,(int)(actual_delay * 1000 + 0.5));////開定時器去顯示下一幀

video_display(is);////立馬顯示當前幀

if(++is->pictq_rindex ==VIDEO_PICTURE_QUEUE_SIZE) {

is->pictq_rindex = 0;

}

SDL_LockMutex(is->pictq_mutex);

is->pictq_size--;

SDL_CondSignal(is->pictq_cond);

SDL_UnlockMutex(is->pictq_mutex);

}

} else {

schedule_refresh(is, 100);

}