【FFmpeg视频播放器开发】视频和音频解码写入文件(二)
一、前言
由于 FFmpeg 是使用 C 语言开发,所有和函数调用都是面向过程的。所以这里全部代码先放在 main 函数中实现,经过测试和修改后功能正常,再以 C++ 面向对象的方式逐步将代码分解和封装。
二、效果展示
下面代码只先实现音视频解码,解码数据写入文件。解码后的 RGB 和 PCM 数据存放在工程目录下的 dove_640x360.rgb 和 dove.pcm 文件。
使用 yuvplayer 播放 RGB 文件,如下图所示:
使用 AudioConverter 软件播放 PCM 文件,如下图所示:
三、搭建开发环境
平台:Windows
IDE:VS2019 + Qt5.15.2
编译器:MSVC2017_64
FFmpeg版本:Vcpkg的最新版本(FFmpeg 4.3.2)
VS2109 和 Qt 的安装可以参考:VS2019 Qt5.15.2 开发环境搭建
Vcpkg 部署 FFmpeg 库可以参考:C++开源库 - 包管理工具Vcpkg安装使用教程
- 如果不想使用 Vcpkg 安装 FFmpeg 库,源码内也存放了个 3.xx 版本的 FFmpeg 库,添加到 include 和 lib 依赖路径即可使用。
- FFmpeg 的传统安装方法参考下面。
FFmpeg安装
FFmpeg 下载地址:
点击上面地址后弹出界面如下图 1 所示,然后选择Windows 32-bit
的 FFmpeg,当然你也可以选择 64 位的,不过我选择的是 32 位。
之后我们需要将它右侧 Linking 下的Shared
和Dev
下载下来,解压后 Dev 的 include 里是它的头文件、lib 里是他的静态链接库,Shared 里的 bin 是它的 dll 和 .exe 程序。之后我们将它 Dev 里的 include、lib 和 Shared 里的 bin 拷贝出来形成如下图 2 所示。
四、代码实现
VS2019 新建一个 Win32 控制台空项目,添加一个 main.cpp 文件。输出路径设置为../bin/win64/
,中间目录设置为../bin/win64/obj/
。main 函数中的全部代码在下面。
步骤0:准备工作
#include <iostream>
#include <fstream>
extern "C" {
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
#include "libswscale/swscale.h"
#include "libswresample/swresample.h"
}
// 传统安装方法需要
#pragma comment(lib,"avformat.lib")
#pragma comment(lib,"avutil.lib")
#pragma comment(lib,"avcodec.lib")
#pragma comment(lib,"swscale.lib")
#pragma comment(lib,"swresample.lib")
using namespace std;
static double r2d(AVRational r)
{
return r.den == 0 ? 0 : (double)r.num / (double)r.den;
}
int main(int argc, char* argv[])
{
// 打开rgb文件
FILE* outFileRgb = fopen("../bin/win64/dove_640x360.rgb", "wb");
if (outFileRgb == NULL) {
cout << "file not exist!" << endl;
return false;
}
// 打开pcm文件
FILE* outFilePcm = fopen("../bin/win64/dove.pcm", "wb");
if (outFilePcm == NULL) {
cout << "file not exist!" << endl;
return false;
}
// ....(省略下面代码)
}
步骤1:打开视频文件、探测获取流信息
//===================1、打开视频文件===================
const char* path = "dove_640x360.mp4";
// 参数设置
AVDictionary* opts = NULL;
// 设置rtsp流已tcp协议打开
av_dict_set(&opts, "rtsp_transport", "tcp", 0);
// 网络延时时间
av_dict_set(&opts, "max_delay", "500", 0);
// 解封装上下文
AVFormatContext* pFormatCtx = NULL;
int nRet = avformat_open_input(
&pFormatCtx,
path,
0, // 0表示自动选择解封器
&opts // 参数设置,比如rtsp的延时时间
);
if (nRet != 0)
{
char buf[1024] = { 0 };
av_strerror(nRet, buf, sizeof(buf) - 1);
cout << "open " << path << " failed! :" << buf << endl;
return -1;
}
cout << "open " << path << " success! " << endl;
// 探测获取流信息
nRet = avformat_find_stream_info(pFormatCtx, 0);
// 获取媒体总时长,单位为毫秒
int totalMs = pFormatCtx->duration / (AV_TIME_BASE / 1000);
cout << "totalMs = " << totalMs << endl;
// 打印视频流详细信息
av_dump_format(pFormatCtx, 0, path, 0);
步骤2:获取音视频流索引
//===================2、获取音视频流索引===================
int nVStreamIndex = -1; // 视频流索引(读取时用来区分音视频)
int nAStreamIndex = -1; // 音频流索引
// 获取视频流索引(新版本方法:使用av_find_best_stream函数)
nVStreamIndex = av_find_best_stream(pFormatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
if (nVStreamIndex == -1) {
cout << "find videoStream failed!" << endl;
return -1;
}
// 打印视频信息(这个pStream只是指向pFormatCtx的成员,未申请内存,为栈指针无需释放,下面同理)
AVStream* pVStream = pFormatCtx->streams[nVStreamIndex];
cout << "=======================================================" << endl;
cout << "VideoInfo: " << nVStreamIndex << endl;
cout << "codec_id = " << pVStream->codecpar->codec_id << endl;
cout << "format = " << pVStream->codecpar->format << endl;
cout << "width=" << pVStream->codecpar->width << endl;
cout << "height=" << pVStream->codecpar->height << endl;
// 帧率 fps 分数转换
cout << "video fps = " << r2d(pVStream->avg_frame_rate) << endl;
// 帧率 fps 分数转换
cout << "video fps = " << r2d(pFormatCtx->streams[nVStreamIndex]->avg_frame_rate) << endl;
// 获取音频流索引
nAStreamIndex = av_find_best_stream(pFormatCtx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
if (nVStreamIndex == -1) {
cout << "find audioStream failed!" << endl;
return -1;
}
// 打印音频信息
AVStream* pAStream = pFormatCtx->streams[nAStreamIndex];
cout << "=======================================================" << endl;
cout << "AudioInfo: " << nAStreamIndex << endl;
cout << "codec_id = " << pAStream->codecpar->codec_id << endl;
cout << "format = " << pAStream->codecpar->format << endl;
cout << "sample_rate = " << pAStream->codecpar->sample_rate << endl;
// AVSampleFormat;
cout << "channels = " << pAStream->codecpar->channels << endl;
// 一帧数据?? 单通道样本数
cout << "frame_size = " << pAStream->codecpar->frame_size << endl;
这里使用av_find_best_stream
来获取音视频索引,而不是遍历查找方法,更加方便且效率更高,推荐使用。
步骤3:打开音视频解码器
//===================3、打开视频解码器===================
// 根据codec_id找到视频解码器
AVCodec* pVCodec = avcodec_find_decoder(pVStream->codecpar->codec_id);
if (!pVCodec)
{
cout << "can't find the codec id " << pVStream->codecpar->codec_id;
return -1;
}
cout << "find the AVCodec " << pVStream->codecpar->codec_id << endl;
// 创建视频解码器上下文
AVCodecContext* pVCodecCtx = avcodec_alloc_context3(pVCodec);
// 配置视频解码器上下文参数
avcodec_parameters_to_context(pVCodecCtx, pVStream->codecpar);
// 八线程视频解码
pVCodecCtx->thread_count = 8;
// 打开视频解码器上下文
nRet = avcodec_open2(pVCodecCtx, 0, 0);
if (nRet != 0)
{
char buf[1024] = { 0 };
av_strerror(nRet, buf, sizeof(buf) - 1);
cout << "avcodec_open2 failed! :" << buf << endl;
return -1;
}
cout << "video avcodec_open2 success!" << endl;
//===================3、打开音频解码器===================
// 找到音频解码器
AVCodec* pACodec = avcodec_find_decoder(pFormatCtx->streams[nAStreamIndex]->codecpar->codec_id);
if (!pACodec)
{
cout << "can't find the codec id " << pFormatCtx->streams[nAStreamIndex]->codecpar->codec_id;
return -1;
}
cout << "find the AVCodec " << pFormatCtx->streams[nAStreamIndex]->codecpar->codec_id << endl;
// 创建音频解码器上下文
AVCodecContext* pACodecCtx = avcodec_alloc_context3(pACodec);
// /配置音频解码器上下文参数
avcodec_parameters_to_context(pACodecCtx, pFormatCtx->streams[nAStreamIndex]->codecpar);
// 八线程音频解码
pACodecCtx->thread_count = 8;
// 打开音频解码器上下文
nRet = avcodec_open2(pACodecCtx, 0, 0);
if (nRet != 0)
{
char buf[1024] = { 0 };
av_strerror(nRet, buf, sizeof(buf) - 1);
cout << "avcodec_open2 failed! :" << buf << endl;
return -1;
}
cout << "audio avcodec_open2 success!" << endl;
步骤4:循环解码前初始化各缓冲区
//===================4、循环解码前初始化各缓冲区===================
// malloc AVPacket并初始化
AVPacket* pkt = av_packet_alloc();
AVFrame* frame = av_frame_alloc();
// 像素格式和尺寸转换上下文
SwsContext* vSwsCtx = NULL;
unsigned char* rgb = NULL;
// 音频重采样 上下文初始化
SwrContext* actx = swr_alloc();
actx = swr_alloc_set_opts(actx,
av_get_default_channel_layout(2), // 输出格式
AV_SAMPLE_FMT_S16, // 输出样本格式
pACodecCtx->sample_rate, // 输出采样率
av_get_default_channel_layout(pACodecCtx->channels), // 输入格式
pACodecCtx->sample_fmt,
pACodecCtx->sample_rate,
0, 0
);
// 初始化音频采样数据上下文
nRet = swr_init(actx);
if (nRet != 0)
{
char buf[1024] = { 0 };
av_strerror(nRet, buf, sizeof(buf) - 1);
cout << "swr_init failed! :" << buf << endl;
return -1;
}
unsigned char* pcm = NULL;
// 缓冲区大小 = 采样率(44100HZ) * 采样精度(16位 = 2字节)
int MAX_AUDIO_SIZE = 44100 * 2;
uint8_t* out_audio = (uint8_t*)av_malloc(MAX_AUDIO_SIZE);;
// 获取输出的声道个数
int out_nb_channels = av_get_channel_layout_nb_channels(AV_CH_LAYOUT_STEREO);
步骤5:解码
//===================5、开始循环解码===================
while(1)
{
int nRet = av_read_frame(pFormatCtx, pkt);
if (nRet != 0)
{
#if 0
// 循环"播放"
cout << "==============================end==============================" << endl;
int ms = 3000; // 三秒位置 根据时间基数(分数)转换
long long pos = (double)ms / (double)1000 * r2d(ic->streams[pkt->stream_index]->time_base);
av_seek_frame(ic, nVStreamIndex, pos, AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_FRAME);
continue;
#else
// "播放"完一次之后退出
break;
#endif
}
cout << "pkt->size = " << pkt->size << endl;
// 显示的时间
cout << "pkt->pts = " << pkt->pts << endl;
// 转换为毫秒,方便做同步
cout << "pkt->pts ms = " << pkt->pts * (r2d(pFormatCtx->streams[pkt->stream_index]->time_base) * 1000) << endl;
// 解码时间
cout << "pkt->dts = " << pkt->dts << endl;
AVCodecContext* cc = 0;
if (pkt->stream_index == nVStreamIndex)
{
cout << "图像" << endl;
cc = pVCodecCtx;
}
if (pkt->stream_index == nAStreamIndex)
{
cout << "音频" << endl;
cc = pACodecCtx;
}
// 解码视频
// 发送packet到解码线程 send传NULL后调用多次receive取出所有缓冲帧
nRet = avcodec_send_packet(cc, pkt);
// 释放,引用计数-1 为0释放空间
av_packet_unref(pkt);
if (nRet != 0)
{
char buf[1024] = { 0 };
av_strerror(nRet, buf, sizeof(buf) - 1);
cout << "avcodec_send_packet failed! :" << buf << endl;
continue;
}
for (;;)
{
// 从线程中获取解码接口,一次send可能对应多次receive
nRet = avcodec_receive_frame(cc, frame);
if (nRet != 0) break;
cout << "recv frame " << frame->format << " " << frame->linesize[0] << endl;
// 视频
if (cc == pVCodecCtx)
{
vSwsCtx = sws_getCachedContext(
vSwsCtx, // 传NULL会新创建
frame->width, frame->height, // 输入的宽高
(AVPixelFormat)frame->format, // 输入格式 YUV420p
frame->width, frame->height, // 输出的宽高
AV_PIX_FMT_RGBA, // 输出格式RGBA
SWS_BILINEAR, // 尺寸变化的算法
0, 0, 0);
// if(vSwsCtx)
// cout << "像素格式尺寸转换上下文创建或者获取成功!" << endl;
// else
// cout << "像素格式尺寸转换上下文创建或者获取失败!" << endl;
if (vSwsCtx)
{
// RGB缓冲区分配内存,只第一次分配
//(当然也可以创建pFrameRGB,用avpicture_fill初始化pFrameRGB来实现)
if (!rgb) rgb = new unsigned char[frame->width * frame->height * 4];
uint8_t* data[2] = { 0 };
data[0] = rgb;
int lines[2] = { 0 };
lines[0] = frame->width * 4;
// 类型转换:YUV转换成RGB
nRet = sws_scale(vSwsCtx,
frame->data, // 输入数据
frame->linesize, // 输入行大小
0,
frame->height, // 输入高度
data, // 输出数据和大小
lines
);
cout << "sws_scale = " << nRet << endl;
// 将数据以二进制的形式写入文件中
fwrite(data[0], frame->width* frame->height * 4, 1, outFileRgb);
}
}
else // 音频
{
// 创建音频采样缓冲区
uint8_t* data[2] = { 0 };
if (!pcm) pcm = new uint8_t[frame->nb_samples * 2 * 2];
data[0] = pcm;
// 类型转换:转换成PCM
nRet = swr_convert(actx,
data, frame->nb_samples, // 输出
(const uint8_t**)frame->data, frame->nb_samples // 输入
);
cout << "swr_convert = " << nRet << endl;
// 获取缓冲区实际存储大小
int out_buffer_size = av_samples_get_buffer_size(NULL, out_nb_channels, frame->nb_samples,
AV_SAMPLE_FMT_S16, 1);
// 将数据以二进制的形式写入文件中
fwrite(data[0], 1, out_buffer_size, outFilePcm);
}
}
}
步骤6:内存释放
//===================6、内存释放===================
fclose(outFileRgb);
fclose(outFilePcm);
av_frame_free(&frame);
av_packet_free(&pkt);
if (pFormatCtx)
{
// 释放封装上下文,并且把ic置0
avformat_close_input(&pFormatCtx);
}
五、打印音视频流信息
如果是使用传统安装方法,在运行前要将 bin 目录下的 dll 文件拷贝到编译生成的 exe 所在的目录下,否则会提示:程序异常结束
,无法运行。原因是缺少库文件。编译时,提前设置好库路径即可,但运行时的路径和编译时的路径往往不一样,这样就导致运行时找不到库文件,需要将库文件拷贝至运行路径下才行。
打印出的音频流和视频流信息如下:
open dove_640x360.mp4 success!
totalMs = 15060
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'dove_640x360.mp4':
Metadata:
major_brand : isom
minor_version : 1
compatible_brands: isom
creation_time : 2015-06-30T08:50:41.000000Z
copyright :
copyright-eng :
Duration: 00:00:15.06, start: 0.000000, bitrate: 470 kb/s
Stream #0:0(und): Video: h264 (Main) (avc1 / 0x31637661), yuv420p, 640x360 [SAR 1:1 DAR 16:9], 418 kb/s, 24 fps, 24 tbr, 24k tbn, 48 tbc (default)
Metadata:
creation_time : 2015-06-30T08:50:40.000000Z
handler_name : TrackHandler
Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 49 kb/s (default)
Metadata:
creation_time : 2015-06-30T08:50:40.000000Z
handler_name : Sound Media Handler
=======================================================
VideoInfo: 0
codec_id = 28
format = 0
width=640
height=360
video fps = 24
video fps = 24
=======================================================
AudioInfo: 1
codec_id = 86018
format = 8
sample_rate = 48000
channels = 2
frame_size = 1024
find the AVCodec 28
video avcodec_open2 success!
find the AVCodec 86018
audio avcodec_open2 success!
pkt->size = 18908
pkt->pts = 0
pkt->pts ms = 0
pkt->dts = -2000
图像
pkt->size = 73
pkt->pts = 1000
pkt->pts ms = 41.6667
pkt->dts = -1000
图像
pkt->size = 5607
pkt->pts = 5000
pkt->pts ms = 208.333
pkt->dts = 0
// ...调试输出信息太多,这里省略部分
音频
recv frame 8 8192
swr_convert = 1024
pkt->size = 21
pkt->pts = 1024
pkt->pts ms = 21.3333
pkt->dts = 1024
音频
recv frame 8 8192
swr_convert = 1024
pkt->size = 10
pkt->pts = 2048
pkt->pts ms = 42.6667
pkt->dts = 2048
// ...省略下方全部调试信息
E:\Learn\FFmpeg\XPlayer\XPlayer_1\bin\win32\XPlayer_1.exe (进程 13840)已退出,代码为 0。
按任意键关闭此窗口. .
六、代码下载
下载链接:https://github.com/confidentFeng/FFmpeg/tree/master/XPlayer/XPlayer_1
参考:
Qt与FFmpeg联合开发指南(一)——解码(1):功能实现
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!