【FFmpeg视频播放器开发】视频和音频解码写入文件(二)

一、前言

由于 FFmpeg 是使用 C 语言开发,所有和函数调用都是面向过程的。所以这里全部代码先放在 main 函数中实现,经过测试和修改后功能正常,再以 C++ 面向对象的方式逐步将代码分解和封装。

二、效果展示

下面代码只先实现音视频解码,解码数据写入文件。解码后的 RGB 和 PCM 数据存放在工程目录下的 dove_640x360.rgb 和 dove.pcm 文件。

使用 yuvplayer 播放 RGB 文件,如下图所示:

FFmpeg_XPlay_A.png

使用 AudioConverter 软件播放 PCM 文件,如下图所示:

FFmpeg_XPlay_B.png

三、搭建开发环境

平台: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 下的SharedDev下载下来,解压后 Dev 的 include 里是它的头文件、lib 里是他的静态链接库,Shared 里的 bin 是它的 dll 和 .exe 程序。之后我们将它 Dev 里的 include、lib 和 Shared 里的 bin 拷贝出来形成如下图 2 所示。

【FFmpeg视频播放器开发】视频和音频解码写入文件(二)

【FFmpeg视频播放器开发】视频和音频解码写入文件(二)

四、代码实现

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:LearnFFmpegXPlayerXPlayer_1binwin32XPlayer_1.exe (进程 13840)已退出,代码为 0。
按任意键关闭此窗口. .    

六、代码下载

下载链接:https://github.com/confidentFeng/FFmpeg/tree/master/XPlayer/XPlayer_1

参考:

基于Qt、FFMpeg的音视频播放器设计一(准备环境)

Qt与FFmpeg联合开发指南(一)——解码(1):功能实现

Qt与FFmpeg联合开发指南(二)-- 解码本地视频

原文链接: https://www.cnblogs.com/linuxAndMcu/p/14603458.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

    【FFmpeg视频播放器开发】视频和音频解码写入文件(二)

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/209202

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月12日 下午11:46
下一篇 2023年2月12日 下午11:47

相关推荐