侧边栏
首页代码

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->datadata[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();
......
}

源码链接

posted @   咸鱼Jay  阅读(189)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!
页脚HTML代码
点击右上角即可分享
微信分享提示
电磁波切换