基于qt和ffmpeg视频播放器开发笔记

视频播放器开发总览

image-20220316110606356.png

大体流程如上图,读取视频文件,解封装将音视频数据分离,然后解码送往设备显示
其中需要用到的技术
qt:做播放器的界面,和用户操作层
ffmpeg:音视频的解封装解码
opengl: 调用显卡将yuv数据转rgb渲染到屏幕

项目地址:

https://gitee.com/lisiwen945/av_codec

最终效果图

image-20220316111740724.png

需要实现的功能

打开视频文件播放
进度条显示,拖动进度条视频seek功能
视频的播放暂停
单击屏幕暂停播放
双击屏幕全屏和退出全屏
窗体大小变化后,画面和控件需要自适应变化

ffmpeg介绍

FFmpeg有非常强大,几乎涵盖了音视频所有内容,音视频编解码,采集功能、视频格式转换、视频抓图、给视频加水印,直播推流拉流等,很多知名软件如暴风影音 格式工厂都是基于ffmpeg二次开发

我们这次需要用ffmpeg进行解封装和解码,可以去官网下载编译好的库文件,也可以直接用源码编译
官网地址:http://ffmpeg.org/

解封装

// 关键代码解析,详细代码看git链接

// 创建上下文
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);

视频解码

// 创建视频解码上下文
m_vctx = avcodec_alloc_context3(m_vdec);
avcodec_parameters_to_context(m_vctx, m_videoStream->codecpar);
// 打开解码器
avcodec_open2(m_vctx, m_vdec, nullptr);
// 往解码器灌入数据
avcodec_send_packet(m_vctx, pkt);
// 获取解码结果
avcodec_receive_frame(m_vctx, frame);

注意因为有b帧的存在,会有后向依赖帧,所以并不是输入一帧就能立马有结果,在没有结果的时候解码器会返回EAGAIN,解码结束后会有少量帧残留在解码器,需要输入空数据将其挤出来,即调用avcodec_send_packet(m_actx, nullptr);

音频解码

音频解码与数据解码大致相同
但是音频因为采样率和精度各个文件不相同需要进行重采样
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完要清除缓冲区
avcodec_flush_buffers(m_vctx);
m_pts = m_totalMs * pos;

QT播放音频

QAudioFormat // 详情见qt帮助文档
    
// 设置声道数量
void setChannelCount(int channels)
// 设置编码格式 "audio/pcm"
void setCodec(const QString &codec)
// 设置采样率
void setSampleRate(int samplerate)
// 设置采样精度 8/16
void setSampleSize(int sampleSize)
// 类型 QAudioFormat::SignedInt UnSignedInt Float
void setSampleType(QAudioFormat::SampleType sampleType)

// 播放行为设置
QAudioOutput 
void setVolume(qreal volume)
QIODevice *start()
QAudio::State state() const
void stop()
void suspend()
int bufferSize() const // 缓冲区buf大小
int bytesFree() const // 缓冲区空闲大小
int periodSize() const // 驱动一次处理多少数据
#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显示比较复杂,可以跟着https://learnopengl-cn.readthedocs.io/zh/latest/ 教程把顶点着色器,材质贴图,shader程序编写学会
能够在屏幕上画一个三角形,能够将材质贴到指定区域

但是这个官方教程的前置依赖比较多,很多接口比较老旧,而且需要有窗体的支持,建议在qt上opengl开发比较简单
qt上开发opengl可以看b站视频:https://www.bilibili.com/video/BV1UL411W71w?spm_id_from=333.999.0.0
看到第17章如何加载纹理单元就行,这时候你已经掌握了怎么将一个rgb图像显示出来

opengl如何显示yuv图像

与显示rgb图像类似,openg显示yuv图像需要将y,u,v分离为三个材质,将三个材质进行相关转换贴上去,代码如下

// 材质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三个材质
auto frame = m_frame->m_avFrame;
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texs[0]); //0层绑定到Y材质
//修改材质内容(复制内存内容)
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RED, GL_UNSIGNED_BYTE, frame->data[0]);
//与shader uni遍历关联
glUniform1i(unis[0], 0);

glActiveTexture(GL_TEXTURE0+1);
glBindTexture(GL_TEXTURE_2D, texs[1]); //1层绑定到U材质
//修改材质内容(复制内存内容)
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width/2, height / 2, GL_RED, GL_UNSIGNED_BYTE, frame->data[1]);
//与shader uni遍历关联
glUniform1i(unis[1],1);

glActiveTexture(GL_TEXTURE0+2);
glBindTexture(GL_TEXTURE_2D, texs[2]); //2层绑定到V材质
//修改材质内容(复制内存内容)
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width / 2, height / 2, GL_RED, GL_UNSIGNED_BYTE, frame->data[2]);
//与shader uni遍历关联
glUniform1i(unis[2], 2);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, NULL);

滑动条seek

qt滑动条seek在使用过程中如果点击到某个位置,滑动条并不会立即到这个位置,而是往前前进一小格
这个时候需要重写mousePressEvent方法,实现指哪打哪

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

屏幕单击双击事件捕获

// 单击和双击需要重写下面两个方法

// 单击响应
void MainWindow::mousePressEvent(QMouseEvent *e)
{
    if (m_status == STATUS_PAUSE) {
        m_status = STATUS_RUNNING;
        m_cvStatus.notify_one();
    } else {
        m_status = STATUS_PAUSE;
    }
}

// 双击响应
void MainWindow::mouseDoubleClickEvent(QMouseEvent *e)
{
    if (isFullScreen()) {
        this->showNormal();
    } else {
        this->showFullScreen();
    }
}

其他注意事项

  1. 在解码后送显示的过程中不能立即将frame释放,因为更新画布和送帧是异步的,可能还没有来得及显示图像已经被释放了,需要用智能指针引用,在update之后释放
  2. 在视频播放的时候,滑动条会跟随移动,这个用一个定时器实现,100ms刷新一次,在拖动滑动条的时候,一定要讲定时器取消,在seek完事之后,再重新启动定时器(实测加锁不能解决问题,没搞懂原因)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,427评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,551评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,747评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,939评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,955评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,737评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,448评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,352评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,834评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,992评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,133评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,815评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,477评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,022评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,147评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,398评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,077评论 2 355

推荐阅读更多精彩内容