大家好,又见面了,我是你们的朋友全栈君。
背景
因为在2021年给自己定了目标和计划,学习ffmpeg,所以这篇文章是实现计划的第一步。
ffmpeg 众所周知,就不展开介绍了,下面给出 FFmpeg 4.2 windows x64 lib库和头文件的下载地址(粉丝免积分下载):
https://download.csdn.net/download/u012534831/14045436
本文也是属于博主的入门学习总结与分享,因此我们先从ffmpeg的软解码开始,从解码到绘制,一起体验下亲自动手的快乐。本文的语言环境基于C++,界面部分是 QT。
流程分析
在开始看代码之前,我们必须先了解下ffmpeg软解的常规流程:
在以前的教程中我们经常见到av_regeister_all
,这是旧版ffmpeg的用法,必须在开始进行初始化,新版的ffmpeg4.0之后已经不需要了,详见github: av_register_all() has been deprecated in ffmpeg 4.0。
1、avformat_open_input
为 AVFormatContext 分配空间,打开输入的视频数据并且探测视频的格式,这个函数里面包含了复杂的格式解析与探测算法,可解析的内容包括:视频流、音频流、视频流参数、音频流参数、视频帧索引等。用雷神的话说就是 可以算作FFmpeg的“灵魂”
。
2、avformat_find_stream_info
获取多媒体流的信息,包括码流、帧率、时长等信息。但是有些早期格式或者裸流数据它的索引并没有放到头当中,因此需要在后面进行探测。注意一个视频文件中可能会同时包括视频文件、音频文件、字幕文件等多个媒体流。
3、av_find_best_stream
当视频被解封装出来后,需要分开处理音频和视频,需要找到对应的音频流和视频流,获取音视频对应的stream_index。
4、avcodec_find_decoder(enum AVCodecID id)
“Find a registered decoder with a matching codec ID.”
上一步找到的AVStream中的成员变量 codecpar->codec_id 就是这儿的参数 ID,codecpar类型为AVCodecParameters。网上的很多资料为 AVCodecContext->codec_id,这个用法在FFMPEG3.4及以上版本已经被弃用了,官方推荐使用codecpar。
5、avcodec_alloc_context3
创建AVCodecContext并分配空间。
6、avcodec_parameters_to_context
该函数用于将流里面的参数,也就是AVStream里面的参数直接复制到AVCodecContext的上下文当中,执行真正的内容拷贝。avcodec_parameters_to_context()是新的API,替换了旧版本的avcodec_copy_context()。
7、avcodec_open2
用给定的 AVCodec 去初始化 AVCodecContext。
到这儿,解码器的初始化工作已经完成。下面就可以开始真正的解码操作了。
8、av_read_frame
读取码流中的音频若干帧或者视频一帧,av_read_frame()函数是新型ffmpeg的用法,对 av_read_packet 进行了封装,旧用法之所以被抛弃,就是因为以前获取的数据可能不是完整的,而av_read_frame()保证了视频数据一帧的完整性,使读出的数据总是完整的一帧。
8、avcodec_send_packet
发送数据到后台解码队列。
It can be NULL (or an AVPacket with data set to NULL and
size set to 0); in this case, it is considered a flush
packet, which signals the end of the stream. Sending the
first flush packet will return success. Subsequent ones are
unnecessary and will return AVERROR_EOF. If the decoder
still has frames buffered, it will return them after sending
源码中关于发送一包空数据的解释:
由于ffmpeg内部会缓存帧,在av_read_frame读不到数据的时候,需要通过packet.data = NULL;packet.size = 0;
给ffmpeg发送一包空数据,即再avcodec_send_packet
一次,将ffmpeg里面缓存的帧全部刷出来,解决最后几帧没有解码出来的问题。
9、avcodec_receive_frame
从解码器读取帧数据,这个函数执行完后,就已经能拿到我们的帧数据了,它被存储在 AVFrame 中。
此处需要注意的是:
一般而言,一次avcodec_send_packet()对应一次avcodec_receive_frame(),但是也会有一次对应多次的情况。这个得看具体的流,并常见于音频流,会存在一个AVPacket对应多个AVFrame的情况。因此可以看我上面的流程图有两个while循环。
代码示例
//头文件
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include<thread>
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavutil/pixfmt.h"
#include "libswscale/swscale.h"
#include "libavdevice/avdevice.h"
#include <libavutil/pixdesc.h>
#include <libavutil/hwcontext.h>
#include <libavutil/opt.h>
#include <libavutil/avassert.h>
#include <libavutil/imgutils.h>
}
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
void init();
void play();
private:
std::thread m_decodecThread;
Ui::MainWindow *ui;
AVFormatContext *pAVFormatCtx;
AVCodecContext *pAVCodecCtx;
SwsContext *pSwsCtx;
uint8_t *pRgbBuffer;
AVPacket packet;
AVFrame *pAVFrame = NULL;
AVFrame *pAVFrameRGB;
int iVideoIndex = -1;
QImage m_image;
bool isFinish =false;
void decodec();
signals:
void signalDraw();
public slots:
void slotDraw();
protected:
void paintEvent(QPaintEvent *event) override;
};
#endif // MAINWINDOW_H
//CPP文件
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDebug>
#include <QPainter>
#include<thread>
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
connect(this,&MainWindow::signalDraw,this,&MainWindow::slotDraw);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::init()
{
std::string file = "E:/Video/bb.mp4";
//描述多媒体文件的构成及其基本信息
if (avformat_open_input(&pAVFormatCtx, file.data(), NULL, NULL) != 0)
{
qDebug() <<"open file fail";
avformat_free_context(pAVFormatCtx);
return;
}
//读取一部分视音频数据并且获得一些相关的信息
if (avformat_find_stream_info(pAVFormatCtx, NULL) < 0)
{
qDebug() <<"vformat find stream fail";
avformat_close_input(&pAVFormatCtx);
return;
}
// 根据解码器枚举类型找到解码器
AVCodec *pAVCodec;
int ret = av_find_best_stream(pAVFormatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, &pAVCodec, 0);
if (ret < 0) {
qDebug()<< "av_find_best_stream faliture";
avformat_close_input(&pAVFormatCtx);
return;
}
iVideoIndex = ret;
pAVCodec = avcodec_find_decoder(pAVFormatCtx->streams[iVideoIndex]->codecpar->codec_id);
if (pAVCodec == NULL)
{
qDebug()<<"not find decoder";
return;
}
qDebug()<<"avcodec_open2 pAVCodec->name:" << QString::fromStdString(pAVCodec->name);
if(pAVFormatCtx->streams[iVideoIndex]->avg_frame_rate.den != 0) {
float fps_ = pAVFormatCtx->streams[iVideoIndex]->avg_frame_rate.num / pAVFormatCtx->streams[iVideoIndex]->avg_frame_rate.den;
qDebug() <<"fps:" << fps_;
}
int64_t video_length_sec_ = pAVFormatCtx->duration/AV_TIME_BASE;
qDebug() <<"video_length_sec_:" << video_length_sec_;
pAVCodecCtx = avcodec_alloc_context3(pAVCodec);
if (pAVCodecCtx == NULL)
{
qDebug() <<"get pAVCodecCtx fail";
avformat_close_input(&pAVFormatCtx);
return;
}
ret = avcodec_parameters_to_context(pAVCodecCtx,pAVFormatCtx->streams[iVideoIndex]->codecpar);
if (ret < 0)
{
qDebug() <<"avcodec_parameters_to_context fail";
avformat_close_input(&pAVFormatCtx);
return;
}
if (avcodec_open2(pAVCodecCtx, pAVCodec, NULL) < 0)
{
qDebug()<<"avcodec_open2 fail";
return;
}
//为解码帧分配内存
//AVFrame 存放从AVPacket中解码出来的原始数据
pAVFrame = av_frame_alloc();
pAVFrameRGB = av_frame_alloc();
//用于视频图像的转换,将源数据转换为RGB32的目标数据
pSwsCtx = sws_getContext(pAVCodecCtx->width, pAVCodecCtx->height, pAVCodecCtx->pix_fmt,
pAVCodecCtx->width, pAVCodecCtx->height, AV_PIX_FMT_RGB32,
SWS_BICUBIC, NULL, NULL, NULL);
int m_size = av_image_get_buffer_size(AVPixelFormat(AV_PIX_FMT_RGB32), pAVCodecCtx->width, pAVCodecCtx->height, 1);
pRgbBuffer = (uint8_t *)(av_malloc(m_size));
//为已经分配的空间的结构体AVPicture挂上一段用于保存数据的空间
avpicture_fill((AVPicture *)pAVFrameRGB, pRgbBuffer, AV_PIX_FMT_BGR32, pAVCodecCtx->width, pAVCodecCtx->height);
//av_image_fill_arrays
//AVpacket 用来存放解码数据
av_new_packet(&packet, pAVCodecCtx->width * pAVCodecCtx->height);
}
void MainWindow::play()
{
m_decodecThread = std::thread([this]()
{
decodec();
});
m_decodecThread.detach();
}
void MainWindow::decodec()
{
//读取码流中视频帧
while (true)
{
int ret = av_read_frame(pAVFormatCtx, &packet);
if(ret != 0)
{
qDebug()<<"file end";
isFinish = !isFinish;
return;
}
if (packet.stream_index != iVideoIndex)
{
av_packet_unref(&packet);
continue;
}
int iGotPic = AVERROR(EAGAIN);
// //解码一帧视频数据
iGotPic = avcodec_send_packet(pAVCodecCtx, &packet);
if(iGotPic!=0){
qDebug()<<"avcodec_send_packet error";
continue;
}
iGotPic = avcodec_receive_frame(pAVCodecCtx, pAVFrame);
if(iGotPic == 0){
//转换像素
sws_scale(pSwsCtx, (uint8_t const * const *)pAVFrame->data, pAVFrame->linesize, 0, pAVCodecCtx->height, pAVFrameRGB->data, pAVFrameRGB->linesize);
//构造QImage
QImage img(pRgbBuffer, pAVCodecCtx->width, pAVCodecCtx->height, QImage::Format_RGB32);
qDebug()<<"decode img";
m_image = img;
emit signalDraw();
}else {
qDebug()<<"decode error";
}
av_packet_unref(&packet);
std::this_thread::sleep_for(std::chrono::milliseconds(25));
}
//资源回收
av_free(pAVFrame);
av_free(pAVFrameRGB);
sws_freeContext(pSwsCtx);
avcodec_close(pAVCodecCtx);
avformat_close_input(&pAVFormatCtx);
}
void MainWindow::slotDraw()
{
update();
}
void MainWindow::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
painter.setBrush(Qt::black);
painter.drawRect(0, 0, this->width(), this->height());
if (m_image.size().width() <= 0)
return;
//比例缩放
QImage img = m_image.scaled(this->size(),Qt::KeepAspectRatio);
int x = this->width() - img.width();
int y = this->height() - img.height();
x /= 2;
y /= 2;
//QPoint(x,y)为中心绘制图像
painter.drawImage(QPoint(x,y),img);
}
如有兴趣,欢迎加入我的QQ群,QT/Android/音视频 问题在线解答,资源分享。
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/153390.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...