33_音视频播放器_画面显示
一、简介
上节介绍了使用SDL播放音频,这节介绍视频显示,其解码流程跟音频差不多。
解码视频是比较耗时的,需要我们自己开个线程去解码,而音频是SDL帮我们管理了子线程去解码音频,初始化音频SDL后就开始进行播放(
SDL_PauseAudio(0);
)了,一播放就会调用回调函数(sdlAudioCallback
),然后在去调用音频解码(decodeAudio
),这个decodeAudio
是被动调用,而且每次调用只解码一个。但是视频解码是主动去调用,然后解码所有东西,所有需要while循环。
二、视频解码
2.1 解码视频
void VideoPlayer::decodeVideo(){ while (true) { _vMutex->lock(); if(_vPktList->empty()){ _vMutex->unlock(); continue; } // 取出头部的视频包 AVPacket pkt = _vPktList->front(); _vPktList->pop_front(); _vMutex->unlock(); // 发送压缩数据到解码器 int ret = avcodec_send_packet(_vDecodeCtx, &pkt); // 释放pkt av_packet_unref(&pkt); CONTINUE(avcodec_send_packet); while (true) { ret = avcodec_receive_frame(_vDecodeCtx, _vSwsInFrame); if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { break; } else BREAK(avcodec_receive_frame); // 像素格式的转换 sws_scale(_vSwsCtx, _vSwsInFrame->data, _vSwsInFrame->linesize, 0, _vDecodeCtx->height, _vSwsOutFrame->data, _vSwsOutFrame->linesize); qDebug()<< _vSwsOutFrame->data[0]; } } }
2.2 调用解码视频方法
开启子线程调用视频解码方法:
// 初始化视频信息 int VideoPlayer::initVideoInfo() { int ret = initDecoder(&_vDecodeCtx,&_vStream,AVMEDIA_TYPE_VIDEO); RET(initDecoder); // 初始化像素格式转换 ret = initSws(); RET(initSws); // 开启新的线程去解码视频数据 std::thread([this](){ decodeVideo(); }).detach(); return 0; }
三、像素格式转换
上面解码出_vSwsInFrame
后是YUV数据,而显示是需要RGB的,所以这里需要进行像素格式转换。
3.1 初始化
初始化像素格式转换:
int VideoPlayer::initSws(){ int inW = _vDecodeCtx->width; int inH = _vDecodeCtx->height; // 输出frame的参数 _vSwsOutSpec.width = inW >> 4 << 4;// 先除以16在乘以16,保证是16的倍数 _vSwsOutSpec.height = inH >> 4 << 4; _vSwsOutSpec.pixFmt = AV_PIX_FMT_RGB24; _vSwsOutSpec.size = av_image_get_buffer_size( _vSwsOutSpec.pixFmt, _vSwsOutSpec.width, _vSwsOutSpec.height, 1); // 初始化像素格式转换的上下文 _vSwsCtx = sws_getContext(inW, inH, _vDecodeCtx->pix_fmt, _vSwsOutSpec.width, _vSwsOutSpec.height, _vSwsOutSpec.pixFmt, SWS_BILINEAR, nullptr, nullptr, nullptr); if (!_vSwsCtx) { qDebug() << "sws_getContext error"; return -1; } // 初始化像素格式转换的输入frame _vSwsInFrame = av_frame_alloc(); if (!_vSwsInFrame) { qDebug() << "av_frame_alloc error"; return -1; } // 初始化像素格式转换的输出frame _vSwsOutFrame = av_frame_alloc(); if (!_vSwsOutFrame) { qDebug() << "av_frame_alloc error"; return -1; } // _vSwsOutFrame的data[0]指向的内存空间 // int ret = av_image_alloc(_vSwsOutFrame->data, // _vSwsOutFrame->linesize, // _vSwsOutSpec.width, // _vSwsOutSpec.height, // _vSwsOutSpec.pixFmt, // 1); // RET(av_image_alloc); return 0; }
在像素转换的时候是有要求的,最好是16的倍数,否则其他分辨率的不能播放,所以我们需要在输出参数中控制宽高的大小
3.2 像素格式转换
然后在视频解码方法中进行像素格式的转换
// 像素格式的转换 sws_scale(_vSwsCtx, _vSwsInFrame->data, _vSwsInFrame->linesize, 0, _vDecodeCtx->height, _vSwsOutFrame->data, _vSwsOutFrame->linesize); qDebug()<< _vSwsOutFrame->data[0];
这里的_vSwsOutFrame->data
的data[0]
就是像素格式转换后的RGB数据。现在通过运行打印data[0]
,可以发现它是空的。
这里跟音频的重采样里的data[0]
道理是一样,需要我们手动的给_vSwsOutFrame->data[0]
创建一块内存区域,这块内存区域需要多大呢,因为视频解码avcodec_receive_frame
解码出来的就是一帧大小,所以这个_vSwsOutFrame->data[0]
指向的内存空间只需要一帧大小就可以了。
// _vSwsOutFrame的data[0]指向的内存空间 int ret = av_image_alloc(_vSwsOutFrame->data, _vSwsOutFrame->linesize, _vSwsOutSpec.width, _vSwsOutSpec.height, _vSwsOutSpec.pixFmt, 1); RET(av_image_alloc);
我们在调用
avcodec_receive_frame
后_vSwsInFrame
的_vSwsInFrame.data[0]
在其内部已经给创建好内存区域了,而且每次调用此方法其内部都会先自动销毁_vSwsInFrame.data[0]
,然后在给其分配空间,所以不需要我们手动创建和销毁data[0]
。
如果avcodec_receive_frame
是最有一次调用,不会再有下一次调用了,那么最后一次内部分配的data[0]
的空间是不是一直没有释放呢?其实是不会的,因为我们这个程序最后一次调用_vSwsInFrame
是有值的,而且它的ret还是返回的是成功状态,那么它就会继续执行while
循环,当再次执行时,因为已经没有数据量,ret返回的状态就会满足if
条件if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
,最后就会退出while
循环。
四、显示画面
上面完成像素格式的转换后,就需要通过发送信号出去给到VideoWidget
进行显示。
4.1 定义信号
在videoplayer.h
中定义信号
void frameDecoded(VideoPlayer *player, uint8_t *data, VideoSwsSpec &spec);
在videoplayer_video.cpp
的视频解码方法里最后进行发送信号
4.2 发送信号
// 发出信号 emit frameDecoded(this, _vSwsOutFrame->data[0], _vSwsOutSpec);
这里是不能直接发送_vSwsOutSpec
类型的数据的,需要我们注册此类型的数据。
// mainwindow.cpp的构造方法里 // 注册信号的参数类型,保证能够发出信号 qRegisterMetaType<VideoPlayer::VideoSwsSpec>("VideoSwsSpec&");
4.3 定义槽函数
在videowidget.h
中定义槽函数:
public slots: void onPlayerFrameDecoded(VideoPlayer *player, uint8_t *data, VideoPlayer::VideoSwsSpec &spec);
4.4 注册监听信号
在mainwindow.cpp
中注册监听信号:
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ...... connect(_player, &VideoPlayer::frameDecoded, ui->videoWidget, &VideoWidget::onPlayerFrameDecoded); ...... }
4.5 画面显示
实现videowidget.cpp
里的方法:
#include "videowidget.h" #include <QDebug> #include <QPainter> VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) { // 设置背景色 setAttribute(Qt::WA_StyledBackground); setStyleSheet("background: black"); } VideoWidget::~VideoWidget() { if (_image) { delete _image; _image = nullptr; } } void VideoWidget::onPlayerFrameDecoded(VideoPlayer *player, uint8_t *data, VideoPlayer::VideoSwsSpec &spec) { // 释放之前的图片 if (_image) { delete _image; _image = nullptr; } // 创建新的图片 if (data != nullptr) { _image = new QImage((uchar *) data, spec.width, spec.height, QImage::Format_RGB888); // 计算最终的尺寸 // 组件的尺寸 int w = width(); int h = height(); // 计算rect int dx = 0; int dy = 0; int dw = spec.width; int dh = spec.height; // 计算目标尺寸 if (dw > w || dh > h) { // 缩放 if (dw * h > w * dh) { // 视频的宽高比 > 播放器的宽高比 dh = w * dh / dw; dw = w; } else { dw = h * dw / dh; dh = h; } } // 居中 dx = (w - dw) >> 1; dy = (h - dh) >> 1; _rect = QRect(dx, dy, dw, dh); } update();//触发paintEvent方法 } void VideoWidget::paintEvent(QPaintEvent *event) { if (!_image) return; // 将图片绘制到当前组件上 QPainter(this).drawImage(_rect, *_image); }
这里的计算最终的尺寸我们可以参考之前介绍的《24_用Qt和FFmpeg实现简单的YUV播放器》
此时我们运行播放的时候,可以发现视频画面非常的快速的播放完了,此时我们先使用SDL_Delay(33);
控制播放速度,后面在进行音视频同步的时候在处理。
五、释放资源
void VideoPlayer::freeVideo(){ clearVideoPktList(); avcodec_free_context(&_vDecodeCtx); av_frame_free(&_vSwsInFrame); if (_vSwsOutFrame) { av_freep(&_vSwsOutFrame->data[0]); av_frame_free(&_vSwsOutFrame); } sws_freeContext(_vSwsCtx); _vSwsCtx = nullptr; _vStream = nullptr; }
我们实现释放资源后,再去运行后点击停止会出现内存错误。
这是因为我们在像素格式转换后,将_vSwsOutFrame->data[0]
数据直接发送给onPlayerFrameDecoded
方法的uint8_t *data
,这里就会牵扯到多线程同时访问一块内存区域(橡树转换sws_scale
是在子线程,渲染时在主线程)
解决办法就是把_vSwsOutFrame->data[0]
指向的RGB数据拷贝到另外一个内存空间
// 像素格式的转换 sws_scale(_vSwsCtx, _vSwsInFrame->data, _vSwsInFrame->linesize, 0, _vDecodeCtx->height, _vSwsOutFrame->data, _vSwsOutFrame->linesize); uint8_t *data = (uint8_t *)av_malloc(_vSwsOutSpec.size); memcpy(data, _vSwsOutFrame->data[0], _vSwsOutSpec.size); // 发出信号 emit frameDecoded(this,data,_vSwsOutSpec);
void VideoWidget::freeImage() { if (_image) { av_free(_image->bits()); delete _image; _image = nullptr; } }
六、细节处理
6.1 非音频、视频流补充av_packet_unref
6.2 很快读完了所有数据包,数据包过多
我们通过while
循环只要不是停止状态就拼命的读av_read_frame
,读到了就往里面塞数据包(addAudioPkt(pkt)
或addVideoPkt(pkt)
)。实际上就会发现这个段代码不管音视频多大很快就会读完,这样如果音视频非常大,一下载入到内存中就会有问题,所以需要做一下限制
#define AUDIO_MAX_PKT_SIZE 1000 #define VIDEO_MAX_PKT_SIZE 500 // 从输入文件中读取数据 AVPacket pkt; while (_state != Stopped) { if (_vPktList->size() >= VIDEO_MAX_PKT_SIZE || _aPktList->size() >= AUDIO_MAX_PKT_SIZE) { SDL_Delay(10); continue; } qDebug()<< _vPktList->size()<< _aPktList->size(); ...... }
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!