簡(jiǎn)介
- 開(kāi)發(fā)環(huán)境
- FFmpeg sdk下載
- 項(xiàng)目配置
- 代碼流程
開(kāi)發(fā)環(huán)境
vs 2017
FFmpeg sdk下載
下載地址
這里下載3.3.3 — 32bit — share和Dev
- Shared包含運(yùn)行時(shí)的動(dòng)態(tài)庫(kù)在bin目錄下
- Dev包含開(kāi)發(fā)是編譯需要的頭文件(include目錄下)和庫(kù)文件(lib目錄下)
項(xiàng)目配置
先看下項(xiàng)目的目錄結(jié)構(gòu)
這里的bin、include也糊、lib就是我們剛才在FFmpeg下載的相關(guān)文件炼蹦。
src是我們的項(xiàng)目源碼目錄。
新建Win32控制臺(tái)應(yīng)用程序狸剃、選擇位置掐隐、項(xiàng)目名稱钞馁。注意:去掉“為結(jié)局方案創(chuàng)建目錄”的勾選
然后選擇空項(xiàng)目虑省、去掉預(yù)編譯頭。完成項(xiàng)目的創(chuàng)建
項(xiàng)目屬性配置
右擊項(xiàng)目屬性
- 【常規(guī)】=>【輸出目錄】 修改為
..\..\bin
- 【調(diào)試】=>【工作目錄】修改為
..\..\bin
- 【C/C++】=>【常規(guī)】=>【附加包含目錄】修改為
..\..\include
-【鏈接器】=>【常規(guī)】=>【附加庫(kù)目錄】修改為..\..\lib
注意:這里所有的路徑都是相對(duì)路徑僧凰,相對(duì)于源碼的路徑
這里設(shè)置輸出目錄到bin探颈。是因?yàn)閣in下運(yùn)行時(shí)會(huì)默認(rèn)在當(dāng)前運(yùn)行的目錄下尋找dll文件。而我們的dll文件放在bin目錄下训措。
開(kāi)發(fā)流程
流程詳解
av_register_all()
該方法初始化所有的封裝和解封裝伪节。在使用FFmpeg的時(shí)候首先要調(diào)用這個(gè)方法。
找到這個(gè)方法的源碼libavformat\allformats.c
void av_register_all(void)
{
static AVOnce control = AV_ONCE_INIT;
ff_thread_once(&control, register_all);
}
這里控制注冊(cè)方法只被調(diào)用一次register_all是函數(shù)指針隙弛〖懿觯看到實(shí)現(xiàn)部分。
static void register_all(void)
{
avcodec_register_all();
/* (de)muxers */
REGISTER_MUXER (A64, a64);
REGISTER_DEMUXER (AA, aa);
REGISTER_DEMUXER (AAC, aac);
//...
}
這里面就是進(jìn)行各種注冊(cè)全闷,而REGISTER_MUXER 叉寂、REGISTER_DEMUXER 是前面定義的宏。我們看到是靜態(tài)方法总珠,說(shuō)明該方法只能在所在的文件中使用屏鳍,這也防止被注冊(cè)多次勘纯。
#define REGISTER_MUXER(X, x) \
{ \
extern AVOutputFormat ff_##x##_muxer; \
if (CONFIG_##X##_MUXER) \
av_register_output_format(&ff_##x##_muxer); \
}
#define REGISTER_DEMUXER(X, x) \
{ \
extern AVInputFormat ff_##x##_demuxer; \
if (CONFIG_##X##_DEMUXER) \
av_register_input_format(&ff_##x##_demuxer); \
}
#define REGISTER_MUXDEMUX(X, x) REGISTER_MUXER(X, x); REGISTER_DEMUXER(X, x)
av_register_input_format和av_register_output_format我們也可以單獨(dú)去初始化。這里內(nèi)部細(xì)節(jié)就不做過(guò)多介紹钓瞭。
avformat_network_init()
網(wǎng)絡(luò)相關(guān)初始化驳遵。如果我們使用了網(wǎng)絡(luò)拉流和推流等等,要先初始化山涡。
avformat_open_input()
聲明是
int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);
定義在libavformat\utils.c
中堤结。主要功能
- 輸入輸出結(jié)構(gòu)體AVIOContext的初始化;
- 輸入數(shù)據(jù)的協(xié)議URLProtocol鸭丛,通過(guò)函數(shù)指針的方式竞穷,與FFMPEG關(guān)聯(lián),剩下的就是調(diào)用該URLProtocol的函數(shù)進(jìn)行open,read等操作了
avformat_find_stream_info
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
可以讀取視音頻數(shù)據(jù)并且獲得一些相關(guān)的信息鳞溉。定義在libavformat\utils.c
下
avformat_alloc_output_context2
int avformat_alloc_output_context2(AVFormatContext **ctx, AVOutputFormat *oformat,
const char *format_name, const char *filename);
定義在libavformat\mux.c
中
- ctx:函數(shù)調(diào)用成功之后創(chuàng)建的AVFormatContext結(jié)構(gòu)體瘾带。
- oformat:指定AVFormatContext中的AVOutputFormat,用于確定輸出格式熟菲。如果指定為NULL看政,可以設(shè)定后兩個(gè)參數(shù)(format_name或者filename)由FFmpeg猜測(cè)輸出格式。
PS:使用該參數(shù)需要自己手動(dòng)獲取AVOutputFormat抄罕,相對(duì)于使用后兩個(gè)參數(shù)來(lái)說(shuō)要麻煩一些允蚣。 - format_name:指定輸出格式的名稱。根據(jù)格式名稱贞绵,F(xiàn)Fmpeg會(huì)推測(cè)輸出格式厉萝。輸出格式可以是“flv”,“mkv”等等榨崩。
- filename:指定輸出文件的名稱。根據(jù)文件名稱章母,F(xiàn)Fmpeg會(huì)推測(cè)輸出格式母蛛。文件名稱可以是“xx.flv”,“yy.mkv”等等乳怎。
函數(shù)執(zhí)行成功的話彩郊,其返回值大于等于0。
內(nèi)部流程
- 調(diào)用avformat_alloc_context()初始化一個(gè)默認(rèn)的AVFormatContext蚪缀。
- 如果指定了輸入的AVOutputFormat秫逝,則直接將輸入的AVOutputFormat賦值給AVOutputFormat的oformat。如果沒(méi)有指定輸入的AVOutputFormat询枚,就需要根據(jù)文件格式名稱或者文件名推測(cè)輸出的AVOutputFormat违帆。無(wú)論是通過(guò)文件格式名稱還是文件名推測(cè)輸出格式,都會(huì)調(diào)用一個(gè)函數(shù)av_guess_format()金蜀。
avio_open
打開(kāi)FFmpeg的輸入輸出文件
int avio_open2(AVIOContext **s, const char *url, int flags,
const AVIOInterruptCB *int_cb, AVDictionary **options);
- s:函數(shù)調(diào)用成功之后創(chuàng)建的AVIOContext結(jié)構(gòu)體刷后。
- url:輸入輸出協(xié)議的地址(文件也是一種“廣義”的協(xié)議的畴,對(duì)于文件來(lái)說(shuō)就是文件的路徑)。
- flags:打開(kāi)地址的方式尝胆∩ゲ茫可以選擇只讀,只寫含衔,或者讀寫煎娇。取值如下。
AVIO_FLAG_READ:只讀贪染。
AVIO_FLAG_WRITE:只寫缓呛。
AVIO_FLAG_READ_WRITE:讀寫。 - int_cb:不太清楚
- options:不太清楚
avformat_write_header
寫視頻文件頭,av_write_trailer()用于寫視頻文件尾
av_read_frame
定義在libavformat\utils.c
中
讀取碼流中的音頻若干幀或者視頻一幀抑进。解碼視頻的時(shí)候强经,每解碼一個(gè)視頻幀,需要先調(diào)用 av_read_frame()獲得一幀視頻的壓縮數(shù)據(jù)寺渗,然后才能對(duì)該數(shù)據(jù)進(jìn)行解碼(例如H.264中一幀壓縮數(shù)據(jù)通常對(duì)應(yīng)一個(gè)NAL)匿情。
這里我貼上官方的注釋,很詳細(xì):
/**
* Return the next frame of a stream.
* This function returns what is stored in the file, and does not validate
* that what is there are valid frames for the decoder. It will split what is
* stored in the file into frames and return one for each call. It will not
* omit invalid data between valid frames so as to give the decoder the maximum
* information possible for decoding.
*
* If pkt->buf is NULL, then the packet is valid until the next
* av_read_frame() or until avformat_close_input(). Otherwise the packet
* is valid indefinitely. In both cases the packet must be freed with
* av_packet_unref when it is no longer needed. For video, the packet contains
* exactly one frame. For audio, it contains an integer number of frames if each
* frame has a known fixed size (e.g. PCM or ADPCM data). If the audio frames
* have a variable size (e.g. MPEG audio), then it contains one frame.
*
* pkt->pts, pkt->dts and pkt->duration are always set to correct
* values in AVStream.time_base units (and guessed if the format cannot
* provide them). pkt->pts can be AV_NOPTS_VALUE if the video format
* has B-frames, so it is better to rely on pkt->dts if you do not
* decompress the payload.
*
* @return 0 if OK, < 0 on error or end of file
*/
總結(jié)起來(lái)每段的核心意思
- 讀取碼流中的音頻若干幀或者視頻一幀
- 如果pkt->buf是空信殊,那么就要等待下一次av_read_frame調(diào)用炬称。否則無(wú)法確定是否有效
- pts dts duration通常被設(shè)置為正確的值。但如果視頻幀包括Bzh幀涡拘,那么pts可以是AV_NOPTS_VALUE玲躯。所以最好依賴dts。
av_interleaved_write_frame
輸出一幀視音頻數(shù)據(jù)
核心類
AVFormatContext
AVFormatContext是一個(gè)貫穿始終的數(shù)據(jù)結(jié)構(gòu)鳄乏,很多函數(shù)都要用到它作為參數(shù)跷车。它是FFMPEG解封裝(flv,mp4橱野,rmvb朽缴,avi)功能的結(jié)構(gòu)體。
內(nèi)部的成員變量水援,大家可以查看頭文件密强。這里我們列舉下一些常用重要的成員變量:
- struct AVInputFormat *iformat:輸入數(shù)據(jù)的封裝格式
- AVIOContext *pb:輸入數(shù)據(jù)的緩存
- unsigned int nb_streams:視音頻流的個(gè)數(shù)
- AVStream **streams:視音頻流
- char filename[1024]:文件名
- int64_t duration:時(shí)長(zhǎng)(單位:微秒us,轉(zhuǎn)換為秒需要除以1000000)
- int bit_rate:比特率(單位bps蜗元,轉(zhuǎn)換為kbps需要除以1000)
- AVDictionary *metadata:元數(shù)據(jù)
視頻的原數(shù)據(jù)(metadata)信息可以通過(guò)AVDictionary獲取或渤。元數(shù)據(jù)存儲(chǔ)在AVDictionaryEntry結(jié)構(gòu)體中
typedef struct AVDictionaryEntry {
char *key;
char *value;
} AVDictionaryEntry;
每一條元數(shù)據(jù)分為key和value兩個(gè)屬性。
在ffmpeg中通過(guò)av_dict_get()函數(shù)獲得視頻的原數(shù)據(jù)奕扣。
cout << endl << endl << "======元信息=======" << endl;
string meta, key, value;
AVDictionaryEntry *m = NULL;
while (m = av_dict_get(ictx->metadata, "", m, AV_DICT_IGNORE_SUFFIX)) {
key=m->key;
value=m->value;
meta.append(key).append("\t:").append(value).append("\r\n");
}
cout << meta.c_str() << endl;
AVStream
AVStream是存儲(chǔ)每一個(gè)視頻/音頻流信息的結(jié)構(gòu)體薪鹦。
- int index:標(biāo)識(shí)該視頻/音頻流
- AVCodecContext *codec:指向該視頻/音頻流的AVCodecContext(它們是一一對(duì)應(yīng)的關(guān)系)
- AVRational time_base:時(shí)基。通過(guò)該值可以把PTS成畦,DTS轉(zhuǎn)化為真正的時(shí)間距芬。- FFMPEG其他結(jié)構(gòu)體中也有這個(gè)字段涝开,但是根據(jù)我的經(jīng)驗(yàn),只有AVStream中的time_base是可用的框仔。PTS*time_base=真正的時(shí)間
- int64_t duration:該視頻/音頻流長(zhǎng)度
- AVDictionary *metadata:元數(shù)據(jù)信息
- AVRational avg_frame_rate:幀率(注:對(duì)視頻來(lái)說(shuō)舀武,這個(gè)挺重要的)
- AVPacket attached_pic:附帶的圖片。比如說(shuō)一些MP3离斩,AAC音頻文件附帶的專輯封面银舱。
AVPacket
AVPacket是存儲(chǔ)壓縮編碼數(shù)據(jù)相關(guān)信息的結(jié)構(gòu)體。
uint8_t *data:壓縮編碼的數(shù)據(jù)跛梗。
例如對(duì)于H.264來(lái)說(shuō)寻馏。1個(gè)AVPacket的data通常對(duì)應(yīng)一個(gè)NAL。
注意:在這里只是對(duì)應(yīng)核偿,而不是一模一樣雇毫。他們之間有微小的差別:使用FFMPEG類庫(kù)分離出多媒體文件中的H.264碼流
因此在使用FFMPEG進(jìn)行視音頻處理的時(shí)候胁艰,常撑璧ⅲ可以將得到的AVPacket的data數(shù)據(jù)直接寫成文件莹桅,從而得到視音頻的碼流文件。int size:data的大小
int64_t pts:顯示時(shí)間戳
int64_t dts:解碼時(shí)間戳
int stream_index:標(biāo)識(shí)該AVPacket所屬的視頻/音頻流尼荆。
源碼
#include <iostream>
using namespace std;
//引入頭文件
extern "C"
{
#include "libavformat/avformat.h"
//引入時(shí)間
#include "libavutil/time.h"
}
//引入庫(kù)
#pragma comment(lib,"avformat.lib")
//工具庫(kù)左腔,包括獲取錯(cuò)誤信息等
#pragma comment(lib,"avutil.lib")
//編解碼的庫(kù)
#pragma comment(lib,"avcodec.lib")
int avError(int errNum);
static double r2d(AVRational r)
{
return r.num == 0 || r.den == 0 ? 0. : (double)r.num / (double)r.den;
}
int main() {
//所有代碼執(zhí)行之前要調(diào)用av_register_all和avformat_network_init
//初始化所有的封裝和解封裝 flv mp4 mp3 mov。不包含編碼和解碼
av_register_all();
//初始化網(wǎng)絡(luò)庫(kù)
avformat_network_init();
//使用的相對(duì)路徑捅儒,執(zhí)行文件在bin目錄下液样。test.mp4放到bin目錄下即可
const char *inUrl = "test.flv";
//輸出的地址
const char *outUrl = "rtmp://192.168.136.131/live/test";
//////////////////////////////////////////////////////////////////
// 輸入流處理部分
/////////////////////////////////////////////////////////////////
//打開(kāi)文件,解封裝 avformat_open_input
//AVFormatContext **ps 輸入封裝的上下文巧还。包含所有的格式內(nèi)容和所有的IO鞭莽。如果是文件就是文件IO,網(wǎng)絡(luò)就對(duì)應(yīng)網(wǎng)絡(luò)IO
//const char *url 路徑
//AVInputFormt * fmt 封裝器
//AVDictionary ** options 參數(shù)設(shè)置
AVFormatContext *ictx = NULL;
//打開(kāi)文件麸祷,解封文件頭
int ret = avformat_open_input(&ictx, inUrl, 0, NULL);
if (ret < 0) {
return avError(ret);
}
cout << "avformat_open_input success!" << endl;
//獲取音頻視頻的信息 .h264 flv 沒(méi)有頭信息
ret = avformat_find_stream_info(ictx, 0);
if (ret != 0) {
return avError(ret);
}
//打印視頻視頻信息
//0打印所有 inUrl 打印時(shí)候顯示撮抓,
av_dump_format(ictx, 0, inUrl, 0);
//////////////////////////////////////////////////////////////////
// 輸出流處理部分
/////////////////////////////////////////////////////////////////
AVFormatContext * octx = NULL;
//如果是輸入文件 flv可以不傳,可以從文件中判斷摇锋。如果是流則必須傳
//創(chuàng)建輸出上下文
ret = avformat_alloc_output_context2(&octx, NULL, "flv", outUrl);
if (ret < 0) {
return avError(ret);
}
cout << "avformat_alloc_output_context2 success!" << endl;
//配置輸出流
//AVIOcontext *pb //IO上下文
//AVStream **streams 指針數(shù)組,存放多個(gè)輸出流 視頻音頻字幕流
//int nb_streams;
//duration ,bit_rate
//AVStream
//AVRational time_base
//AVCodecParameters *codecpar 音視頻參數(shù)
//AVCodecContext *codec
//遍歷輸入的AVStream
for (int i = 0; i < ictx->nb_streams; i++) {
//創(chuàng)建一個(gè)新的流到octx中
AVStream *out = avformat_new_stream(octx, ictx->streams[i]->codec->codec);
if (!out) {
return avError(0);
}
//復(fù)制配置信息 用于mp4 過(guò)時(shí)的方法
//ret=avcodec_copy_context(out->codec, ictx->streams[i]->codec);
ret = avcodec_parameters_copy(out->codecpar, ictx->streams[i]->codecpar);
if (ret < 0) {
return avError(ret);
}
out->codec->codec_tag = 0;
}
av_dump_format(octx, 0, outUrl, 1);
//////////////////////////////////////////////////////////////////
// 準(zhǔn)備推流
/////////////////////////////////////////////////////////////////
//打開(kāi)IO
ret = avio_open(&octx->pb, outUrl, AVIO_FLAG_WRITE);
if (ret < 0) {
avError(ret);
}
//寫入頭部信息
ret = avformat_write_header(octx, 0);
if (ret < 0) {
avError(ret);
}
cout << "avformat_write_header Success!" << endl;
//推流每一幀數(shù)據(jù)
//int64_t pts [ pts*(num/den) 第幾秒顯示]
//int64_t dts 解碼時(shí)間 [P幀(相對(duì)于上一幀的變化) I幀(關(guān)鍵幀站超,完整的數(shù)據(jù)) B幀(上一幀和下一幀的變化)] 有了B幀壓縮率更高荸恕。
//uint8_t *data
//int size
//int stream_index
//int flag
AVPacket avPacket;
//獲取當(dāng)前的時(shí)間戳 微妙
long long startTime = av_gettime();
while (true)
{
ret = av_read_frame(ictx, &avPacket);
if (ret < 0) {
break;
}
cout << avPacket.pts << " " << flush;
//計(jì)算轉(zhuǎn)換時(shí)間戳 pts dts
//獲取時(shí)間基數(shù)
AVRational itime = ictx->streams[avPacket.stream_index]->time_base;
AVRational otime = octx->streams[avPacket.stream_index]->time_base;
avPacket.pts = av_rescale_q_rnd(avPacket.pts, itime, otime, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_NEAR_INF));
avPacket.dts = av_rescale_q_rnd(avPacket.pts, itime, otime, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_NEAR_INF));
//到這一幀時(shí)候經(jīng)歷了多長(zhǎng)時(shí)間
avPacket.duration = av_rescale_q_rnd(avPacket.duration, itime, otime, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_NEAR_INF));
avPacket.pos = -1;
//視頻幀推送速度
if (ictx->streams[avPacket.stream_index]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
AVRational tb = ictx->streams[avPacket.stream_index]->time_base;
//已經(jīng)過(guò)去的時(shí)間
long long now = av_gettime() - startTime;
long long dts = 0;
dts = avPacket.dts * (1000 * 1000 * r2d(tb));
if (dts > now)
av_usleep(dts - now);
else {
cout << "sss";
}
}
//推送 會(huì)自動(dòng)釋放空間 不需要調(diào)用av_packet_unref
ret = av_interleaved_write_frame(octx, &avPacket);
if (ret < 0) {
break;
}
//視頻幀推送速度
//if (avPacket.stream_index == 0)
// av_usleep(30 * 1000);
//釋放空間。內(nèi)部指向的視頻空間和音頻空間
//av_packet_unref(&avPacket);
}
return 0;
}
int avError(int errNum) {
char buf[1024];
//獲取錯(cuò)誤信息
av_strerror(errNum, buf, sizeof(buf));
cout << " failed! " << buf << endl;
return -1;
}
彩蛋
上面的代碼在推送flv格式文件時(shí)候可能沒(méi)問(wèn)題死相,當(dāng)換成mp4或者rmvb時(shí)候可能出現(xiàn)各種問(wèn)題融求。如果你是在無(wú)法解開(kāi)這個(gè)問(wèn)題,請(qǐng)看下節(jié)基于FFmpeg進(jìn)行RTMP推流(二)