目录
一、简介
上节介绍了使用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();
......
}
标签:播放器,33,解码,音视频,vSwsOutFrame,像素,vSwsOutSpec,data,image
From: https://www.cnblogs.com/zuojie/p/16870625.html