一. 創建上下文
1.1 概述
解封裝是直接調用avformat_open_input()
函數就生成了一個上下文,但是封裝卻需要創建一個上下文。因為有這樣一個區別,在解封裝過程中,上下文中有很多信息是由FFmpeg
的接口填入的,但是如果是封裝的話,很多信息需要我們自己填入(畢竟FFmpeg
不知道你最終想要生成的視頻的具體參數是什么)。FFmpeg
提供了一個函數avformat_alloc_output_context2()
用於創建此上下文,當上下文創建完畢之后,我們需要自行設定上下文參數。
1.2 需要用到的接口說明
// 創建封裝的上下文
int avformat_alloc_output_context2(AVFormatContext **ctx, // 生成的封裝的上下文(可以看到它是一個二級指針,本身就是用做賦值用的)
ff_const59 AVOutputFormat *oformat, // 輸出的格式指定,一般傳NULL, 因為輸出的格式可以直接通過文件名后綴進行指定
const char *format_name, // 輸出的格式名稱,一般傳NULL,也是可以通過文件名后綴進行指定
const char *filename); // 最終輸出的文件名,比如abc.mp4, 那FFmpeg就會以mp4的封裝格式進行創建
二. 添加音頻視頻流
2.1 概述
在上下文中插入音頻或視頻流信息。在AVFormatContext
的streams[]
數組中插入音頻或視頻流信息。
2.2 需要用到的接口說明
// 添加流信息
AVStream *avformat_new_stream(AVFormatContext *s, // 上面創建的封裝的上下文
const AVCodec *c); // 指定編碼音頻或視頻時所用的編碼器對象
注意:new stream是有次序的,可以看到上述接口並沒有需要傳入索引號,那第一個new的stream,它的索引號就是
0
,第二個new的stream,它的索引號就是1
,盡量使用通用的方式,比如0
代表video
,1
代表audio
,這樣可以使一些播放器能夠兼容(有些播放器沒有去區分流的信息是音頻還是視頻, 默認它們就以索引號為0的當做視頻,索引號為1的當做音頻)。另外,這個新new的
AVStream
的空間不需要管理,因為它是關聯在AVFormatContext
對象當中,當AVFormatContext
在做清理的時候,會把它也給清理掉。
三. 打開輸出IO
3.1 概述
不管你是想在本地生成一個視頻文件,還是想通過rtmp
進行推流,都需要指定一個具體的輸出對象。如果是推流的話,則是通過網絡接口往外發,如果是生成文件的話,則需要打開輸入輸出的IO。
3.2 需要用到的接口說明
// 打開輸出的IO
int avio_open(AVIOContext **s, // 封裝器的IO上下文,它是AVFormatContext的成員 pb
const char *url, // 打開的地址,就是輸出的文件路徑,前面也傳了一個filename,不過那個filename主要是用於做格式的判斷
int flags); // 涉及IO操作的FLAG,如果是寫文件的話,可以傳 AVIO_FLAG_WRITE
// 實際調用
avio_open(&c->pb, url, AVIO_FLAG_WRITE);
// 關閉封裝器的IO上下文
int avio_closep(AVIOContext **s); // 實測, AVFormatContext在清理的時候並沒有關閉IO上下文,所以需要在AVFormatContext在做清理之前 // 把該封裝器的IO上下文給關掉。
接下來就是具體寫文件的操作了。具體的寫文件操作包含以下三部分:寫入文件頭,寫入幀數據,寫入尾部數據。
四. 寫入文件頭
4.1 概述
比如操作的視頻的編碼格式是H264
,則需要寫入一些標題信息,比如頭部,協議版本之類的信息。
4.2 需要用到的接口說明
// 寫入頭部信息
int avformat_write_header(AVFormatContext *s, // 上面創建的封裝的上下文
AVDictionary **options); // 一些額外的設定參數,若不指定,可傳遞NULL
五. 寫入幀數據(需要考慮寫入次序)
5.1 概述
這里意思很明白,就是寫入具體的視頻或音頻信息。
在寫入具體的音視頻信息的時候,需要注意兩點:
-
PTS的計算。你寫入的音頻或視頻數據將來是要給播放器去播放的,這個時候就需要考慮一下計算
pts
,為什么呢,因為pts
本身就是用來指導播放器端的播放行為的。舉個例子,你生成了一個MP4
文件,它每一幀的播放次序和播放速度全部跟PTS
和DTS
相關。 -
寫入次序的問題。另外需要注意,你寫入的次序是什么(如果只是純視頻部分,很簡單,每編碼生成一個
AVPacket
我們就把它寫進去,但是加上音頻部分就沒有這么容易了,音視頻之間的pts
是否也要保持一定的次序,而且不能相差太大,因為將來解碼讀取的時候肯定是一段一段在讀的,可能音頻讀了一堆,但是視頻還沒有,還要繼續等,那這樣就會造成一些播放的延遲,有些播放器如果沒有處理好的話,可能會造成音視頻不同步,正常情況下每個播放器都有一個同步校驗,它是有一個超時機制的,就是超過多少時間播放器就不去同步了,如果硬去同步的話,會導致整個畫面停住,這樣給用戶的感覺不好,所以超過一段時間后,播放器就自動播放了,因此這里需要考慮一下寫入次序問題)。FFmpeg
提供了幾種方案,一種是由我們自己自行計算次序,還有一種方案就是通過FFmpeg
提供的接口來計算次序(通過內部緩沖來實現寫入的次序)。
5.2 需要用到的接口說明
// 用於PTS的轉換 (a * bq / cq) [最終文件的pts = 原來的pts * 原來的time_base / 最終文件的time_base]
int64_t av_rescale_q_rnd(int64_t a, // 源文件的PTS
AVRational bq, // 源文件的time_base
AVRational cq, // 目標的time_base
enum AVRounding rnd) av_const; // 轉換的規則,因為內部涉及到除法運算,而最終生成的結果又是整型,可以根據該規則確定是要四舍五入 // 還是其它
// 寫入幀數據[FFmpeg提供的寫入方案一]
int av_write_frame(AVFormatContext *s, // 上面創建的封裝的上下文
AVPacket *pkt); // 已編碼的音視頻幀
// 寫入幀數據[FFmpeg提供的寫入方案二]
int av_interleaved_write_frame(AVFormatContext *s, // 上面創建的封裝的上下文
AVPacket *pkt); // 已編碼的音視頻幀
關於
av_write_frame()
寫入方案中的pkt
:
- 該函數並不會改變傳入pkt的引用計數,即不會對pkt引用的數據做清理。可以傳
NULL
, 傳NULL
的話代表刷新它的寫入緩沖;- 寫入的
pkt
的stream_index
一定要與AVFormatContext
中的streams[]
相對應。比如現在AVFormatContext
中的streams[]
長度為2
:下標0
代表的是視頻,下標1
代表的是音頻,那么你在寫視頻數據的時候,pkt 的stream_index也必須是0
,寫音頻的話,stream index就是1
。pkt
的pts
也要計算好。如果寫入的pts
值計算錯誤,可能會打印一些錯誤信息,或寫入失敗,這里如果是計算視頻幀的pts
, 那就需要采用 streams[]中對應的AVStream
的time_base
, 比如假設視頻流的下標為0
,那就需要取streams[0]->time_base
來參與pts
的計算。
關於
av_interleaved_write_frame()
:它是
FFmpeg
引入的另外一個函數,也用於完成音視頻幀的寫入。既然av_write_frame()
已經可以實現寫入音視頻幀了,為什么要再引入這樣一個函數呢?有這樣一種場景: 假設原來存在有一個視頻文件,它里面同時包含有音頻和視頻,現在需要根據原有的視頻文件進行重新封裝,新的封裝格式要求其中的視頻部分需要轉碼成新的格式,其中的音頻部分不必轉碼直接保留。這樣可能會造成一種情況:音頻如果不需要轉碼的話,那可以直接讀一個音頻Packet就寫進去,讀一個音頻Packet就寫進去... 但是視頻部分因為要做轉碼,所以視頻部分可能需要花費一定時間才能編碼成新的指定格式,然后才能寫入,這樣的話就帶來一個問題,可能音頻幀已經寫入了很多,但是視頻幀卻寫入很少,這樣視頻幀和音頻幀之間的差距就會很大(比如相差了3秒),這個時候假設一個播放器它的緩沖區小於3秒,這樣的話音視頻就不同步了,因此這個時候就需要對
pts
進行計算, 也就是拿到音頻的packet之后,也不能立刻寫進去,可能需要把待寫入的packet先排好序,然后再寫進去,確保視頻幀和音頻幀的間隔相差不大。
av_interleaved_write_frame()
函數就是用來解決上述問題的,從字面意思上就可以看出,該函數的寫入方式是用於interleaved(交錯寫入),有區別於av_write_frame()
,后者則是直接寫入,那它們兩個有什么差別呢?差別一:處理寫入數據的方式不同,
av_interleaved_write_frame()
會在內部先緩沖,再寫入,av_write_frame()
則會直接寫入。這個函數會在內部緩沖數據包,也就是說你通過這個函數傳入了一個
音視頻Packet
,它並不會直接寫入,它會在內部把傳入的音視頻Packet
緩存起來,然后根據dts
進行排序,排好充之后再寫到文件當中去,以確保音視頻的差距不會太大。另外,由於該接口內部實現了緩沖機制,所以必然會涉及到對緩沖數據的處理,如果寫入文件結尾處需要將接口內部的緩沖數據一齊寫入到文件,這個時候可以給該接口傳
NULL
,即:av_interleaved_write_frame(NULL)
,傳NULL
它就會把剩余的緩沖數據全部寫入到文件當中。針對該接口內部的緩沖大小,可以通過
AVFormatContext
中的max_interleave_delta
成員進行設置。根據實際情況,如果你的音視頻流的時間相差過大的話,可以把這個緩沖值加大,如果相關不大,想節省內存的話,可以把該值改小一點。差別二:對寫入packet的引用計數的處理方式不同
如果傳入的packet是采取引用計數的方式,
av_interleaved_write_frame()
在使用完這個packet的時候,會對該packet的引用計數減1,調用此函數后就不可以在外部再訪問該packet,因為這樣,所以如果寫入方式是av_interleaved_write_frame()
, 就不需要再調用av_packet_unref()
,當然如果引用計數已經是0,你再調用一次也不會有問題。而如果是
av_write_frame()
這種方式寫入,它是不會改變packet的引用計數的,這個時候下次如果再讀一幀數據,則需要手動調用av_packet_unref()
.
六. 寫入尾部數據(pts索引)
6.1 概述
最后還需要寫入尾部的數據,就是當所有的幀數據全部編碼出來后,需要把每一幀在文件當中的偏移位置和pts
等數據寫入尾部,如果尾部沒有寫入的話,可能會造成你的視頻能播放,但是不能seek
,也不能看到視頻的時長
。
6.2 需要用到的接口說明
// 寫入尾部信息
int av_write_trailer(AVFormatContext *s); // 上面創建的封裝的上下文
七. 代碼演示
7.1 概述
下面演示如何重封裝一個新的MP4
文件,新封裝的視頻文件內容取自原有的視頻文件:v1080.mp4
7.2 示例Code
#include <iostream>
#include <thread>
using namespace std;
extern "C" { //指定函數是c語言函數,函數名不包含重載標注
//引用ffmpeg頭文件
#include <libavformat/avformat.h>
}
//預處理指令導入庫
#pragma comment(lib,"avformat.lib")
#pragma comment(lib,"avutil.lib")
#pragma comment(lib,"avcodec.lib")
void PrintErr(int err)
{
char buf[1024] = { 0 };
av_strerror(err, buf, sizeof(buf) - 1);
cerr << endl;
}
#define CERR(err) if(err!=0){ PrintErr(err);getchar();return -1;}
int main(int argc, char* argv[])
{
//打開媒體文件
const char* url = "v1080.mp4";
////////////////////////////////////////////////////////////////////////////////////
/// 解封裝
//解封裝輸入上下文
AVFormatContext* ic = nullptr;
auto re = avformat_open_input(&ic, url,
NULL, //封裝器格式 null 自動探測 根據后綴名或者文件頭
NULL //參數設置,rtsp需要設置
);
CERR(re);
//獲取媒體信息 無頭部格式
re = avformat_find_stream_info(ic, NULL);
CERR(re);
//打印封裝信息
av_dump_format(ic, 0, url,
0 //0表示上下文是輸入 1 輸出
);
AVStream* as = nullptr; //音頻流
AVStream* vs = nullptr; //視頻流
for (int i = 0; i < ic->nb_streams; i++)
{
//音頻
if (ic->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
{
as = ic->streams[i];
cout << "=====音頻=====" << endl;
cout << "sample_rate:" << as->codecpar->sample_rate << endl;
}
else if (ic->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
{
vs = ic->streams[i];
cout << "=========視頻=========" << endl;
cout << "width:" << vs->codecpar->width << endl;
cout << "height:" << vs->codecpar->height << endl;
}
}
////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////
/// 解封裝
//編碼器上下文
const char* out_url = "test_mux.mp4";
AVFormatContext* ec = nullptr;
re = avformat_alloc_output_context2(&ec, NULL, NULL,
out_url //根據文件名推測封裝格式
);
CERR(re);
//添加視頻流、音頻流
auto mvs = avformat_new_stream(ec, NULL); //視頻流
auto mas = avformat_new_stream(ec, NULL); //音頻流
//打開輸出IO
re = avio_open(&ec->pb, out_url, AVIO_FLAG_WRITE);
CERR(re);
//設置編碼音視頻流參數
//ec->streams[0];
//mvs->codecpar;//視頻參數
if (vs)
{
mvs->time_base = vs->time_base;// 時間基數與原視頻一致
//從解封裝復制參數
avcodec_parameters_copy(mvs->codecpar, vs->codecpar);
}
if (as)
{
mas->time_base = as->time_base;
//從解封裝復制參數
avcodec_parameters_copy(mas->codecpar, as->codecpar);
}
//寫入文件頭
re = avformat_write_header(ec, NULL);
CERR(re);
//打印輸出上下文
av_dump_format(ec, 0, out_url, 1);
////////////////////////////////////////////////////////////////////////////////////
AVPacket pkt;
for (;;)
{
re = av_read_frame(ic, &pkt);
if (re != 0)
{
PrintErr(re);
break;
}
if (vs && pkt.stream_index == vs->index)
{
cout << "視頻:";
}
else if (as && pkt.stream_index == as->index)
{
cout << "音頻:";
}
cout << pkt.pts << " : " << pkt.dts << " :" << pkt.size << endl;
//寫入音視頻幀 會清理pkt
re = av_interleaved_write_frame(ec,
&pkt);
if (re != 0)
{
PrintErr(re);
}
//av_packet_unref(&pkt);
//this_thread::sleep_for(100ms);
}
//寫入結尾 包含文件偏移索引
re = av_write_trailer(ec);
if (re != 0)PrintErr(re);
avformat_close_input(&ic);
avio_closep(&ec->pb);
avformat_free_context(ec);
ec = nullptr;
return 0;
}
注意:上面僅是一個最簡單的封裝,雖然可以正常跑通,但是可以說是毫無意義,基本上等於是把源視頻復制了一遍,接下來會嘗試從源視頻截取10s,然后再重新封裝成一個新的文件。