- 視頻播放器開發(fā)總覽
- 最終效果圖
- 需要實現(xiàn)的功能
- ffmpeg介紹
- 解封裝
- 視頻解碼
- 音頻解碼
- seek
- QT播放音頻
- opengl顯示圖像
- opengl如何顯示yuv圖像
- 滑動條seek
- 屏幕單擊雙擊事件捕獲
- 其他注意事項
視頻播放器開發(fā)總覽
大體流程如上圖,讀取視頻文件,解封裝將音視頻數(shù)據(jù)分離筐钟,然后解碼送往設(shè)備顯示
其中需要用到的技術(shù)
qt:做播放器的界面平道,和用戶操作層
ffmpeg:音視頻的解封裝解碼
opengl: 調(diào)用顯卡將yuv數(shù)據(jù)轉(zhuǎn)rgb渲染到屏幕
項目地址:
https://gitee.com/lisiwen945/av_codec
最終效果圖
需要實現(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();
}
}
其他注意事項
- 在解碼后送顯示的過程中不能立即將frame釋放,因為更新畫布和送幀是異步的内舟,可能還沒有來得及顯示圖像已經(jīng)被釋放了冯遂,需要用智能指針引用,在update之后釋放
- 在視頻播放的時候谒获,滑動條會跟隨移動蛤肌,這個用一個定時器實現(xiàn),100ms刷新一次批狱,在拖動滑動條的時候裸准,一定要講定時器取消,在seek完事之后赔硫,再重新啟動定時器(實測加鎖不能解決問題炒俱,沒搞懂原因)