基于qt和ffmpeg視頻播放器開發(fā)筆記

視頻播放器開發(fā)總覽

image-20220316110606356.png

大體流程如上圖,讀取視頻文件,解封裝將音視頻數(shù)據(jù)分離筐钟,然后解碼送往設(shè)備顯示
其中需要用到的技術(shù)
qt:做播放器的界面平道,和用戶操作層
ffmpeg:音視頻的解封裝解碼
opengl: 調(diào)用顯卡將yuv數(shù)據(jù)轉(zhuǎn)rgb渲染到屏幕

項目地址:

https://gitee.com/lisiwen945/av_codec

最終效果圖

image-20220316111740724.png

需要實現(xiàn)的功能

打開視頻文件播放
進(jìn)度條顯示,拖動進(jìn)度條視頻seek功能
視頻的播放暫停
單擊屏幕暫停播放
雙擊屏幕全屏和退出全屏
窗體大小變化后胸懈,畫面和控件需要自適應(yīng)變化

ffmpeg介紹

FFmpeg有非常強(qiáng)大,幾乎涵蓋了音視頻所有內(nèi)容,音視頻編解碼脐供,采集功能、視頻格式轉(zhuǎn)換借跪、視頻抓圖政己、給視頻加水印,直播推流拉流等垦梆,很多知名軟件如暴風(fēng)影音 格式工廠都是基于ffmpeg二次開發(fā)

我們這次需要用ffmpeg進(jìn)行解封裝和解碼匹颤,可以去官網(wǎng)下載編譯好的庫文件仅孩,也可以直接用源碼編譯
官網(wǎng)地址:http://ffmpeg.org/

解封裝

// 關(guān)鍵代碼解析,詳細(xì)代碼看git鏈接

// 創(chuàng)建上下文
m_ctx = avformat_alloc_context();
// 打開文件路徑
avformat_open_input(&m_ctx, path, 0, &opts);
// 找到視頻索引
m_videoIndex = av_find_best_stream(m_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
// 讀取一個待解碼的包印蓖,這個包可能是音頻可能是視頻
// 通過avPacket->stream_index 來判斷是音頻還是視頻
av_read_frame(m_ctx, avPacket);

視頻解碼

// 創(chuàng)建視頻解碼上下文
m_vctx = avcodec_alloc_context3(m_vdec);
avcodec_parameters_to_context(m_vctx, m_videoStream->codecpar);
// 打開解碼器
avcodec_open2(m_vctx, m_vdec, nullptr);
// 往解碼器灌入數(shù)據(jù)
avcodec_send_packet(m_vctx, pkt);
// 獲取解碼結(jié)果
avcodec_receive_frame(m_vctx, frame);

注意因為有b幀的存在辽慕,會有后向依賴幀,所以并不是輸入一幀就能立馬有結(jié)果赦肃,在沒有結(jié)果的時候解碼器會返回EAGAIN溅蛉,解碼結(jié)束后會有少量幀殘留在解碼器,需要輸入空數(shù)據(jù)將其擠出來他宛,即調(diào)用avcodec_send_packet(m_actx, nullptr);

音頻解碼

音頻解碼與數(shù)據(jù)解碼大致相同
但是音頻因為采樣率和精度各個文件不相同需要進(jìn)行重采樣
int outsize = av_samples_get_buffer_size(NULL, m_actx->channels,
frame->nb_samples, AV_SAMPLE_FMT_S16, 0);
uint8_t * data[1] = {0};
data[0] = (uint8_t *)malloc(outsize);
int len = swr_convert(m_aswCtx, data, 10000,
(const uint8_t **)frame->data,
frame->nb_samples
);

pcm.data = data[0];
pcm.len = outsize;

seek

int64_t stamp = 0;
stamp = pos * m_ctx->streams[m_videoIndex]->duration;
av_seek_frame(m_ctx, m_videoIndex, stamp,
              AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_FRAME);
// seek完要清除緩沖區(qū)
avcodec_flush_buffers(m_vctx);
m_pts = m_totalMs * pos;

QT播放音頻

QAudioFormat // 詳情見qt幫助文檔
    
// 設(shè)置聲道數(shù)量
void setChannelCount(int channels)
// 設(shè)置編碼格式 "audio/pcm"
void setCodec(const QString &codec)
// 設(shè)置采樣率
void setSampleRate(int samplerate)
// 設(shè)置采樣精度 8/16
void setSampleSize(int sampleSize)
// 類型 QAudioFormat::SignedInt UnSignedInt Float
void setSampleType(QAudioFormat::SampleType sampleType)

// 播放行為設(shè)置
QAudioOutput 
void setVolume(qreal volume)
QIODevice *start()
QAudio::State state() const
void stop()
void suspend()
int bufferSize() const // 緩沖區(qū)buf大小
int bytesFree() const // 緩沖區(qū)空閑大小
int periodSize() const // 驅(qū)動一次處理多少數(shù)據(jù)
#include <QCoreApplication>
#include <QtMultimedia/QAudioFormat>
#include <QtMultimedia/QAudioOutput>
#include <QThread>
#include <iostream>
#include <direct.h>
// ffmpeg -i video -f s16le test.pcm
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    FILE *fp = fopen("C:/Users/Administrator/Desktop/gitee/av_codec/audio_player/audio_player/test.pcm", "rb");
    if (fp == nullptr) {
        char buf[1024];
        getcwd(buf, 1024);
        std::cout << "open file failed pwd " << buf << std::endl;
        return 0;
    }

    QAudioFormat fmt;
    fmt.setSampleRate(44100);
    fmt.setSampleSize(16);
    fmt.setChannelCount(2);
    fmt.setCodec("audio/pcm");
    fmt.setByteOrder(QAudioFormat::LittleEndian);
    fmt.setSampleType(QAudioFormat::UnSignedInt);
    QAudioOutput *out = new QAudioOutput(fmt);
    QIODevice *io = out->start(); //開始播放

    auto minSize = out->periodSize();
    auto bufSize = out->bufferSize();
    char *buf = new char[bufSize];
    while (!feof(fp)) {
        int freeSize = out->bytesFree();
        if (freeSize < minSize) {
            QThread::sleep(0);
            continue;
        }
        auto readSize = fread(buf, 1, freeSize, fp);
        io->write(buf, readSize);
    }

    delete [] buf;
    fclose(fp);

    return a.exec();
}

