github地址提交记录与章节对应方便查看代码变化
1、FFmpeg6.0 和SDLwindows安装
1.1 下载ffmpeg
官网下载:https://ffmpeg.org/download.html
下载这个版本,gpl-shared,6.0-22,下载完成后解压文件
现在代码文件,方便后边使用https://github.com/FFmpeg/FFmpeg/tree/release/6.0
1.2 拷贝文件
将可执行文件拷贝到C:\Windows
dll文件拷贝到
C:\Windows\SysWOW64
(WoW64 (Windows On Windows64 )是一个Windows操作系统的子系统,被设计用来处理许多在32-bit Windows和64-bit Windows之间的不同的问题,使得可以在64-bit Windows中运行32-bit程序。)1.3 添加环境变量
在用户变量path里,添加上解压的ffmpeg的bin文件夹路径,确定
1.4 验证安装
重启电脑,Win+R输入cmd,输入ffmpeg -version
,成功显示安装成功
1.5 SDL2安装
下载SDL2-2.0.10 下载地址 ,解压,把dll文件放入C:\Windows\SysWOW64
2 qt测试ffmpeg调用
创建一个纯c的工程,吧ffmpeg代码放到项目文件夹下
选择MinGW64版本
在.pro文件里添加上ffmpeg的路径和库文件
win32{
INCLUDEPATH += E:\Environment\ffmpeg-6.0\include\
LIBS += E:\Environment\ffmpeg-6.0\lib\avformat.lib \
E:\Environment\ffmpeg-6.0\lib\avcodec.lib \
E:\Environment\ffmpeg-6.0\lib\avdevice.lib \
E:\Environment\ffmpeg-6.0\lib\avfilter.lib \
E:\Environment\ffmpeg-6.0\lib\avutil.lib \
E:\Environment\ffmpeg-6.0\lib\postproc.lib \
E:\Environment\ffmpeg-6.0\lib\swresample.lib \
E:\Environment\ffmpeg-6.0\lib\swscale.lib
}
引入头文件,添加上测试代码
#include <stdio.h>
#include "libavutil/avutil.h"
int main()
{
printf("Hello World! ffmpeg verson %s\n",av_version_info());
return 0;
}
测试成功,若是报错出现.dll确实,可以重启一下电脑,可能是环境变量的路径还没有识别
3 原理讲解(没学过这些东西,请教gpt老师)
这张图片描述了一个视频文件(time.mp4)的解码和播放流程。这是一个多线程的处理流程,通常用于媒体播放器软件中。这里的处理过程包括分离器(Demuxer)、解码器(Decoder)以及音视频同步(SDL,Simple DirectMedia Layer)的输出。下面是每个部分的解释:
(1)time.mp4:这是一个mp4视频文件,它包含视频和音频数据流。文件路径和基本信息显示在图中的左下角。
(2)mp4demuxer:这是一个解复用线程(DemuxThread),它从mp4文件中提取音视频数据。mp4demuxer负责从容器格式中分离出视频和音频流。
(3)AVPacketQueue:音频和视频数据在解复用后会被放入不同的队列中。视频数据是H264编码的,而音频数据是AAC编码的。这些队列存储数据包,等待被解码。
(4)DecodeThread:解码线程从队列中取出数据包,将它们解码成帧数据。有两个解码线程,一个用于音频,一个用于视频。解码后,视频数据包通常变成YUV格式的帧,音频数据包则变为PCM格式的帧。
(5)AVFrameQueue:解码后的帧会被放入另外一个队列,等待进一步的处理。视频帧队列和音频帧队列分别存储不同类型的数据。
(6)SDL:SDL是一个库,用于处理多媒体的输出。在这个流程中,SDL将被用来输出音频和视频。对于视频,YUV格式的帧数据将被SDL处理并输出到屏幕上。对于音频,PCM格式的帧数据将被SDL处理并通过扬声器播放。
整个流程显示了从一个视频文件开始,如何通过不同的处理步骤来最终在设备上播放音频和视频。这是一个典型的多线程应用,它允许音频和视频的解码同时进行,以保持播放的同步。在实际的软件实现中,还需要考虑到缓冲、错误处理、音视频同步等因素。
4 先实现ffmpeg播放视频
创建项目,MinGW64,对于c++,头文件的引入需要修改,执行看到可以运行
win32{
INCLUDEPATH += E:\Environment\ffmpeg-6.0\include\
LIBS += E:\Environment\ffmpeg-6.0\lib\avformat.lib \
E:\Environment\ffmpeg-6.0\lib\avcodec.lib \
E:\Environment\ffmpeg-6.0\lib\avdevice.lib \
E:\Environment\ffmpeg-6.0\lib\avfilter.lib \
E:\Environment\ffmpeg-6.0\lib\avutil.lib \
E:\Environment\ffmpeg-6.0\lib\postproc.lib \
E:\Environment\ffmpeg-6.0\lib\swresample.lib \
E:\Environment\ffmpeg-6.0\lib\swscale.lib
}
//需要这么引入
#ifndef __STDC_CONSTANT_MACROS
#define __STDC_CONSTANT_MACROS
#endif
#ifdef __cplusplus ///
extern "C"
{
#include "libavutil/avutil.h"
}
#endif
4.1 解复用实现
分配解码器上下文(avformat_alloc_context):初始化一个用于存储编解码器相关信息的结构体。
打开视频文件(avformat_open_input):通过给定的URL或文件路径打开视频文件。
获取流信息(avformat_find_stream_info):读取媒体文件中的流信息,如音频、视频等流的编码信息,会给音频和视频各奉陪一个需要我们要记录。
读取和定位流数据(在一个循环内部进行):
读取帧(av_read_frame):读取媒体流的下一帧数据。
定位操作(avformat_seek_file 和 av_seek_frame):在媒体文件中定位,可以快进、倒退到特定的帧。
关闭并释放资源(avformat_close_input):完成视频处理后,关闭文件并释放资源。
添加一个Thread的c++文件
实现了简单的线程管理类
class Thread
{
public:
Thread(); // 构造函数
~Thread(); // 析构函数
int Start(); // 启动线程
int Stop(); // 停止线程
void test_main();
virtual void Run() = 0;
protected:
int abort_ = 0; // 标记线程是否应该停止
std::thread *thread_ = nullptr; // 线程对象指针,默认为 nullptr
};
Thread::~Thread()
{
if(thread_)
{
Thread::Stop(); // 在析构函数中停止线程
}
}
int Thread::Start()
{
// 在此处添加启动线程的代码
// 返回适当的值以指示启动是否成功
}
int Thread::Stop()
{
abort_ = 1; // 设置停止标记为真
if(thread_)
{
thread_->join(); // 等待线程结束
delete thread_; // 删除线程对象
thread_ = nullptr; // 将线程指针设置为 nullptr
}
// 返回适当的值以指示停止是否成功
}
void Thread::test_main()
{
qDebug()<<"Hello";
}
添加c++文件DemuxThread,用来解复用
在thread。cpp里添加上,方便测试
void Thread::test_main()
{
qDebug()<<"Hello test main";
//1、解复用
DemuxThread demux_thread;
demux_thread.Init("test.mp4");
}
头文件代码,后边可以先传一个url来测试是否调用了析构和构造
#ifndef DEMUXTHREAD_H
#define DEMUXTHREAD_H
#include "thread.h"
#include <QDebug>
#ifndef __STDC_CONSTANT_MACROS
#define __STDC_CONSTANT_MACROS
#endif
#ifdef __cplusplus ///
extern "C"
{
#include "libavutil/avutil.h"
#include "libavformat/avformat.h"
}
#endif
class DemuxThread
{
public:
DemuxThread(); // 构造函数
void Init(const char *url); // 初始化方法,使用给定的 URL
int Start(); // 开始解复用方法
int Stop(); // 停止解复用方法
void Run(); // 运行解复用过程的方法
private:
std::string url_; // 要解复用的文件名或 URL
AVFormatContext *ifmt_ctx_ = NULL; // 用于解复用的 AVFormatContext 结构体指针
};
#endif // DEMUXTHREAD_H
我们在init函数中获取视频和音频流,以及视频的相关信息。把一个有视频和声音的视频重命名放到编译的路径下,运行代码我们就能看到相关信息。
int DemuxThread::Init(const char *url)
{
qDebug() << "初始化:"<< url; // 打印初始化的URL
int ret = 0;
url_ = url;
ifmt_ctx_ = avformat_alloc_context(); // 分配 AVFormatContext 结构体内存
ret = avformat_open_input(&ifmt_ctx_, url_.c_str(), NULL, NULL); // 打开输入流
if (ret != 0)
{
char errbuf[AV_ERROR_MAX_STRING_SIZE]; // 用于存储错误信息的缓冲区
av_strerror(ret, errbuf, sizeof(errbuf)); // 将错误码转换为错误消息字符串
qDebug() << "打开输入时发生错误:" << errbuf; // 输出错误信息
return -1; // 返回错误码表示初始化失败
}
ret = avformat_find_stream_info(ifmt_ctx_, NULL); // 查找流信息
if (ret != 0)
{
char errbuf[AV_ERROR_MAX_STRING_SIZE]; // 用于存储错误信息的缓冲区
av_strerror(ret, errbuf, sizeof(errbuf)); // 将错误码转换为错误消息字符串
qDebug() << "查找流信息时发生错误:" << errbuf; // 输出错误信息
return -1; // 返回错误码表示初始化失败
}
av_dump_format(ifmt_ctx_, 0, url_.c_str(), 0); // 打印输入格式信息
audio_index_ = av_find_best_stream(ifmt_ctx_, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0); // 查找最佳音频流索引
video_index_ = av_find_best_stream(ifmt_ctx_, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); // 查找最佳视频流索引
qDebug() << QString("音频流索引:%1,视频流索引:%2").arg(audio_index_).arg(video_index_);
if (audio_index_ < 0 || video_index_ < 0)
{
return -1; // 如果找不到音频或视频流,返回错误码表示初始化失败
}
}
接着编写run函数,他需要读取abort_,这是Thread类的保护变量,所以需要继承
class DemuxThread : public Thread
,这样我们就可以访问abort_变量。Run函数是主要的解复用循环,它不断地从输入流中读取数据包,直到收到终止信号。
void DemuxThread::Run()
{
int ret = 0;
AVPacket pkt;
// 循环读取数据包直到终止信号被设置
while (abort_ != 1)
{
// 读取一个数据包
ret = av_read_frame(ifmt_ctx_, &pkt);
if (ret < 0)
{
char errbuf[AV_ERROR_MAX_STRING_SIZE]; // 用于存储错误信息的缓冲区
av_strerror(ret, errbuf, sizeof(errbuf)); // 将错误码转换为错误消息字符串
qDebug() << "读取数据包时发生错误:" << errbuf; // 输出错误信息
break; // 如果读取失败,退出循环
}
}
}
Start 方法创建了一个新线程来运行 Run 方法,如果创建线程失败则返回错误码;Stop 方法调用了基类 Thread 的停止方法,并关闭了输入流
int DemuxThread::Start()
{
// 创建新线程来运行 Run 方法
thread_ = new std::thread(&DemuxThread::Run, this);
if (!thread_)
{
qDebug() << "创建线程失败";
return -1; // 返回错误码表示启动失败
}
return 0; // 返回成功码表示启动成功
}
int DemuxThread::Stop()
{
Thread::Stop(); // 调用基类 Thread 的停止方法
avformat_close_input(&ifmt_ctx_); // 关闭输入流
}
再到testmain里添加运行代码,启动程序,发现能够读取视频流和音频流
int Thread::test_main()
{
qDebug() << "测试主函数开始";
int ret = 0;
//1、解复用
DemuxThread demux_thread;
ret = demux_thread.Init("test.mp4");
if (ret < 0)
{
qDebug() << "初始化失败";
return -1;
}
ret = demux_thread.Start();
if (ret < 0)
{
qDebug() << "启动失败";
return -1;
}
std::this_thread::sleep_for(std::chrono::milliseconds(10000)); // 休眠10秒
demux_thread.Stop();
}