我们经常需要知道一个媒体文件所包含的媒体流的信息,比如文件格式、播放时长、码率、视音频编码格式,视频分辨率,帧率,音频属性等信息。如何使用FFmpeg API获取这些信息呢?下面我会给出一个完善的类,这个类封装了FFmpeg读取文件信息的相关的API,读者只需要调类的方法就可以获得相关的信息。
这个类能够读取媒体文件的哪些信息呢?假如我们给出一个媒体文件(MP4,AVI,MKV。。。),里面至少要包含一个音频轨或视频轨,则调用该类的方法可以获得的信息如下:
我自己写了一个工具来调用这个类,能够批量获取一个目录里的媒体文件的信息,看下图:
(这个工具的源码读者可以从我的资源里下载)
下面先附上这个类(FFMediaInfoReader)的代码,后面再逐个部分讲解一下。
#ifndef _FFMediaInfoReader_H
#define _FFMediaInfoReader_H
#include
using namespace std;
#ifdef __cplusplus
extern "C" {
#endif
#ifdef HAVE_AV_CONFIG_H
#undef HAVE_AV_CONFIG_H
#endif
#include "./include/libavcodec/avcodec.h"
#include "./include/libavutil/mathematics.h"
#include "./include/libavutil/avutil.h"
#include "./include/libswscale/swscale.h"
#include "./include/libavutil/fifo.h"
#include "./include/libavformat/avformat.h"
#include "./include/libavutil/opt.h"
#include "./include/libavutil/error.h"
#include "./include/libavutil/pixfmt.h"
#include "./include/libswresample/swresample.h"
#ifdef __cplusplus
}
#endif
#pragma comment( lib, "avcodec.lib")
#pragma comment( lib, "avutil.lib")
#pragma comment( lib, "avformat.lib")
#pragma comment(lib, "swresample.lib")
#pragma comment( lib, "swscale.lib" )
#define PIX_FMT_BGR24 AV_PIX_FMT_BGR24
#define PIX_FMT_YUV420P AV_PIX_FMT_YUV420P
#define nullptr NULL
class FFMediaInfoReader
{
public:
FFMediaInfoReader();
virtual ~FFMediaInfoReader();
BOOL OpenFileStream(const char* szFilePath);
void CloseFileStream();
//获取视频分辨率
void GetVideoSize(int & width, int & height)
{
width = m_width;
height = m_height;
}
string GetMediaFormatName(); //获取媒体封装格式描述
//获取媒体轨道数目
int GetTrackNum()
{
int n = 0;
if(HasVideoTrack())
n += 1;
if(HasAudioTrack())
n += 1;
return n;
}
//是否有视频流
bool HasVideoTrack()
{
if( m_videoStreamIndex == -1)
return false;
if(m_width > 0 && m_height > 0)
return true;
return false;
}
//是否有音频流
bool HasAudioTrack()
{
if(m_audioStreamIndex == -1)
return false;
return true;
}
//获取视频帧数
int GetVideoFrameNumber()
{
return m_video_frame_count;
}
//获取文件总的码率
int GetFormatBitrate()
{
if(m_inputAVFormatCxt == NULL)
return -1;
return m_inputAVFormatCxt->bit_rate;
}
//文件播放时长(单位:秒)
int GetFileDuration()
{
if(m_inputAVFormatCxt == NULL)
return -1;
return (m_inputAVFormatCxt->duration)/1000000; //以微秒为单位,转换为秒为单位
}
string GetVideoCodecName()
{
return m_vcodec_name;
}
string GetAudioCodecName()
{
return m_acodec_name;
}
private:
BOOL openMediaFile();
void closeMediaFile();
private:
string m_filePath;
AVFormatContext* m_inputAVFormatCxt;
int m_videoStreamIndex;
int m_audioStreamIndex;
string m_vcodec_name;
string m_acodec_name;
char m_tmpErrString[64];
bool m_stop_status;
BOOL m_bInited;
int m_width, m_height; //视频分辨率
int m_video_frame_count; //视频总帧数
int m_frame_rate; //视频帧率
int m_audio_samplerate;
int m_audio_channels;
};
#endif // _FFMediaInfoReader_H
#include "stdafx.h"
#include "FFMediaInfoReader.h"
#include
//#include
string to_string(int n)
{
std::ostringstream stm;
string str;
stm << n;
str = stm.str();
//std::cout << str << std::endl;
return str;
}
//
FFMediaInfoReader::FFMediaInfoReader()
{
m_stop_status = false;
m_inputAVFormatCxt = nullptr;
m_videoStreamIndex = -1;
m_audioStreamIndex = -1;
m_bInited = FALSE;
m_width = m_height = 0;
m_frame_rate = 25;
m_video_frame_count = 0;
m_audio_samplerate = 0;
m_audio_channels = 0;
/* register all codecs, demux and protocols */
avcodec_register_all();
av_register_all();
}
FFMediaInfoReader::~FFMediaInfoReader()
{
CloseFileStream();
}
BOOL FFMediaInfoReader::OpenFileStream(const char* szFilePath)
{
m_filePath = szFilePath;
m_video_frame_count = 0;
m_videoStreamIndex = -1;
m_audioStreamIndex = -1;
m_vcodec_name = "";
m_acodec_name = "";
m_width = m_height = 0;
m_audio_samplerate = 0;
m_audio_channels = 0;
m_video_frame_count = 0;
m_bInited = FALSE;
return openMediaFile();
}
void FFMediaInfoReader::CloseFileStream()
{
m_stop_status = true;
closeMediaFile();
m_bInited = FALSE;
}
//打开输入文件
BOOL FFMediaInfoReader::openMediaFile()
{
if (m_inputAVFormatCxt)
{
TRACE("already has input avformat \n");
return FALSE;
}
int res = 0;
if ((res = avformat_open_input(&m_inputAVFormatCxt, m_filePath.c_str(), 0, NULL)) < 0)
{
}
if(res < 0)
{
string strError = "can not open file:" + m_filePath + ",errcode:" + to_string(res) + ",err msg:" + av_make_error_string(m_tmpErrString, AV_ERROR_MAX_STRING_SIZE, res);
TRACE("--------------%s \n", strError.c_str());
return FALSE;
}
if (avformat_find_stream_info(m_inputAVFormatCxt, 0) < 0)
{
TRACE("can not find stream info \n");
return FALSE;
}
TRACE("filepath: %s, format: %s, Bitrate: %d Kbps \n", m_filePath.c_str(), m_inputAVFormatCxt->iformat->name, m_inputAVFormatCxt->bit_rate/1000);
av_dump_format(m_inputAVFormatCxt, 0, m_filePath.c_str(), 0);
for (int i = 0; i < m_inputAVFormatCxt->nb_streams; i++)
{
AVStream *in_stream = m_inputAVFormatCxt->streams[i];
if (in_stream->codec->codec_type == AVMEDIA_TYPE_VIDEO)
{
m_videoStreamIndex = i;
m_width = in_stream->codec->width;
m_height = in_stream->codec->height;
if(in_stream->avg_frame_rate.den != 0 && in_stream->avg_frame_rate.num != 0)
{
m_frame_rate = in_stream->avg_frame_rate.num/in_stream->avg_frame_rate.den;//每秒多少帧
}
m_video_frame_count = in_stream->nb_frames; //视频帧数
//m_vcodec_name = in_stream->codec->codec_name; //获取编码器名称。不行,这里获得的名字是空的,后面用别的方法
TRACE("Video Track---stream index: %d, codec id: %d, width: %d, height: %d, FrameRate: %d, Number of Frames: %d\n",
i, in_stream->codec->codec_id, in_stream->codec->width, in_stream->codec->height, m_frame_rate, m_video_frame_count);
}
else if (in_stream->codec->codec_type == AVMEDIA_TYPE_AUDIO)
{
m_audioStreamIndex = i;
m_audio_samplerate = in_stream->codec->sample_rate;
m_audio_channels = in_stream->codec->channels;
//m_acodec_name = in_stream->codec->codec_name; //获取编码器名称。不行,这里获得的名字是空的,后面用别的方法
TRACE("Audio Track---stream index: %d, codec id: %d, sample_rate: %d, channels: %d \n",
i, in_stream->codec->codec_id, in_stream->codec->sample_rate, in_stream->codec->channels);
}
}
if(m_videoStreamIndex != -1)
{
AVCodecContext *avctx;
AVCodec *codec;
do
{
avctx = m_inputAVFormatCxt->streams[m_videoStreamIndex]->codec;
// 寻找视频解码器
codec = avcodec_find_decoder(avctx->codec_id);
if(codec == NULL)
break;
m_vcodec_name = codec->long_name; //视频编码器名称
TRACE("video_codec name: %s \n", codec->long_name);
}while(0);
}
if(m_audioStreamIndex != -1)
{
AVCodecContext *avctx;
AVCodec *codec;
do
{
avctx = m_inputAVFormatCxt->streams[m_audioStreamIndex]->codec;
// 寻找音频解码器
codec = avcodec_find_decoder(avctx->codec_id);
if(codec == NULL)
break;
m_acodec_name = codec->long_name; //音频编码器名称
TRACE("audio_codec_name: %s \n", codec->long_name);
}while(0);
}
#if 1
int nVFrames = 0;
bool bAudioDecoderInited = false;
int ret;
AVPacket pkt;
av_init_packet(&pkt);
//一帧一帧读取
while (av_read_frame(m_inputAVFormatCxt, &pkt) >= 0)
{
if(pkt.stream_index == m_videoStreamIndex)
{
AVStream * pStream = m_inputAVFormatCxt->streams[m_videoStreamIndex];
nVFrames++;
}//Video
else if(pkt.stream_index == m_audioStreamIndex)
{
AVStream * pStream = m_inputAVFormatCxt->streams[m_audioStreamIndex];
if (!bAudioDecoderInited)
{
if (avcodec_open2(pStream->codec, avcodec_find_decoder(pStream->codec->codec_id), NULL) < 0)
{
TRACE("Could not open audio codec.(无法打开解码器)\n");
break;
}
bAudioDecoderInited = true;
}
int dec_got_frame_a = 0;
AVFrame *input_frame = av_frame_alloc();
if (!input_frame)
{
ret = AVERROR(ENOMEM);
break;
}
//解码为PCM音频
if ((ret = avcodec_decode_audio4(pStream->codec, input_frame, &dec_got_frame_a, &pkt)) < 0)
{
TRACE("Could not decode audio frame.\n");
av_frame_free(&input_frame);
break;
}
if (dec_got_frame_a) //解码出一帧
{
//获得音频属性(音频输出格式、采样率、声道数)
TRACE("audio_sample_format: %d, sample_rate: %d, channels: %d \n", pStream->codec->sample_fmt, pStream->codec->sample_rate, pStream->codec->channels);
}
av_frame_free(&input_frame);
//注意:这里根据你的需求情况决定是否屏蔽以下代码
if(dec_got_frame_a) //获取到音频信息后是否跳出循环??
break;
} //Audio
av_free_packet(&pkt);
}
if(m_video_frame_count == 0)
{
m_video_frame_count = nVFrames; //更新视频帧数
}
#endif
m_bInited = FALSE;
return TRUE;
}
void FFMediaInfoReader::closeMediaFile()
{
if (m_inputAVFormatCxt)
{
avformat_close_input(&m_inputAVFormatCxt);
m_inputAVFormatCxt = NULL;
}
}
string FFMediaInfoReader::GetMediaFormatName()
{
if(m_inputAVFormatCxt == NULL)
return "";
//return m_inputAVFormatCxt->iformat->name;
return m_inputAVFormatCxt->iformat->long_name;
}
--------------------------------------------------------------------------------------------------------------------------
对代码中一些重点的部分讲解一下。
首先,要调用FFmpeg获取文件的相关信息,肯定要先打开一个文件,然后获取相关媒体流(视频流、音频流)对应的轨道。
1. 打开一个文件。
int res = 0;
if ((res = avformat_open_input(&m_inputAVFormatCxt, m_filePath.c_str(), 0, NULL)) < 0)
{
}
if(res < 0)
{
string strError = "can not open file:" + m_filePath + ",errcode:" + std::to_string((int)res) + ",err msg:" + av_make_error_string(m_tmpErrString, AV_ERROR_MAX_STRING_SIZE, res);
TRACE("%s \n", strError.c_str());
return false;
}
if (avformat_find_stream_info(m_inputAVFormatCxt, 0) < 0)
{
TRACE("can not find stream info \n");
return false;
}
av_dump_format(m_inputAVFormatCxt, 0, m_filePath.c_str(), 0);
2. 获取各个流的轨道(StreamIndex)
for (int i = 0; i < m_inputAVFormatCxt->nb_streams; i++)
{
AVStream *in_stream = m_inputAVFormatCxt->streams[i];
if (in_stream->codec->codec_type == AVMEDIA_TYPE_VIDEO)
{
m_videoStreamIndex = i;
}
else if (in_stream->codec->codec_type == AVMEDIA_TYPE_AUDIO)
{
m_audioStreamIndex = i;
}
}
3. 获取视频的信息(编码格式、视频宽高、帧率、帧数)
AVStream *in_stream = m_inputAVFormatCxt->streams[i];
if (in_stream->codec->codec_type == AVMEDIA_TYPE_VIDEO)
{
m_videoStreamIndex = i;
m_width = in_stream->codec->width;
m_height = in_stream->codec->height;
if(in_stream->avg_frame_rate.den != 0 && in_stream->avg_frame_rate.num != 0)
{
m_frame_rate = in_stream->avg_frame_rate.num/in_stream->avg_frame_rate.den;//每秒多少帧
}
m_video_frame_count = in_stream->nb_frames; //视频帧数
//m_vcodec_name = in_stream->codec->codec_name; //获取编码器名称。不行,这里获得的名字是空的,后面用别的方法
TRACE("Video Track---stream index: %d, codec id: %d, width: %d, height: %d, FrameRate: %d, Number of Frames: %d\n",
i, in_stream->codec->codec_id, in_stream->codec->width, in_stream->codec->height, m_frame_rate, m_video_frame_count);
}
上面代码中我们先取得AVStream类型的指针, in_stream->codec->codec_id获得表示视频(或音频)的CodecID,这个CodecID是一个枚举类型,表示一种编码格式,AV_CODEC_ID_H264表示H264格式。 in_stream->codec->width、 in_stream->codec->height是视频的宽高。获得帧数的方法是:in_stream->nb_frames; (这里要多谢一位网友的留言和提醒),但是这里要补充一下,获得帧数通过AVStream指针的nb_frames成员变量不一定能拿到实际的帧数,我测试过一些文件,发现读取出来的这个值是为0的,如果遇到这种情况,只能采用笨的方法:遍历整个文件的视频帧了。代码如下:
int nVFrames = 0;
AVPacket pkt;
av_init_packet(&pkt);
//一帧一帧读取
while (av_read_frame(m_inputAVFormatCxt, &pkt) >= 0)
{
if(pkt.stream_index == m_videoStreamIndex)
{
nVFrames++;
}
av_free_packet(&pkt);
}
//读完要把文件指针移到文件开始位置
av_seek_frame(m_inputAVFormatCxt,m_videoStreamIndex, 0*1000, 0);
4. 获得音频的编码格式、采样率、声道数
音频的编码格式的获取方法跟视频的类似。但是这里也要补充一下:用这种方法获得某些文件的音频轨道的采样率和声道数值为0,要用另外一种方法(见下面最后一节)。
AVStream *in_stream = m_inputAVFormatCxt->streams[i];
if (in_stream->codec->codec_type == AVMEDIA_TYPE_AUDIO)
{
m_audioStreamIndex = i;
m_audio_samplerate = in_stream->codec->sample_rate;
m_audio_channels = in_stream->codec->channels;
//m_acodec_name = in_stream->codec->codec_name; //获取编码器名称。不行,这里获得的名字是空的,后面用别的方法
TRACE("Audio Track---stream index: %d, codec id: %d, sample_rate: %d, channels: %d \n",
i, in_stream->codec->codec_id, in_stream->codec->sample_rate, in_stream->codec->channels);
}
5. 获得整个文件的播放时长
读到的duration变量是以微妙为单位的,这个时间值转为秒要除以1000000.
if (m_inputAVFormatCxt->duration > 0)
{
m_Duration = m_inputAVFormatCxt->duration / 1000000; //转换为秒
}
else
{
m_Duration = 0;
}
6. 获取视频编码器、音频编码器名称
虽然我们可以用前面获取到的codec_id来判断流是什么编码格式的,但是如果要获得编码格式的一个字符串描述,最好还是通过获取这种格式(codec_id)对应的解码器的名称,获取代码如下:
if(m_videoStreamIndex != -1)
{
AVCodecContext *avctx;
AVCodec *codec;
do
{
avctx = m_inputAVFormatCxt->streams[m_videoStreamIndex]->codec;
// 寻找视频解码器
codec = avcodec_find_decoder(avctx->codec_id);
if(codec == NULL)
break;
m_vcodec_name = codec->long_name; //视频编码器名称
TRACE("video_codec name: %s \n", codec->long_name);
}while(0);
}
if(m_audioStreamIndex != -1)
{
AVCodecContext *avctx;
AVCodec *codec;
do
{
avctx = m_inputAVFormatCxt->streams[m_audioStreamIndex]->codec;
// 寻找音频解码器
codec = avcodec_find_decoder(avctx->codec_id);
if(codec == NULL)
break;
m_acodec_name = codec->long_name; //音频编码器名称
TRACE("audio_codec_name: %s \n", codec->long_name);
}while(0);
}
7. 获取文件容器格式
我们要知道一个文件是MP4格式,AVI格式,还是MPEG-PS,MPEG-TS或其他,光看后缀名是不行的,FFmpeg能解析文件并获得文件封装用的格式,获得容器格式的函数如下:
string FFMediaInfoReader::GetMediaFormatName()
{
if(m_inputAVFormatCxt == NULL)
return "";
//return m_inputAVFormatCxt->iformat->name;
return m_inputAVFormatCxt->iformat->long_name;
}
注意:上面的m_inputAVFormatCxt->iformat->name和m_inputAVFormatCxt->iformat->long_name字符串值是不一样的,大家可以分别读取不同的值看有什么不同。
8. 获得文件总码率(平均码率)
m_inputAVFormatCxt->bit_rate/1000;//转为K为单位,Kbps
9. 获取轨道数(媒体流数目)
//获取媒体轨道数目
int GetTrackNum()
{
int n = 0;
if(HasVideoTrack())
n += 1;
if(HasAudioTrack())
n += 1;
return n;
}
//是否有视频流
bool HasVideoTrack()
{
if( m_videoStreamIndex == -1)
return false;
if(m_width > 0 && m_height > 0)
return true;
return false;
}
//是否有音频流
bool HasAudioTrack()
{
if(m_audioStreamIndex == -1)
return false;
return true;
}
10. 如何有效获取音频的采样率、声道数
前面遗留了一个问题,现在就回答一下。要获取音频轨道的信息,直接打开文件获取是不可靠的,有些文件这时候还不能拿得到音频属性。这时候,我们就需要将音频帧读出来,解码之后,就能得到具体的音频属性了(注意:我们不需要全部帧解码,只解码一两个音频帧就能得到信息)。代码如下:
int nVFrames = 0;
bool bAudioDecoderInited = false;
int ret;
AVPacket pkt;
av_init_packet(&pkt);
//一帧一帧读取
while (av_read_frame(m_inputAVFormatCxt, &pkt) >= 0)
{
if(pkt.stream_index == m_videoStreamIndex)
{
AVStream * pStream = m_inputAVFormatCxt->streams[m_videoStreamIndex];
nVFrames++;
}//Video
else if(pkt.stream_index == m_audioStreamIndex)
{
AVStream * pStream = m_inputAVFormatCxt->streams[m_audioStreamIndex];
if (!bAudioDecoderInited)
{
if (avcodec_open2(pStream->codec, avcodec_find_decoder(pStream->codec->codec_id), NULL) < 0)
{
TRACE("Could not open audio codec.(无法打开解码器)\n");
break;
}
bAudioDecoderInited = true;
}
int dec_got_frame_a = 0;
AVFrame *input_frame = av_frame_alloc();
if (!input_frame)
{
ret = AVERROR(ENOMEM);
break;
}
//解码为PCM音频
if ((ret = avcodec_decode_audio4(pStream->codec, input_frame, &dec_got_frame_a, &pkt)) < 0)
{
TRACE("Could not decode audio frame.\n");
av_frame_free(&input_frame);
break;
}
if (dec_got_frame_a) //解码出一帧
{
//获得音频属性(音频输出格式、采样率、声道数)
TRACE("audio_sample_format: %d, sample_rate: %d, channels: %d \n", pStream->codec->sample_fmt, pStream->codec->sample_rate, pStream->codec->channels);
}
av_frame_free(&input_frame);
//注意:这里根据你的需求情况决定是否屏蔽以下代码
if(dec_got_frame_a) //获取到音频信息后是否跳出循环??
break;
} //Audio
av_free_packet(&pkt);
}
if(m_video_frame_count == 0)
{
m_video_frame_count = nVFrames; //更新视频帧数
}
#endif
这个类会通过VC++的TRACE语句把读到的媒体信息打印出来,下面是在作者机器上读取一些媒体文件打印的信息:
filepath: G:\videos\钻石珠宝.mp4, format: mov,mp4,m4a,3gp,3g2,mj2, Bitrate: 22498 Kbps
Video Track---stream index: 0, codec id: 28, width: 3840, height: 2160, FrameRate: 29, Number of Frames: 1544
Audio Track---stream index: 1, codec id: 86018, sample_rate: 44100, channels: 2
video_codec name: H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10
audio_codec_name: AAC (Advanced Audio Coding)
filepath: G:\videos(2)\xianjian.ts, format: mpegts, Bitrate: 1042 Kbps
Video Track---stream index: 0, codec id: 2, width: 640, height: 480, FrameRate: 25, Number of Frames: 0
Audio Track---stream index: 1, codec id: 86017, sample_rate: 44100, channels: 2
video_codec name: MPEG-2 video
audio_codec_name: MP3 (MPEG audio layer 3)
除了这种方法之外,你还可以通过调用ffprobe.exe执行命令行方式来获取媒体文件的信息,可以参考这篇文章:
《Java/PHP/C#等语言如何调用ffmpeg/ffprobe获取音视频文件的信息并输出为JSON格式》
如果你觉得文章写得不错,记得点赞一下!