opengl顯示圖像

opengl顯示比較復(fù)雜船侧,可以跟著https://learnopengl-cn.readthedocs.io/zh/latest/ 教程把頂點著色器,材質(zhì)貼圖厅各,shader程序編寫學(xué)會
能夠在屏幕上畫一個三角形镜撩,能夠?qū)⒉馁|(zhì)貼到指定區(qū)域

但是這個官方教程的前置依賴比較多,很多接口比較老舊队塘,而且需要有窗體的支持袁梗,建議在qt上opengl開發(fā)比較簡單
qt上開發(fā)opengl可以看b站視頻:https://www.bilibili.com/video/BV1UL411W71w?spm_id_from=333.999.0.0
看到第17章如何加載紋理單元就行,這時候你已經(jīng)掌握了怎么將一個rgb圖像顯示出來

opengl如何顯示yuv圖像

與顯示rgb圖像類似憔古,openg顯示yuv圖像需要將y遮怜,u,v分離為三個材質(zhì)鸿市,將三個材質(zhì)進(jìn)行相關(guān)轉(zhuǎn)換貼上去锯梁,代碼如下

// 材質(zhì)shader程序
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCord;
uniform sampler2D tex_y;
uniform sampler2D tex_u;
uniform sampler2D tex_v;
void main(void)
{
    vec3 yuv;
    vec3 rgb;
    yuv.x = texture2D(tex_y, TexCord).r;
    yuv.y = texture2D(tex_u, TexCord).r - 0.5;
    yuv.z = texture2D(tex_v, TexCord).r - 0.5;
    rgb = mat3(1.0, 1.0, 1.0, 0.0, -0.39465, 2.03211, 1.13983, -0.58060, 0.0) * yuv;
    FragColor = vec4(rgb, 1.0);
}

// 頂點shader
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aColor;
layout(location = 2) in vec2 aTexCord;
out vec3 ourColor;
out vec2 TexCord;
void main(){
    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0f);
    ourColor=aColor;
    TexCord=aTexCord;
}

// 從yuv圖像中分離y u v三個材質(zhì)
auto frame = m_frame->m_avFrame;
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texs[0]); //0層綁定到Y(jié)材質(zhì)
//修改材質(zhì)內(nèi)容(復(fù)制內(nèi)存內(nèi)容)
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RED, GL_UNSIGNED_BYTE, frame->data[0]);
//與shader uni遍歷關(guān)聯(lián)
glUniform1i(unis[0], 0);

