• 【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 所示。


    四、代码实现

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

    六、代码下载

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


    参考:

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

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

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


  • 相关阅读:
    vmware workstation 10.0
    成为嵌入式程序员应知道的0x10个基本问题
    Linux嵌入式系统与硬件平台的关系
    vasprintf的实现
    GIT常用命令
    ip地址转化代码实例
    Linux网络编程实例解析
    openwrt的交叉编译
    appium初学者,使用之检查appium环境报错Could not detect Mac OS X Version from sw_vers output: '10.12.1’,
    第二章 mac上运行第一个appium实例
  • 原文地址:https://www.cnblogs.com/linuxAndMcu/p/14603458.html
Copyright © 2020-2023  润新知