zoukankan      html  css  js  c++  java
  • 【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联合开发指南(二)-- 解码本地视频


  • 相关阅读:
    Android自定义之仿360Root大师水纹效果
    Android之TextView的Span样式源码剖析
    Android之TextView的样式类Span的使用详解
    随着ScrollView的滑动,渐渐的执行动画View
    仿微信主界面导航栏图标字体颜色的变化
    android自定义之 5.0 风格progressBar
    Android性能优化之内存篇
    Android性能优化之运算篇
    How to install Zabbix5.0 LTS version with Yum on the CentOS 7.8 system?
    How to install Zabbix4.0 LTS version with Yum on the Oracle Linux 7.3 system?
  • 原文地址:https://www.cnblogs.com/linuxAndMcu/p/14603458.html
Copyright © 2011-2022 走看看