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允許我們重新定義那個分配內存的函數。計算前一幀和現在這一幀的時間戳來預測出下一個時間戳的時間。同時,我們需要同步視頻到音頻。我們將設置一個音頻時間audio clock;一個內部值記錄了我們正在播放的音頻的位置。就像從任意的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 zero size, it means the image was buffered */
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;
/* write the compressed frame in the media file */
ret = av_interleaved_write_frame( m_pAvFormatContext, &pkt );
}
else
{
ret = 0;
}
請問avcodec_decode_video解碼的幀爲什麼後面的比前面的pts小呢?
請問如下代碼: |
答覆:
Because you have B - Frame
for example:
the Input sequence for video encoder
1 2 3 4 5 6 7
I B B P B B I
Let's take 1,2,3.. as PTS for simplification
the out sequence for video encoder ( this equals the decoder sequence)
1 4 2 3 7 5 6
I P B B I B B
you will get a PTS sequence as following:
1 4 2 3 7 5 6
7 5 6 sequence 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 decoder is the right order for display because I/P frames will be cached until 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_video packet->pts:%x,packet->dts:%x\n",packet->pts,packet->dts);
printf("avcodec_decode_video pFrame->pkt_pts:%x,pFrame->pkt_dts:%x,pFrame->pts:%x\n",pFrame->pkt_pts,pFrame->pkt_dts,pFrame->pts);
if(pFrame->opaque)
printf("avcodec_decode_video *(uint64_t *)pFrame->opaque:%x\n",*(uint64_t *)pFrame->opaque);
其中播一個mp4文件的打印情況:
-----------------------------------------------------------------------------
avcodec_decode_video packet->pts:1ae,packet->dts:0
avcodec_decode_video pFrame->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_video pFrame->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_video pFrame->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_video pFrame->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_video pFrame->pkt_pts:0,pFrame->pkt_dts:80000000,pFrame->pts:0
avcodec_decode_video *(uint64_t *)pFrame->opaque:24e
以下爲播放rm文件的情況:
-----------------------------------------------------------------------------
avcodec_decode_video packet->pts:1831b,packet->dts:0
avcodec_decode_video pFrame->pkt_pts:0,pFrame->pkt_dts:80000000,pFrame->pts:0
avcodec_decode_video *(uint64_t *)pFrame->opaque:1831b
-----------------------------------------------------------------------------
avcodec_decode_video packet->pts:18704,packet->dts:0
avcodec_decode_video pFrame->pkt_pts:0,pFrame->pkt_dts:80000000,pFrame->pts:0
avcodec_decode_video *(uint64_t *)pFrame->opaque:18704
-----------------------------------------------------------------------------
avcodec_decode_video packet->pts:18aed,packet->dts:0
avcodec_decode_video pFrame->pkt_pts:0,pFrame->pkt_dts:80000000,pFrame->pts:0
avcodec_decode_video *(uint64_t *)pFrame->opaque:18aed
-----------------------------------------------------------------------------
avcodec_decode_video packet->pts:18ed6,packet->dts:0
avcodec_decode_video pFrame->pkt_pts:0,pFrame->pkt_dts:80000000,pFrame->pts:0
avcodec_decode_video *(uint64_t *)pFrame->opaque:18ed6
-----------------------------------------------------------------------------
avcodec_decode_video packet->pts:192bf,packet->dts:0
avcodec_decode_video pFrame->pkt_pts:0,pFrame->pkt_dts:80000000,pFrame->pts:0
avcodec_decode_video *(uint64_t *)pFrame->opaque:192bf
-----------------------------------------------------------------------------
avcodec_decode_video packet->pts:196a8,packet->dts:0
avcodec_decode_video pFrame->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 a video 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) {
double frame_delay;
if(pts != 0) {
/* if we have pts, set video clock to it */
is->video_clock = pts;
} else {
/* if we aren't given a pts, set it to the clock */
pts = is->video_clock;
}
/* update the video clock */
/////很關鍵:前面傳進來的pts已經是時間戳了,是當前frame開始播放的時間戳,
/////下面frame_delay是該幀顯示完將要花費的時間,(pts+frame_delay)也即是/////預測的下一幀將要播放的時間戳。
frame_delay = av_q2d(is->video_st->codec->time_base);
/* if we are repeating a frame, adjust clock accordingly */
//////重複多幀的話要累加上
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
標杆是勻速的
螞蟻或快或慢
David Cen(3727567) 17:07:18
慢了你就抽它 讓他跑起來 快了就拽它
David Cen(3727567) 17:07:38
這樣音(標杆)視頻(螞蟻)就能同步了
David Cen(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; /* the pts from last time */ ////這是當前要顯示的frame和下一副 //////將要顯示的frame的間隔時間
if(delay <= 0 || delay >= 1.0) {
/* if incorrect delay, use previous one */
delay = is->frame_last_delay;
}
/* save for next time */
is->frame_last_delay = delay;
is->frame_last_pts = vp->pts;
/* update delay to sync to audio */
ref_clock = get_audio_clock(is);/////獲取到聲音當前播放的時間戳。
diff = vp->pts - ref_clock;////// vp->pts實際上是預測的下一幀將要播放的開始時間,
//////////也就是說在diff這段時間中聲音是勻速發生的,但是在delay這段時間frame的顯示可能就會有快//////////慢的區別。
/* Skip or repeat the frame. Take delay into account
FFPlay still doesn't "know if this is the best guess." */
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;
/* computer the REAL delay */
actual_delay = is->frame_timer - (av_gettime() / 1000000.0);
if(actual_delay < 0.010) {
/* Really it should skip the picture instead */
actual_delay = 0.010;
}
schedule_refresh(is, (int)(actual_delay * 1000 + 0.5));////開定時器去顯示下一幀
/* show the picture! */
video_display(is);////立馬顯示當前幀
/* update queue for next picture! */
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);
}
}