QtPlayer——基於FFmpeg的Qt音視訊播放器
QtPlayer——基於FFmpeg的Qt音視訊播放器
本文主要講解一個基於Qt GUI的,使用FFmpeg音視訊庫解碼的音視訊播放器,同時也是記錄一點學習心得,本人也是多媒體初學者,也歡迎大家交流,程式執行圖如下:
閒話
平常沒事幹就想多學習學習新東西,然後想想現在的軟體全都是一堆廣告,所以呢就想著自己做一個播放器。本來Qt5也有現成的QMediaPlayer類,也沒去研究過,不過我猜放放普通格式的音視訊檔案應該沒問題,對於多格式的檔案就不知道能不能支援了。
那麼為什麼用FFmpeg呢,因為網上一搜全是這個,沒錯,就是瞎搞,還有就是播放音訊是使用SDL,也是網上的資料比較多而已。其實吧,還有就是考慮到以後說不定還能移植到我的ARM板上玩,總之多學一點總是沒錯的。
音視訊基礎
在做這之前完全對音視訊方面沒有任何專業知識,相信很多人也是一樣,這裡所要講的知識也並不什麼對某個音視訊格式的講解,只是大概說明一下,所要做的工作,如圖:
這裡是從雷神那邊竊取過來的知識,不知道雷神是誰的請點選。整個音視訊播放的流程就是從這四層一步一步往下走。
協議層
協議層主要是說明獲取到視訊檔案的協議,說簡單一點就是什麼HTTP、RTSP、RTMP或者是本地檔案。前面的網路協議自然不用說,本地檔案嘛,本來獲取檔案都是通過地址(URL)獲取的,就是平常本地檔案的路徑。
FFmpeg庫已經支援協議層的檔案獲取,所以這也是極大的方便,所以用別人造好的輪子就是這麼舒服,當然最好是瞭解輪子是怎麼造的。
封裝層
封裝層就是說明多媒體檔案的封裝格式,例如什麼.avi(滑稽),.mp4,.mkv之類的檔案格式。一個視訊檔案其實是由影象和聲音兩部分封裝而成的,當然也可以沒聲音部分,反正就是把這兩個封裝成一個檔案就是封裝層的任務。
壓縮層
壓縮層所講述的是我們所看到的視訊檔案的壓縮格式。視訊採集到的原始資料,我們不可能一幀一幀的原封不動的儲存下來,因為這樣儲存下載的檔案大的嚇人,比如平常我們看到的一個10M的視訊檔案,按原始資料儲存下來說不定大幾十倍都有可能(我瞎猜的),所以為了在這節省空間,需要對原始資料進行壓縮。
當前流行的壓縮格式當屬H264,不過H265也出了這麼多年了,也不知道現在發展的怎麼樣了。
影象層
影象層也就是原始資料層,主要是描述組成影象資料的格式,大多數時候也就是採集裝置,採集到的資料格式,最常用的當屬YUV420格式。不過Qt顯示影象的格式不支援YUV的格式,所以需要轉換成RGB格式。
FFmpeg的音視訊處理
FFmpeg但凡搞多媒體的應該都聽說過,一個很大的音視訊編解碼庫,想啃下來還是要花點時間,畢竟一個ffplay就是3700行程式碼,對不起,我暈程式碼。。。不過為了搞比利,還是要去看,而且不難發現,網上的例子全是用的別人的程式碼,好歹自己改個變數名啊。而且很多人用的老版本的庫,很多方法很不幸都deprecated了。雖然現在我用的方法以後說不定也會過時,不過還是得趕一波新潮。本文用到的FFmpeg版本為3.4。
使用FFmpeg最主要就是用它那強大的編解碼方法,首先,我們需要對它進行初始化:
void MainWindow::initFFmpeg()
{
// av_log_set_level(AV_LOG_INFO);
avfilter_register_all();
/* ffmpeg init */
av_register_all();
/* ffmpeg network init for rtsp */
if (avformat_network_init()) {
qDebug() << "avformat network init failed";
}
/* init sdl audio */
if (SDL_Init(SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
qDebug() << "SDL init failed";
}
}
最上面的av_log_set_level()是用來控制FFmpeg的列印等級的,就像Linux Kernel的列印控制方法一樣。
avfilter_register_all();註冊濾鏡,filter是ffmpeg的重要部分啊,可是我也剛入手,也不是很熟悉。
emmm最主要的就是av_register_all()這個方法,註冊了所有的編解碼混合器,麻麻再也不用擔心我的播放器有不支援的格式了。
然後就是avformat_network_init()網路模組初始化,如果想用什麼rtsp之類的網路直播視訊就必須加這一句。
然後就是處理的主體:
首先需要一個格式化輸入輸出上下文,就是靠這玩意兒開啟檔案,所以是核心的結構體:
pFormatCtx = avformat_alloc_context(); if (avformat_open_input(&pFormatCtx, currentFile.toLocal8Bit().data(), NULL, NULL) != 0) { qDebug() << "Open file failed."; return ; } if (avformat_find_stream_info(pFormatCtx, NULL) < 0) { qDebug() << "Could't find stream infomation."; avformat_free_context(pFormatCtx); return; }
開啟視訊檔案成功之後就需要獲取到音視訊流的索引(還有一個subtitle,至今還不懂怎麼用,望告知):
/* find video & audio stream index */ for (unsigned int i = 0; i < pFormatCtx->nb_streams; i++) { if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { videoIndex = i; qDebug() << "Find video stream."; } if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { audioIndex = i; qDebug() << "Find audio stream."; } if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE) { subtitleIndex = i; qDebug() << "Find subtitle stream."; } }
有了各個型別的資料流索引後就可以獲取到解碼器和資料流的結構體,以備後面處理:
/* find video decoder */ pCodecCtx = avcodec_alloc_context3(NULL); avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[videoIndex]->codecpar); videoStream = pFormatCtx->streams[videoIndex];
東西準備好了就可以開始解碼,要解碼首先當然需要從檔案中讀資料出來,而且解碼這種耗時的東西當然是放在子執行緒裡面,開個死迴圈慢慢來:
while (true) {
...
/* judge haven't read all frame */
if (av_read_frame(pFormatCtx, packet) < 0) {
qDebug() << "Read file completed.";
isReadFinished = true;
emit readFinished();
SDL_Delay(10);
break;
}
...
}
把資料包讀出來過後,就把packet分類,到對應的部分去處理它們:
if (packet->stream_index == videoIndex && currentType == "video") {
videoQueue.enqueue(packet); // video stream
} else if (packet->stream_index == audioIndex) {
audioDecoder->packetEnqueue(packet); // audio stream
} else if (packet->stream_index == subtitleIndex) {
subtitleQueue.enqueue(packet);
av_packet_unref(packet); // subtitle stream
} else {
av_packet_unref(packet);
}
當然解碼的速度肯定跟不上你讀的速度,所以先把讀出來的資料放在佇列裡,慢慢搞。
視訊解碼
視訊解碼相對來說比較簡單,把我們剛才讀的資料從佇列裡面取出來,放解碼器裡面,然後就得到想要的資料幀了= =!
decoder->videoQueue.dequeue(&packet, true);
ret = avcodec_send_packet(decoder->pCodecCtx, &packet);
if ((ret < 0) && (ret != AVERROR(EAGAIN)) && (ret != AVERROR_EOF)) {
qDebug() << "Video send to decoder failed, error code: " << ret;
av_packet_unref(&packet);
continue;
}
ret = avcodec_receive_frame(decoder->pCodecCtx, pFrame);
if ((ret < 0) && (ret != AVERROR_EOF)) {
qDebug() << "Video frame decode failed, error code: " << ret;
av_packet_unref(&packet);
continue;
}
if (av_buffersrc_add_frame(decoder->filterSrcCxt, pFrame) < 0) {
qDebug() << "av buffersrc add frame failed.";
av_packet_unref(&packet);
continue;
}
if (av_buffersink_get_frame(decoder->filterSinkCxt, pFrame) < 0) {
qDebug() << "av buffersink get frame failed.";
av_packet_unref(&packet);
continue;
} else {
QImage tmpImage(pFrame->data[0], decoder->pCodecCtx->width, decoder->pCodecCtx->height, QImage::Format_RGB32);
/* deep copy, otherwise when tmpImage data change, this image cannot display */
QImage image = tmpImage.copy();
decoder->displayVideo(image);
}
上面的程式碼主要注意的有兩點:
- 使用avcodec_send_packet()和avcodec_receive_frame()替換原先的一個什麼什麼decode函式,因為那個方法deprecated了,但是網上一堆程式碼還是用的那個。
- 這裡我用了avfilter直接對frame進行處理,然後得到處理後的RGB格式的frame後,直接例項一個QImage送去顯示。對於得到的Image還是deep copy一份,不然還沒顯示完,QImage指向的data pointer值被改了就麻煩了。
音訊解碼
至於音訊,因為用到了SDL去play sound所以就按照SDL的步驟走吧,首先需要open一個sound device,其實就是設定音訊解碼的一些引數:
int AudioDecoder::openAudio(AVFormatContext *pFormatCtx, int index)
{
AVCodec *codec;
SDL_AudioSpec wantedSpec;
int wantedNbChannels;
const char *env;
/* soundtrack array use to adjust */
int nextNbChannels[] = {0, 0, 1, 6, 2, 6, 4, 6};
int nextSampleRates[] = {0, 44100, 48000, 96000, 192000};
int nextSampleRateIdx = FF_ARRAY_ELEMS(nextSampleRates) - 1;
isStop = false;
isPause = false;
isreadFinished = false;
audioSrcFmt = AV_SAMPLE_FMT_NONE;
audioSrcChannelLayout = 0;
audioSrcFreq = 0;
audioBufIndex = 0;
audioBufSize = 0;
audioBufSize1 = 0;
clock = 0;
pFormatCtx->streams[index]->discard = AVDISCARD_DEFAULT;
stream = pFormatCtx->streams[index];
codecCtx = avcodec_alloc_context3(NULL);
avcodec_parameters_to_context(codecCtx, pFormatCtx->streams[index]->codecpar);
/* find audio decoder */
if ((codec = avcodec_find_decoder(codecCtx->codec_id)) == NULL) {
avcodec_free_context(&codecCtx);
qDebug() << "Audio decoder not found.";
return -1;
}
/* open audio decoder */
if (avcodec_open2(codecCtx, codec, NULL) < 0) {
avcodec_free_context(&codecCtx);
qDebug() << "Could not open audio decoder.";
return -1;
}
totalTime = pFormatCtx->duration;
env = SDL_getenv("SDL_AUDIO_CHANNELS");
if (env) {
qDebug() << "SDL audio channels";
wantedNbChannels = atoi(env);
audioDstChannelLayout = av_get_default_channel_layout(wantedNbChannels);
}
wantedNbChannels = codecCtx->channels;
if (!audioDstChannelLayout ||
(wantedNbChannels != av_get_channel_layout_nb_channels(audioDstChannelLayout))) {
audioDstChannelLayout = av_get_default_channel_layout(wantedNbChannels);
audioDstChannelLayout &= ~AV_CH_LAYOUT_STEREO_DOWNMIX;
}
wantedSpec.channels = av_get_channel_layout_nb_channels(audioDstChannelLayout);
wantedSpec.freq = codecCtx->sample_rate;
if (wantedSpec.freq <= 0 || wantedSpec.channels <= 0) {
avcodec_free_context(&codecCtx);
qDebug() << "Invalid sample rate or channel count, freq: " << wantedSpec.freq << " channels: " << wantedSpec.channels;
return -1;
}
while (nextSampleRateIdx && nextSampleRates[nextSampleRateIdx] >= wantedSpec.freq) {
nextSampleRateIdx--;
}
wantedSpec.format = audioDeviceFormat;
wantedSpec.silence = 0;
wantedSpec.samples = FFMAX(SDL_AUDIO_MIN_BUFFER_SIZE, 2 << av_log2(wantedSpec.freq / SDL_AUDIO_MAX_CALLBACKS_PER_SEC));
wantedSpec.callback = &AudioDecoder::audioCallback;
wantedSpec.userdata = this;
/* This function opens the audio device with the desired parameters, placing
* the actual hardware parameters in the structure pointed to spec.
*/
while (1) {
while (SDL_OpenAudio(&wantedSpec, &spec) < 0) {
qDebug() << QString("SDL_OpenAudio (%1 channels, %2 Hz): %3")
.arg(wantedSpec.channels).arg(wantedSpec.freq).arg(SDL_GetError());
wantedSpec.channels = nextNbChannels[FFMIN(7, wantedSpec.channels)];
if (!wantedSpec.channels) {
wantedSpec.freq = nextSampleRates[nextSampleRateIdx--];
wantedSpec.channels = wantedNbChannels;
if (!wantedSpec.freq) {
avcodec_free_context(&codecCtx);
qDebug() << "No more combinations to try, audio open failed";
return -1;
}
}
audioDstChannelLayout = av_get_default_channel_layout(wantedSpec.channels);
}
if (spec.format != audioDeviceFormat) {
qDebug() << "SDL audio format: " << wantedSpec.format << " is not supported"
<< ", set to advised audio format: " << spec.format;
wantedSpec.format = spec.format;
audioDeviceFormat = spec.format;
SDL_CloseAudio();
} else {
break;
}
}
if (spec.channels != wantedSpec.channels) {
audioDstChannelLayout = av_get_default_channel_layout(spec.channels);
if (!audioDstChannelLayout) {
avcodec_free_context(&codecCtx);
qDebug() << "SDL advised channel count " << spec.channels << " is not supported!";
return -1;
}
}
/* set sample format */
switch (audioDeviceFormat) {
case AUDIO_U8:
audioDstFmt = AV_SAMPLE_FMT_U8;
break;
case AUDIO_S16SYS:
audioDstFmt = AV_SAMPLE_FMT_S16;
break;
case AUDIO_S32SYS:
audioDstFmt = AV_SAMPLE_FMT_S32;
break;
case AUDIO_F32SYS:
audioDstFmt = AV_SAMPLE_FMT_FLT;
break;
default:
audioDstFmt = AV_SAMPLE_FMT_S16;
break;
}
/* open sound */
SDL_PauseAudio(0);
return 0;
}
其中需要一個SDL的callback函式,在這個函式裡面去處理音訊資訊,並且play出來:
void AudioDecoder::audioCallback(void *userdata, quint8 *stream, int SDL_AudioBufSize)
{
AudioDecoder *decoder = (AudioDecoder *)userdata;
int decodedSize;
/* SDL_BufSize means audio play buffer left size
* while it greater than 0, means counld fill data to it
*/
while (SDL_AudioBufSize > 0) {
if (decoder->isStop) {
return ;
}
if (decoder->isPause) {
SDL_Delay(10);
continue;
}
/* no data in buffer */
if (decoder->audioBufIndex >= decoder->audioBufSize) {
decodedSize = decoder->decodeAudio();
/* if error, just output silence */
if (decodedSize < 0) {
/* if not decoded data, just output silence */
decoder->audioBufSize = 1024;
decoder->audioBuf = nullptr;
} else {
decoder->audioBufSize = decodedSize;
}
decoder->audioBufIndex = 0;
}
/* calculate number of data that haven't play */
int left = decoder->audioBufSize - decoder->audioBufIndex;
if (left > SDL_AudioBufSize) {
left = SDL_AudioBufSize;
}
if (decoder->audioBuf) {
memset(stream, 0, left);
SDL_MixAudio(stream, decoder->audioBuf + decoder->audioBufIndex, left, decoder->volume);
}
SDL_AudioBufSize -= left;
stream += left;
decoder->audioBufIndex += left;
}
}
這個callback需要傳入的三個引數:
- 第一個是使用者資料,一般就傳你當前的資料結構進去啦,對於我這種C++寫的,直接在open的時候就傳了個this進去;
- 第二個引數是一個指向播放資料的pointer,解碼後的audio data就需要copy到這個pointer播放;
- 第三個引數是播放資料的空間剩餘大小,如果大於0,我們就可以繼續copy data到前面的stream裡面。
然後就是我們的解碼主體,裡面基本上和視訊解碼是相同的,不過是視訊轉碼用sws,音訊用swr而已。
需要注意的是有時候一個數據packet裡面可能包含多個frame資料,視訊的我沒遇到,音訊的最典型的就是.ape的檔案(擁有音樂夢想的人,聽歌都是ape和flac的,不知道裝逼會不會捱打_ (:з」∠)_)。所以在avcodec_send_packet()需要對返回值進行判斷,如果packet還有其他資料,下次解碼的時候就不去讀其他的packet,繼續搞同一個。
音視訊同步
解碼了視訊和音訊,當然要放啊,放出來就GG了,視訊那速度快的都不知道是幾倍速,我之前試了一下delay了25個ms大概才是正常的速度,這樣明顯不行嘛,所以我們就需要進行音視訊同步。
對於音視訊同步我用的最常用的方法,就是視訊等音訊,畢竟視訊放的那麼快。那麼它們同步的標準呢,就是一個叫做pts(顯示時間戳)的東西,當我們讀了一個音訊和一個視訊frame的pts後,比較一下,如果視訊的pts大了,證明視訊快了,就讓它delay一下:
while (1) {
if (decoder->isStop) {
break;
}
double audioClk = decoder->audioDecoder->getAudioClock();
pts = decoder->videoClk;
if (pts <= audioClk) {
break;
}
int delayTime = (pts - audioClk) * 1000;
delayTime = delayTime > 5 ? 5 : delayTime;
SDL_Delay(delayTime);
}
因為pts的單位是us,一般延時有ms級別就夠了,反正人眼就這麼瞎,快了也看不出來,就像打遊戲一樣其實上了30FPS和你300FPS效果都是差不多的,不過最好就是電腦顯示屏的重新整理率60Hz就enough了。而且一般的視訊幀率也就是25左右,所以用ms級的delay妥妥的。
至於其他的介面和播放控制請參考我的程式碼(寫的差,見諒,還有就是要吐槽CSDN,自己上傳的資源自己還不能管理這是什麼道理,我這傳的是用sws進行視訊影象轉碼的,需要參考avfliter的同學請移步GitHub,我就懶得傳2遍了):