QtPlayer——基于FFmpeg的Qt音視訊播放器
本文主要講解一個基于Qt GUI的,使用FFmpeg音視訊庫解碼的音視訊播放器,同時也是記錄一點學習心得,本人也是多媒體初學者,也歡迎大家交流,程式運作圖如下:
- QtPlayer基于FFmpeg的Qt音視訊播放器
- 閑話
- 音視訊基礎
- 協定層
- 封裝層
- 壓縮層
- 圖像層
- 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) != ) { qDebug() << "Open file failed."; return ; } if (avformat_find_stream_info(pFormatCtx, NULL) < ) { qDebug() << "Could't find stream infomation."; avformat_free_context(pFormatCtx); return; }
- 打開視訊檔案成功之後就需要擷取到音視訊流的索引(還有一個subtitle,至今還不懂怎麼用,望告知):
/* find video & audio stream index */ for (unsigned int i = ; 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 < ) && (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 < ) && (ret != AVERROR_EOF)) {
qDebug() << "Video frame decode failed, error code: " << ret;
av_packet_unref(&packet);
continue;
}
if (av_buffersrc_add_frame(decoder->filterSrcCxt, pFrame) < ) {
qDebug() << "av buffersrc add frame failed.";
av_packet_unref(&packet);
continue;
}
if (av_buffersink_get_frame(decoder->filterSinkCxt, pFrame) < ) {
qDebug() << "av buffersink get frame failed.";
av_packet_unref(&packet);
continue;
} else {
QImage tmpImage(pFrame->data[], 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[] = {, , , , , , , };
int nextSampleRates[] = {, , , , };
int nextSampleRateIdx = FF_ARRAY_ELEMS(nextSampleRates) - ;
isStop = false;
isPause = false;
isreadFinished = false;
audioSrcFmt = AV_SAMPLE_FMT_NONE;
audioSrcChannelLayout = ;
audioSrcFreq = ;
audioBufIndex = ;
audioBufSize = ;
audioBufSize1 = ;
clock = ;
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 -;
}
/* open audio decoder */
if (avcodec_open2(codecCtx, codec, NULL) < ) {
avcodec_free_context(&codecCtx);
qDebug() << "Could not open audio decoder.";
return -;
}
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 <= || wantedSpec.channels <= ) {
avcodec_free_context(&codecCtx);
qDebug() << "Invalid sample rate or channel count, freq: " << wantedSpec.freq << " channels: " << wantedSpec.channels;
return -;
}
while (nextSampleRateIdx && nextSampleRates[nextSampleRateIdx] >= wantedSpec.freq) {
nextSampleRateIdx--;
}
wantedSpec.format = audioDeviceFormat;
wantedSpec.silence = ;
wantedSpec.samples = FFMAX(SDL_AUDIO_MIN_BUFFER_SIZE, << 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 () {
while (SDL_OpenAudio(&wantedSpec, &spec) < ) {
qDebug() << QString("SDL_OpenAudio (%1 channels, %2 Hz): %3")
.arg(wantedSpec.channels).arg(wantedSpec.freq).arg(SDL_GetError());
wantedSpec.channels = nextNbChannels[FFMIN(, 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 -;
}
}
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 -;
}
}
/* 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();
return ;
}
其中需要一個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 , means counld fill data to it
*/
while (SDL_AudioBufSize > ) {
if (decoder->isStop) {
return ;
}
if (decoder->isPause) {
SDL_Delay();
continue;
}
/* no data in buffer */
if (decoder->audioBufIndex >= decoder->audioBufSize) {
decodedSize = decoder->decodeAudio();
/* if error, just output silence */
if (decodedSize < ) {
/* if not decoded data, just output silence */
decoder->audioBufSize = ;
decoder->audioBuf = nullptr;
} else {
decoder->audioBufSize = decodedSize;
}
decoder->audioBufIndex = ;
}
/* 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, , 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 () {
if (decoder->isStop) {
break;
}
double audioClk = decoder->audioDecoder->getAudioClock();
pts = decoder->videoClk;
if (pts <= audioClk) {
break;
}
int delayTime = (pts - audioClk) * ;
delayTime = delayTime > ? : delayTime;
SDL_Delay(delayTime);
}
因為pts的機關是us,一般延時有ms級别就夠了,反正人眼就這麼瞎,快了也看不出來,就像打遊戲一樣其實上了30FPS和你300FPS效果都是差不多的,不過最好就是電腦顯示屏的重新整理率60Hz就enough了。而且一般的視訊幀率也就是25左右,是以用ms級的delay妥妥的。
至于其他的界面和播放控制請參考我的代碼(寫的差,見諒,還有就是要吐槽CSDN,自己上傳的資源自己還不能管理這是什麼道理,我這傳的是用sws進行視訊圖像轉碼的,需要參考avfliter的同學請移步GitHub,我就懶得傳2遍了):
CSDN:
http://download.csdn.net/download/q294971352/10104287
GitHub:
https://github.com/DragonPang/QtPlayer