音視頻技術應用(19)- 封裝視頻的步驟


一. 創建上下文

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 概述

在上下文中插入音頻或視頻流信息。在AVFormatContextstreams[]數組中插入音頻或視頻流信息。

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 概述

這里意思很明白,就是寫入具體的視頻或音頻信息。

在寫入具體的音視頻信息的時候,需要注意兩點:

  1. PTS的計算。你寫入的音頻或視頻數據將來是要給播放器去播放的,這個時候就需要考慮一下計算pts,為什么呢,因為pts本身就是用來指導播放器端的播放行為的。舉個例子,你生成了一個MP4文件,它每一幀的播放次序播放速度全部跟 PTSDTS相關。

  2. 寫入次序的問題。另外需要注意,你寫入的次序是什么(如果只是純視頻部分,很簡單,每編碼生成一個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:

  1. 該函數並不會改變傳入pkt的引用計數,即不會對pkt引用的數據做清理。可以傳NULL, 傳NULL的話代表刷新它的寫入緩沖;
  2. 寫入的pktstream_index一定要與AVFormatContext中的 streams[]相對應。比如現在AVFormatContext中的 streams[]長度為2:下標0代表的是視頻,下標1代表的是音頻,那么你在寫視頻數據的時候,pkt 的stream_index也必須是0,寫音頻的話,stream index就是1
  3. pktpts也要計算好。如果寫入的pts值計算錯誤,可能會打印一些錯誤信息,或寫入失敗,這里如果是計算視頻幀的pts, 那就需要采用 streams[]中對應的AVStreamtime_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,然后再重新封裝成一個新的文件。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM