我們經常需要知道一個媒體文件所包含的媒體流的信息,比如文件格式、播放時長、碼率、視音頻編碼格式,視頻分辨率,幀率,音頻屬性等信息。如何使用FFmpeg API獲取這些信息呢?下面我會給出一個完善的類,這個類封裝了FFmpeg讀取文件信息的相關的API,讀者只需要調類的方法就可以獲得相關的信息。
這個類能夠讀取媒體文件的哪些信息呢?假如我們給出一個媒體文件(MP4,AVI,MKV。。。),裏面至少要包含一個音頻軌或視頻軌,則調用該類的方法可以獲得的信息如下:
媒體容器封裝格式
文件播放時長
文件平均碼率(視頻+音頻)
視頻屬性(編碼器名稱、視頻分辨率、幀率、視頻幀數、編碼碼率)
音頻屬性(編碼器名稱、採樣率、聲道數、編碼碼率)
下面先附上這個類(FFMediaInfoReader)的代碼,後面再逐個部分講解一下。
類頭文件代碼:
#ifndef _FFMediaInfoReader_H
#define _FFMediaInfoReader_H
#include <string>
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 <sstream>
//#include <mmsystem.h>
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)