glActiveTexture(GL_TEXTURE0+1);
glBindTexture(GL_TEXTURE_2D, texs[1]); //1層綁定到U材質(zhì)
//修改材質(zhì)內(nèi)容(復(fù)制內(nèi)存內(nèi)容)
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width/2, height / 2, GL_RED, GL_UNSIGNED_BYTE, frame->data[1]);
//與shader uni遍歷關(guān)聯(lián)
glUniform1i(unis[1],1);

glActiveTexture(GL_TEXTURE0+2);
glBindTexture(GL_TEXTURE_2D, texs[2]); //2層綁定到V材質(zhì)
//修改材質(zhì)內(nèi)容(復(fù)制內(nèi)存內(nèi)容)
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width / 2, height / 2, GL_RED, GL_UNSIGNED_BYTE, frame->data[2]);
//與shader uni遍歷關(guān)聯(lián)
glUniform1i(unis[2], 2);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, NULL);

滑動條seek

qt滑動條seek在使用過程中如果點擊到某個位置,滑動條并不會立即到這個位置焰情,而是往前前進(jìn)一小格
這個時候需要重寫mousePressEvent方法陌凳,實現(xiàn)指哪打哪

void LynSlider::mousePressEvent(QMouseEvent *e)
{
    int value = ((float)e->pos().x() / (float)this->width())*(this->maximum() +1);
    this->setValue(value);
    QSlider::mousePressEvent(e);
}

屏幕單擊雙擊事件捕獲

// 單擊和雙擊需要重寫下面兩個方法

// 單擊響應(yīng)
void MainWindow::mousePressEvent(QMouseEvent *e)
{
    if (m_status == STATUS_PAUSE) {
        m_status = STATUS_RUNNING;
        m_cvStatus.notify_one();
    } else {
        m_status = STATUS_PAUSE;
    }
}

// 雙擊響應(yīng)
void MainWindow::mouseDoubleClickEvent(QMouseEvent *e)
{
    if (isFullScreen()) {
        this->showNormal();
    } else {
        this->showFullScreen();
    }
}

其他注意事項

  1. 在解碼后送顯示的過程中不能立即將frame釋放,因為更新畫布和送幀是異步的内舟,可能還沒有來得及顯示圖像已經(jīng)被釋放了冯遂,需要用智能指針引用,在update之后釋放
  2. 在視頻播放的時候谒获,滑動條會跟隨移動蛤肌,這個用一個定時器實現(xiàn),100ms刷新一次批狱,在拖動滑動條的時候裸准,一定要講定時器取消,在seek完事之后赔硫,再重新啟動定時器(實測加鎖不能解決問題炒俱,沒搞懂原因)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子权悟,更是在濱河造成了極大的恐慌砸王,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件峦阁,死亡現(xiàn)場離奇詭異谦铃,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)榔昔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進(jìn)店門驹闰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人撒会,你說我怎么就攤上這事嘹朗。” “怎么了诵肛?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵屹培,是天一觀的道長。 經(jīng)常有香客問我怔檩,道長惫谤,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任珠洗,我火速辦了婚禮,結(jié)果婚禮上若专,老公的妹妹穿的比我還像新娘许蓖。我一直安慰自己,他們只是感情好调衰,可當(dāng)我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布膊爪。 她就那樣靜靜地躺著,像睡著了一般嚎莉。 火紅的嫁衣襯著肌膚如雪米酬。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天趋箩,我揣著相機(jī)與錄音赃额,去河邊找鬼。 笑死叫确,一個胖子當(dāng)著我的面吹牛跳芳,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播竹勉,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼飞盆,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起吓歇,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤孽水,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后城看,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體女气,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年析命,在試婚紗的時候發(fā)現(xiàn)自己被綠了主卫。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡鹃愤,死狀恐怖簇搅,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情软吐,我是刑警寧澤瘩将,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站凹耙,受9級特大地震影響姿现,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜肖抱,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一备典、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧意述,春花似錦提佣、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至术荤,卻和暖如春倚喂,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瓣戚。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工端圈, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人子库。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓枫笛,卻偏偏與公主長得像,于是被迫代替她去往敵國和親刚照。 傳聞我的和親對象是個殘疾皇子刑巧,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內(nèi)容