FFMPEG+SDL實現視頻播放器


一. 前言

基於學習ffmpeg和sdl,寫一個視頻播放器是個不錯的練手項目。
視頻播放器的原理很多人的博客都有講過,這里出於自己總結的目的,還是會做一些概況。

二. 視頻播放器基本原理

image.png

2.1 解封裝

視頻文件基本上都是將編碼好的音頻和視頻數據封裝在一起形成的,因此拿到視頻文件的第一步就是先將它解封裝,分為視頻流和音頻流壓縮編碼數據。常見的封裝格式有MP4、MKV、FLV、AVI、RMVB、TS等。例如,FLV格式的文件經過解封裝后,可能得到H.264編碼的視頻碼流和AAC編碼的音頻碼流。
在FFMPEG中,解封裝的流程如下:
image.png
這一步最重要的是得到解封裝器的上下文結構體"AVFormatContext *m_pFormatCtx", 以及接下來我們要解碼的音視頻流索引。

2.2 解碼

原始數據基本上都是經過壓縮編碼后的數據,解碼過程就是將H.264、AAC等壓縮后的數據解碼成非壓縮的音頻/視頻原始數據,視頻一般是YUV或者RGB數據,音頻一般是PCM抽樣數據。
解碼過程可以總結如下:
image.png

2.3 SDL2播放視頻數據

我們都知道視頻其實都是由連續的一幀幀圖像快速播放形成的動態效果,一般視頻都設置成了25幀,即1s內播放25幅圖片。
我們使用SDL2庫來播放視頻。這和我之前的SDL2學習(一): 顯示一張圖片中寫到的SDL2顯示一張圖片就關聯了起來,不過這里更加復雜點。
在視頻解碼完后,我們在avcodec_receive_frame得到的AVFrame對象,就是視頻的一幀數據。我們要做的是將這一幀的數據顯示到SDL的Render中。總體流程如下:
image.png
首先我們需要對得到的AVFrame數據進行大小格式的變換,這里使用sws_scale函數實現,之后就是更新SDL中的Texture和Render了。下面是關鍵代碼:

AVFrame *frame = m_videoFrameQueue.front();
m_videoFrameQueue.pop();

AVFrame *frameYUV = av_frame_alloc();
int ret = av_image_alloc(frameYUV->data, frameYUV->linesize, m_sdlRect.w, m_sdlRect.h, AV_PIX_FMT_YUV420P, 1);
//Convert image
if (m_imgConvertCtx)
{
sws_scale(m_imgConvertCtx, frame->data, frame->linesize, 0, m_videoCodecParams.height, frameYUV->data, frameYUV->linesize);
SDL_UpdateYUVTexture(m_sdlTexture, NULL, frameYUV->data[0], frameYUV->linesize[0], frameYUV->data[1], frameYUV->linesize[1], frameYUV->data[2], frameYUV->linesize[2]);
SDL_RenderClear(m_sdlRender);
SDL_RenderCopy(m_sdlRender, m_sdlTexture, NULL, &m_sdlRect);

// Present picture
SDL_RenderPresent(m_sdlRender);
}

2.4 SDL2播放音頻數據

對於音頻數據,avcodec_receive_frame后得到的AVFrame是音頻的pcm數據,但是它不向視頻那樣表示"一幀",它可能包含很多的sample,即多次的采樣數據。
播放音頻,同樣需要對音頻數據進行格式轉換,以支持音頻設備的播放。音頻格式轉換主要通過swr_convert函數完成。轉換后的音頻數據可以放到一個公共緩沖區中。
播放音頻使用SDL_OpenAudio函數,它需要闖入一個SDL_AudioSpec結構體用於設置播放參數,其中需要設置一個callback用於音頻設備取數據時執行,因此我們需要在這個回調里向音頻設備"喂"數據:

SDL_AudioSpec m_sdlAudioSpec;
auto audioCtx = m_audioDecoder.GetCodecContext();

m_sdlAudioSpec.freq = audioCtx->sample_rate; //根據你錄制的PCM采樣率決定
m_sdlAudioSpec.format = AUDIO_S16SYS;
m_sdlAudioSpec.channels = audioCtx->channels;
m_sdlAudioSpec.silence = 0;
m_sdlAudioSpec.samples = SDL_AUDIO_BUFFER_SIZE;
m_sdlAudioSpec.callback = &SDLVideoPlayer::ReadAudioData;
m_sdlAudioSpec.userdata = NULL;

int re = SDL_OpenAudio(&m_sdlAudioSpec, NULL);
if (re < 0)
{
    std::cout << "can't open audio: " << GetErrorInfo(re);
}
else
{
    //Start play audio
    SDL_PauseAudio(0);
}

void SDLVideoPlayer::ReadAudioData(void *udata, Uint8 *stream, int len) {
	SDL_memset(stream, 0, len);
    //需要向stream中填充len長度的音頻數據
    ...
    SDL_MixAudio(stream, m_audioPcmDataBuf, len, g_volum);
}

2.5 音視頻同步的設計

用兩個線程分別播放音頻和視頻,音頻的話可以直接在所設置的回調中喂數據即可,而視頻則需要我們自己來控制播放速度,這就涉及到兩者播放速度的統一問題。
音視頻同步的基本方式就是確定一個時鍾作為主時鍾,播放過程中,主時鍾作為同步基准,不斷判斷當前流的播放時間和主時鍾的差異,以調節自身的播放速度。按照主時鍾的不同種類,可以分為:

  • 音頻同步到視頻,視頻時鍾作為主時鍾;
  • 視頻同步到音頻,音頻時鍾作為主時鍾;
  • 音視頻都同步到外部時鍾。

由於音頻播放時往往都是送很多數據到設備緩存中,而且音頻播放效果對人的敏感度更高,因此以音頻時鍾為主是比較合理且簡單的辦法。具體實現就是:

  1. 在每次喂音頻數據的時候,記錄送入數據的起始pts時間戳,表示當前音頻的播放進度;
  2. 每次刷新圖片時,記錄當前圖片幀的pts時間戳;
  3. 在記錄當前音頻pts的同時,根據記錄的圖片pts,記錄兩者間的延時delay;
  4. 刷新圖片時,根據delay值判斷,當前視頻如果比音頻快,那么一次性調整視頻等待時間為正常兩幀間隔加音視頻之間的延時,之后將delay置0;如果音頻比視頻快,那么直接丟棄當前的視頻幀,直到和音頻時間一致。

2.6 快進和快退

快進和快退,或者一些播放器直接拖動進度條,實現思路都是一樣的,即使用av_seek_frame實現:

 av_seek_frame(m_pFormatCtx, -1, pts * AV_TIME_BASE, AVSEEK_FLAG_BACKWARD);

因此關鍵就是獲取要跳轉的時間戳,這個在做音視頻同步處理后,這個時間戳就很容易拿到。

2.7 SDL事件處理

對於窗口大小更改、暫停、快進快退等,都是需要交互的,這個可以通過SDL的事件機制來實現。
監聽事件:

SDL_Event event;
SDL_WaitEvent(&event);
if (event.type == SDL_WINDOWEVENT) {
	...
}
...

除了預定義的事件,比如窗口事件、鼠標事件、按鍵事件等,你也可以自己觸發或定義新的事件:

SDL_Event event;
event.type = SFM_REFRESH_PIC_EVENT;
SDL_PushEvent(&event);

我這里就是使用SDL事件來通知視頻播放線程來進行下一幀的播放。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM