FFmpeg代碼結構
libavformat 實現了流協議,容器格式及其基本IO訪問
一:日志系統的使用
日志級別:(依次降低)
AV_LOG_ERROR
AV_LOG_WARNING
AV_LOG_INFO
AV_LOG_DEBUG
(一)日志系統編程

#include <stdio.h> #include <libavutil/log.h> int main(int argc,char* argv[]) { av_log_set_level(AV_LOG_DEBUG); av_log(NULL,AV_LOG_INFO,"...Hello world:%s %s\n",argv[0],argv[1]); return 0; }
編譯.c文件:
gcc 01log.c -o 01log -lavutil
運行結果:
(二)回顧gcc編譯如何尋找頭文件、庫文件(gcc -I -L -l區別)
我們用gcc編譯程序時,可能會用到“-I”(大寫i),“-L”(大寫l),“-l”(小寫l)等參數,例:
gcc -o hello hello.c -I /home/hello/include -L /home/hello/lib -lworld
上面這句表示在編譯hello.c時:
-I /home/hello/include : 表示將/home/hello/include目錄作為第一個尋找頭文件的目錄,尋找的順序是:/home/hello/include-->/usr/include-->/usr/local/include
-L /home/hello/lib : 表示將/home/hello/lib目錄作為第一個尋找庫文件的目錄,尋找的順序是:/home/hello/lib-->/lib-->/usr/lib-->/usr/local/lib
-lworld : 表示在上面的lib的路徑中尋找libworld.so動態庫文件或libworld.a靜態庫,同時存在時候動態庫優先,
如果要強制鏈接靜態庫可以用-static或直接用libword.a, gcc -o hello hello.c -I /home/hello/include -L /home/hello/lib /home/hello/lib/libworld.a
(三)linux中的動態庫和靜態庫
1.概念和區別:
靜態庫就是在編譯過程中一些目標文件的集合。靜態庫在程序鏈接的時候使用,鏈接器會將程序中使用到函數的代碼從庫文件中拷貝到應用程序中。一旦鏈接完成,在執行程序的時候就不需要靜態庫了。由於每個使用靜態庫的應用程序都需要拷貝所用函數的代碼,所以靜態鏈接的文件會比較大。
相對於靜態函數庫,動態函數庫在編譯的時候並沒有被編譯進目標代碼中,而只是作些標記。然后在程序開始啟動運行的時候,動態地加載所需模塊,因此動態函數庫所產生的可執行文件比較小。由於函數庫沒有被整合進你的程序,而是程序運行時動態的申請並調用,所以程序的運行環境中必須提供相應的庫。動態函數庫的改變並不影響你的程序,所以動態函數庫的升級比較方便。
2.命名:
靜態庫的名字一般為libxxxx.a,其中xxxx是該lib的名稱。
動態庫的名字一般為libxxxx.so.major.minor,xxxx是該lib的名稱,major是主版本號,minor是副版本號。版本號也可以沒有,一般都會建立個沒有版本號的軟連接文件鏈接到全名的庫文件。
3.創建:
無論靜態庫還是動態庫,創建都分為兩步,第一步創建目標文件,第二步生產庫。
1).靜態庫的創建:
gcc -c test.c -o test.o #生成編譯文件
ar rcs libtest.a test.o #生成靜態庫
名字為libtest.a的靜態庫就生產了,其中選項:
r 表明將模塊加入到靜態庫中;
c 表示創建靜態庫;
s 表示生產索引;
還有更多選項像增加、刪除庫中的目標文件,包括將靜態庫解包等可以通過man來獲得。
2).動態庫的創建:
gcc -fPIC -c test.c -o test.c
gcc --share test.o -o libtest.so
-fPIC 為了跨平台
4.使用:
編譯鏈接目標程序的方法是一樣的:
gcc main.c -L. -ltest -o main
-L. : 指定現在本目錄下搜索庫,如果沒有,會到系統默認的目錄下搜索,一般為/lib、/usr/lib下。
對於靜態庫,這個步驟之后就可以將libtest.a庫刪掉,因為它已經被編譯進了目標程序,不再需要它了。
而對於動態庫,libtest.so庫只是在目標程序里做了標記,在運行程序時才會動態加載,那么從哪加載呢?
加載目錄會由/etc/ld.so.conf來指定,一般默認是/lib、/usr/lib,所以要想讓動態庫順利加載,你可以將庫文件copy到上面的兩個目錄下,
或者設置export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/XXX/YYY,后面為你自己動態庫的目錄,再或者修改/etc/ld.so.conf文件,把庫所在的路徑加到文件末尾,並執行ldconfig刷新。這樣,加入的目錄下的所有庫文件都可見。
5.補充
另外還有個文件需要了解/etc/ld.so.cache,里面保存了常用的動態函數庫,且會先把他們加載到內存中,因為內存的訪問速度遠遠大於硬盤的訪問速度,這樣可以提高軟件加載動態函數庫的速度了。
最后提一點,當同一目錄下既有動態庫又有靜態庫,並且兩個庫的名字相同時,編譯時會如何鏈接呢?
gcc編譯時默認都是動態鏈接,如果要指定優先鏈接靜態庫,需要指定參數static。
6.使用案例:https://blog.csdn.net/ayz671101/article/details/101812040(重點)
(四)分析 gcc 01log.c -o 01log -lavutil
1.回顧安裝FFmpeg時的配置:Fmpeg學習(一)FFmpeg安裝與測試
因此,我們早就將FFmpeg動態庫目錄加入/etc/ld.so.conf文件中,因此-lavutil會先去FFmpeg庫目錄下查找
2.查看庫目錄
存在我們所需要的動態庫文件,所以編譯成功!!
二:文件的刪除與重命名
(一)文件編程

#include <libavutil/log.h> #include <libavformat/avformat.h> int main(int argc,char* argv[]) { int ret; //獲取返回值狀態 char* filename = "./2.txt"; av_log_set_level(AV_LOG_DEBUG); //設置日志級別 //1.移動文件測試 ret = avpriv_io_move("1.txt","3.txt"); if(ret<0){ av_log(NULL,AV_LOG_ERROR,"Failed to move\n"); return -1; } av_log(NULL,AV_LOG_INFO,"Success to move\n"); ret = avpriv_io_delete(filename); if(ret<0){ av_log(NULL,AV_LOG_ERROR,"Failed to delete\n"); return -1; } av_log(NULL,AV_LOG_INFO,"Success to delete %s\n",filename); return 0; }
編譯文件:
gcc -o fio ffmpeg_io.c -I /usr/local/ffmpeg/include -L /usr/local/ffmpeg/lib -lavutil -lavformat
注意:編譯過程中我們可以不需要指定-I 但是我們必須指定-L (沒有搞明白) ,雖然我們在去掉-L后也可以編譯成功,但是運行會出現以下問題:
我們可以修改程序:添加av_register_all() 初始化libavformat並注冊所有muxer、demuxer和協議(我們的File操作也在里面<可以自己查看源碼,推薦3.0版本,太高太多東西看不到,太低和自己使用的方法不兼容)。如果不調用此函數,則可以選擇希望支持的格式。

#include <libavutil/log.h> #include <libavformat/avformat.h> int main(int argc,char* argv[]) { int ret; //獲取返回值狀態 char* filename = "./2.txt"; av_log_set_level(AV_LOG_DEBUG); //設置日志級別 av_register_all(); //1.移動文件測試 ret = avpriv_io_move("1.txt","3.txt"); if(ret<0){ av_log(NULL,AV_LOG_ERROR,"Failed to move\n"); return -1; } av_log(NULL,AV_LOG_INFO,"Success to move\n"); ret = avpriv_io_delete(filename); if(ret<0){ av_log(NULL,AV_LOG_ERROR,"Failed to delete\n"); return -1; } av_log(NULL,AV_LOG_INFO,"Success to delete %s\n",filename); return 0; }
編譯方案推薦第一種,雖然還沒搞明白,但是兼容性看來好些,不容易出錯!!!
三:目錄操作
(一)重要結構體
AVIODirContext:操作目錄的上下文,由avio_open_dir方法進行賦值

typedef struct URLContext { const AVClass *av_class; /**< information for av_log(). Set by url_open(). */ struct URLProtocol *prot; void *priv_data; char *filename; /**< specified URL */ int flags; int max_packet_size; /**< if non zero, the stream is packetized with this max packet size */ int is_streamed; /**< true if streamed (no seek possible), default = false */ int is_connected; AVIOInterruptCB interrupt_callback; int64_t rw_timeout; /**< maximum time to wait for (network) read/write operation completion, in mcs */ const char *protocol_whitelist; } URLContext

typedef struct AVIODirContext { struct URLContext *url_context; } AVIODirContext;

int avio_open_dir(AVIODirContext **s, const char *url, AVDictionary **options) { URLContext *h = NULL; AVIODirContext *ctx = NULL; int ret; av_assert0(s); ctx = av_mallocz(sizeof(*ctx)); if (!ctx) { ret = AVERROR(ENOMEM); goto fail; } if ((ret = ffurl_alloc(&h, url, AVIO_FLAG_READ, NULL)) < 0) goto fail; if (h->prot->url_open_dir && h->prot->url_read_dir && h->prot->url_close_dir) { if (options && h->prot->priv_data_class && (ret = av_opt_set_dict(h->priv_data, options)) < 0) goto fail; ret = h->prot->url_open_dir(h); } else ret = AVERROR(ENOSYS); if (ret < 0) goto fail; h->is_connected = 1; ctx->url_context = h; *s = ctx; return 0; fail: av_free(ctx); *s = NULL; ffurl_close(h); return ret; }
AVIODirEntry:目錄項。用於存放文件名、文件大小等信息

typedef struct AVIODirEntry { char *name; /**< Filename */ int type; /**< Type of the entry */ int utf8; /**< Set to 1 when name is encoded with UTF-8, 0 otherwise. Name can be encoded with UTF-8 even though 0 is set. */ int64_t size; /**< File size in bytes, -1 if unknown. */ int64_t modification_timestamp; /**< Time of last modification in microseconds since unix epoch, -1 if unknown. */ int64_t access_timestamp; /**< Time of last access in microseconds since unix epoch, -1 if unknown. */ int64_t status_change_timestamp; /**< Time of last status change in microseconds since unix epoch, -1 if unknown. */ int64_t user_id; /**< User ID of owner, -1 if unknown. */ int64_t group_id; /**< Group ID of owner, -1 if unknown. */ int64_t filemode; /**< Unix file mode, -1 if unknown. */ } AVIODirEntry;

int avio_read_dir(AVIODirContext *s, AVIODirEntry **next) { URLContext *h; int ret; if (!s || !s->url_context) return AVERROR(EINVAL); h = s->url_context; if ((ret = h->prot->url_read_dir(h, next)) < 0) avio_free_directory_entry(next); return ret; }
(二)目錄信息編程

#include <libavutil/log.h> #include <libavformat/avformat.h> int main(int argc,char* argv[]) { int ret; AVIODirContext* ctx=NULL; //操作目錄的上下文,將由avio_open_dir賦值 AVIODirEntry* entry=NULL; //獲取文件項的信息 av_log_set_level(AV_LOG_INFO); //打開目錄 ret = avio_open_dir(&ctx,"./",NULL); if(ret<0){ av_log(NULL,AV_LOG_ERROR,"Can`t open dir:%s\n",av_err2str(ret)); goto _fail; } //讀取文件項 while(1){ ret = avio_read_dir(ctx,&entry); if(ret<0){ av_log(NULL,AV_LOG_ERROR,"Can`t read dir:%s\n",av_err2str(ret)); goto _fail; } if(!entry) break; av_log(NULL,AV_LOG_INFO,"%12"PRId64" %s\n",entry->size,entry->name); //PRId64表示打印64位數據,前面12是占位 avio_free_directory_entry(&entry); //釋放使用過的空間 } _fail: avio_close_dir(&ctx); return 0; }
四:處理流數據的基本概念
(一)基本概念
多媒體文件(mp4、flv...)其實是個容器,在容器里有很多流(Stream/Track)(音頻流、視頻流、....沒有交叉性<即便是多路音頻、視頻>),每種流是由不同的編碼器編碼的。
從流中讀出的數據稱為包(幀壓縮后),在一個包中包含着一個或多個幀(未壓縮)。
(二)幾個重要的結構體
分別對應多媒體文件上下文、流、包
(三)ffmpeg 操作流數據的基本步驟
1.解復用:打開多媒體文件 2.獲取文件中多路流中想要的 3.獲取數據包,進行解碼,對原始數據進行處理,變聲、變速、濾波 4.釋放相關資源
五:打印音/視頻Meta信息
av_register_all() : 初始化libavformat並注冊所有muxer、demuxer和協議。(所有FFmpeg程序開始前都要去調用他) avformat_open_input()/avformat_close_input() : 打開、關閉多媒體文件,結合前面的結構體 av_dump_format() : 打印多媒體meta信息
(一)多媒體文件meta數據獲取

#include <libavutil/log.h> #include <libavformat/avformat.h> int main(int argc,char* argv[]) { int ret; AVFormatContext* fmt_ctx = NULL; AVStream* stm = NULL; AVPacket* pkt = NULL; av_register_all(); av_log_set_level(AV_LOG_INFO); ret = avformat_open_input(&fmt_ctx,"./gfxm.mp4",NULL,NULL); if(ret<0){ av_log(NULL,AV_LOG_ERROR,"Can`t open file: %s\n",av_err2str(ret)); return -1; } ret = avformat_find_stream_info(fmt_ctx, 0); //獲取流詳細信息 if(ret<0){ av_log(NULL,AV_LOG_WARNING,"Can`t get stream information!just show aac, not show aac(LC)!\n"); } av_dump_format(fmt_ctx,0,"./gfxm.mp4",0); //第一個0是流的索引值,,第二個表示輸入/輸出流,由於是輸入文件,所以為0 //關閉上下文 avformat_close_input(&fmt_ctx); return 0; }
其中Input是我們設置的0號索引流,其中Stream表示多路流(第一路流為視頻流,第二路為音頻流)
1.沒有加上avformat_find_stream_info時,缺少部分信息(后面數據處理時需要用到,如acc(LC),下面並沒有顯示LC)
2.使用avformat_find_stream_info后,顯示完整信息
六:FFmpeg抽取音頻數據
av_init_packet() 初始化一個數據包結構體 av_find_best_stream() 由四(一)可以知道在多媒體文件中有多種流,而每種流可能存在多路,該函數可以幫助找到其中最佳的一路流 av_read_frame()/av_packet_unref() 拿到流之后使用av_read_frame()獲取流的數據包
從流中讀取數據包之后,數據包就會增減引用基數,當包不用的時候,調用av_packet_unref(),將包的引用基數減 1。ffmpeg 檢測到包的引用基數為0的時候,就是釋放相應的資源,防止內存泄露。
補充:抽取出來的aac文件需要加adts頭才能正常播放
AAC的ADTS頭文件信息介紹:https://blog.csdn.net/qq_29028177/article/details/54694861(重點)

void adts_header(char *szAdtsHeader, int dataLen){ int audio_object_type = 2; //通過av_dump_format顯示音頻信息或者ffplay獲取多媒體文件的音頻流編碼acc(LC),對應表格中Object Type ID -- 2 int sampling_frequency_index = 4; //音頻信息中采樣率為44100 Hz 對應采樣率索引0x4 int channel_config = 2; //音頻信息中音頻通道為雙通道2 int adtsLen = dataLen + 7; //采用頭長度為7字節,所以protection_absent=1 =0時為9字節,表示含有CRC校驗碼 szAdtsHeader[0] = 0xff; //syncword :總是0xFFF, 代表一個ADTS幀的開始, 用於同步. 高8bits szAdtsHeader[1] = 0xf0; //syncword:0xfff 低4bits szAdtsHeader[1] |= (0 << 3); //MPEG Version:0 : MPEG-4(mp4a),1 : MPEG-2 1bit szAdtsHeader[1] |= (0 << 1); //Layer:0 2bits szAdtsHeader[1] |= 1; //protection absent:1 沒有CRC校驗 1bit szAdtsHeader[2] = (audio_object_type - 1)<<6; //profile=(audio_object_type - 1) 表示使用哪個級別的AAC 2bits szAdtsHeader[2] |= (sampling_frequency_index & 0x0f)<<2; //sampling frequency index:sampling_frequency_index 4bits szAdtsHeader[2] |= (0 << 1); //private bit:0 1bit szAdtsHeader[2] |= (channel_config & 0x04)>>2; //channel configuration:channel_config 高1bit szAdtsHeader[3] = (channel_config & 0x03)<<6; //channel configuration:channel_config 低2bits szAdtsHeader[3] |= (0 << 5); //original:0 1bit szAdtsHeader[3] |= (0 << 4); //home:0 1bit ----------------固定頭完結,開始可變頭 szAdtsHeader[3] |= (0 << 3); //copyright id bit:0 1bit szAdtsHeader[3] |= (0 << 2); //copyright id start:0 1bit szAdtsHeader[3] |= ((adtsLen & 0x1800) >> 11); //frame length:value 高2bits 000|1 1000|0000 0000 szAdtsHeader[4] = (uint8_t)((adtsLen & 0x7f8) >> 3); //frame length:value 中間8bits 0000 0111 1111 1000 szAdtsHeader[5] = (uint8_t)((adtsLen & 0x7) << 5); //frame length:value 低 3bits 0000 0000 0000 0111 //number_of_raw_data_blocks_in_frame:表示ADTS幀中有number_of_raw_data_blocks_in_frame + 1個AAC原始幀。所以說number_of_raw_data_blocks_in_frame == 0 表示說ADTS幀中有一個AAC數據塊。(一個AAC原始幀包含一段時間內1024個采樣及相關數據) szAdtsHeader[5] |= 0x1f; //buffer fullness:0x7ff 高5bits 0x7FF 說明是碼率可變的碼流 ---> 111 1111 1111 00----> 1 1111 1111 1100--->0x1f與0xfc szAdtsHeader[6] = 0xfc; }

#include <libavutil/log.h> #include <libavformat/avformat.h> #include <libavcodec/avcodec.h> #define ADTS_HEAD_LEN 7 void adts_header(char *szAdtsHeader, int dataLen){ int audio_object_type = 2; //通過av_dump_format顯示音頻信息或者ffplay獲取多媒體文件的音頻流編碼acc(LC),對應表格中Object Type ID -- 2 int sampling_frequency_index = 4; //音頻信息中采樣率為44100 Hz 對應采樣率索引0x4 int channel_config = 2; //音頻信息中音頻通道為雙通道2 int adtsLen = dataLen + 7; //采用頭長度為7字節,所以protection_absent=1 =0時為9字節,表示含有CRC校驗碼 szAdtsHeader[0] = 0xff; //syncword :總是0xFFF, 代表一個ADTS幀的開始, 用於同步. 高8bits szAdtsHeader[1] = 0xf0; //syncword:0xfff 低4bits szAdtsHeader[1] |= (0 << 3); //MPEG Version:0 : MPEG-4(mp4a),1 : MPEG-2 1bit szAdtsHeader[1] |= (0 << 1); //Layer:0 2bits szAdtsHeader[1] |= 1; //protection absent:1 沒有CRC校驗 1bit szAdtsHeader[2] = (audio_object_type - 1)<<6; //profile=(audio_object_type - 1) 表示使用哪個級別的AAC 2bits szAdtsHeader[2] |= (sampling_frequency_index & 0x0f)<<2; //sampling frequency index:sampling_frequency_index 4bits szAdtsHeader[2] |= (0 << 1); //private bit:0 1bit szAdtsHeader[2] |= (channel_config & 0x04)>>2; //channel configuration:channel_config 高1bit szAdtsHeader[3] = (channel_config & 0x03)<<6; //channel configuration:channel_config 低2bits szAdtsHeader[3] |= (0 << 5); //original:0 1bit szAdtsHeader[3] |= (0 << 4); //home:0 1bit ----------------固定頭完結,開始可變頭 szAdtsHeader[3] |= (0 << 3); //copyright id bit:0 1bit szAdtsHeader[3] |= (0 << 2); //copyright id start:0 1bit szAdtsHeader[3] |= ((adtsLen & 0x1800) >> 11); //frame length:value 高2bits 000|1 1000|0000 0000 szAdtsHeader[4] = (uint8_t)((adtsLen & 0x7f8) >> 3); //frame length:value 中間8bits 0000 0111 1111 1000 szAdtsHeader[5] = (uint8_t)((adtsLen & 0x7) << 5); //frame length:value 低 3bits 0000 0000 0000 0111 //number_of_raw_data_blocks_in_frame:表示ADTS幀中有number_of_raw_data_blocks_in_frame + 1個AAC原始幀。所以說number_of_raw_data_blocks_in_frame == 0 表示說ADTS幀中有一個AAC數據塊。(一個AAC原始幀包含一段時間內1024個采樣及相關數據) szAdtsHeader[5] |= 0x1f; //buffer fullness:0x7ff 高5bits 0x7FF 說明是碼率可變的碼流 ---> 111 1111 1111 00----> 1 1111 1111 1100--->0x1f與0xfc szAdtsHeader[6] = 0xfc; } int main(int argc,char* argv[]) { //參數初始化以及檢測 int ret,audio_idx,len; AVFormatContext* fmt_ctx = NULL; AVPacket pkt; //不是指針 char* src,*dst; FILE* dst_fd = NULL; //文件句柄 char AdtsHeader[ADTS_HEAD_LEN]; if(argc<3){ av_log(NULL,AV_LOG_ERROR,"the count of params should be more than 3!\n"); return -1; } src = argv[1]; dst = argv[2]; if(!src||!dst){ av_log(NULL,AV_LOG_ERROR,"src or dst is null!\n"); return -1; } //環境設置 av_register_all(); av_log_set_level(AV_LOG_INFO); //打開多媒體文件 ret = avformat_open_input(&fmt_ctx,src,NULL,NULL); //第三個參數強制指定AVFormatContext中AVInputFormat的。這個參數一般情況下可以設置為NULL,這樣FFmpeg可以自動檢測AVInputFormat;第四個為附加選項,一般為NULL if(ret<0){ av_log(NULL,AV_LOG_ERROR,"Can`t open file: %s\n",av_err2str(ret)); if(fmt_ctx) avformat_close_input(&fmt_ctx); return -1; } //開始獲取流(和元數據不一樣),這里自動去獲取獲取最佳流 //媒體文件句柄 / 流類型 / 請求的流編號(-1則自動去找) / 相關流索引號(比如音頻對應的視頻流索引號),不指定則-1 / 如果非空,則返回所選流的解碼器(指針獲取) / flag當前未定義 ret = av_find_best_stream(fmt_ctx,AVMEDIA_TYPE_AUDIO,-1,-1,NULL,0); //成功則返回非負流號 if(ret<0){ av_log(NULL,AV_LOG_ERROR,"Can`t find the best stream!\n"); avformat_close_input(&fmt_ctx); return -1; } audio_idx = ret; ret = avformat_find_stream_info(fmt_ctx, 0); //獲取流詳細信息 if(ret<0){ av_log(NULL,AV_LOG_WARNING,"Can`t get stream information!just show aac, not show aac(LC)!\n"); } //打印我們要獲取的流的元數據 av_dump_format(fmt_ctx,audio_idx,src,0); ////第一個0是流的索引值,,第二個表示輸入/輸出流,由於是輸入文件,所以為0 //打開目標文件 dst_fd = fopen(dst,"wb"); if(!dst_fd){ avformat_close_input(&fmt_ctx); av_log(NULL,AV_LOG_ERROR,"Can`t open dst file!\n"); return -1; } //開始從流中讀取包,先初始化包結構 av_init_packet(&pkt); while(av_read_frame(fmt_ctx,&pkt)>=0){ //讀取包 if(pkt.stream_index==audio_idx){ adts_header(AdtsHeader,pkt.size); //設置ADTS頭部 len = fwrite(AdtsHeader,1,ADTS_HEAD_LEN,dst_fd); //寫入ADTS頭部到文件中去 if(len!=ADTS_HEAD_LEN){ av_log(NULL,AV_LOG_WARNING,"warning,ADTS Header is not send to dest file!\n"); } len = fwrite(pkt.data,1,pkt.size,dst_fd); //寫入音頻數據到文件中去 if(len!=pkt.size){ av_log(NULL,AV_LOG_WARNING,"warning,length of data is not equal size of packet!\n"); } } //每讀取一次包,就需要將包引用-1,使得內存釋放 av_packet_unref(&pkt); } //關閉文件 avformat_close_input(&fmt_ctx); if(dst_fd){ fclose(dst_fd); } return 0; }
gcc ffmpeg_ad.c -o fad -I /usr/local/ffmpeg/include/ -L /usr/local/ffmpeg/lib/ -lavutil -lavformat -lavcodec
ffplay gfxm.aac
七: FFmpeg轉換H264數據視頻,從MP4(AVCC)格式到(AnnexB實時流)
(一)基礎知識
H264流媒體協議解析
FFmpeg AVPacket 剖析以及使用

typedef struct AVPacket { /** * A reference to the reference-counted buffer where the packet data is * stored. * May be NULL, then the packet data is not reference-counted. */ AVBufferRef *buf; /** * Presentation timestamp in AVStream->time_base units; the time at which * the decompressed packet will be presented to the user. * Can be AV_NOPTS_VALUE if it is not stored in the file. * pts MUST be larger or equal to dts as presentation cannot happen before * decompression, unless one wants to view hex dumps. Some formats misuse * the terms dts and pts/cts to mean something different. Such timestamps * must be converted to true pts/dts before they are stored in AVPacket. */ int64_t pts; /** * Decompression timestamp in AVStream->time_base units; the time at which * the packet is decompressed. * Can be AV_NOPTS_VALUE if it is not stored in the file. */ int64_t dts; uint8_t *data; int size; int stream_index; /** * A combination of AV_PKT_FLAG values */ int flags; /** * Additional packet data that can be provided by the container. * Packet can contain several types of side information. */ AVPacketSideData *side_data; int side_data_elems; /** * Duration of this packet in AVStream->time_base units, 0 if unknown. * Equals next_pts - this_pts in presentation order. */ int64_t duration; int64_t pos; ///< byte position in stream, -1 if unknown #if FF_API_CONVERGENCE_DURATION /** * @deprecated Same as the duration field, but as int64_t. This was required * for Matroska subtitles, whose duration values could overflow when the * duration field was still an int. */ attribute_deprecated int64_t convergence_duration; #endif } AVPacket;

typedef struct AVBufferRef { AVBuffer *buffer; /** * The data buffer. It is considered writable if and only if * this is the only reference to the buffer, in which case * av_buffer_is_writable() returns 1. */ uint8_t *data; /** * Size of data in bytes. */ #if FF_API_BUFFER_SIZE_T int size; #else size_t size; #endif } AVBufferRef;
細心發現pkt.buf->size總比pkt.size多64個字節,對應着宏AV_INPUT_BUFFER_PADDING_SIZE值,所以,如果實際應用中要修改pkt中的數據,pkt.buf->size是無時無刻都要比pkt.size多64個字節。
(二)代碼實現
流程圖轉自:https://blog.csdn.net/ty13392186270/article/details/106826367

1 #include <stdio.h> 2 #include <libavutil/log.h> 3 #include <libavformat/avio.h> 4 #include <libavformat/avformat.h> 5 #include <libavcodec/avcodec.h> 6 7 //這里是將前面的(startcode+SPS+PPS,size)+(NALU數據,size)傳入函數中,使得函數在NALU 前面加入startcode;這里的startcode不是4字節,而是3字節 8 static int alloc_and_copy(AVPacket* out,const uint8_t* sps_pps,uint32_t sps_pps_size, 9 const uint8_t* in,uint32_t in_size) 10 { 11 uint32_t offset = out->size; //偏移量,就是out已有數據的大小,后面再寫入數據就要從偏移量處開始操作 12 uint8_t start_code_size = sps_pps==NULL?3:4; //特征碼的大小,SPS/PPS占4字節,其余占3字節 13 int err; 14 15 err = av_grow_packet(out,sps_pps_size+in_size+start_code_size); //包擴容,在原來out已有數據基礎上進行擴容,使得可以加入所有數據 16 if(err<0) 17 return err; 18 19 //1.含有SPS、PPS數據,直接寫入。注意:在SPS、PPS前面已經寫入了start code 20 if(sps_pps) 21 memcpy(out->data+offset,sps_pps,sps_pps_size); //先寫入SPS、PPS數據 22 23 //2.在NALU前面加入start code 24 for(int i=0;i<start_code_size;i++){ 25 (out->data+offset+sps_pps_size)[i] = i==start_code_size-1?1:0; 26 } 27 28 //3.最后將NALU數據拷貝過去 29 memcpy(out->data+offset+sps_pps_size+start_code_size,in,in_size); 30 31 return 0; 32 } 33 34 //從AVCC中的extradata中獲取SPS、PPS數據;此外由於SPS、PPS前面的start code為4字節,所以我們這里直接寫入進去吧 35 //注意:第4個參數padding,是表示AVBufferRef中填充的字節數。取決於ffmpeg版本,這里是64字節;所以AVPacket中data大小最大字節數為INT_MAX - 64;而這里64被宏定義為AV_INPUT_BUFFER_PADDING_SIZE 36 int h264_extradata_to_annexb(const uint8_t* codec_extradata,const int codec_extradata_size,AVPacket* out_extradata,int padding) 37 { 38 uint16_t unit_size; //讀取兩個字節,用來獲取SPS、PPS的大小 39 uint64_t total_size = 0; //用來記錄從extradata中讀取的全部SPS、PPS大小,最后來驗證大小不要超過AVPacket中data的大小限制(因為我們最后是將數據存放在AVPacket中返回的) 40 41 uint8_t* out = NULL;//out:是一個指向一段內存的指針,這段內存用於存放所有拷貝的sps/pps數據和其特征碼數據 42 uint8_t unit_nb; //unit_nb:sps/pps個數 43 uint8_t sps_done = 0; //表示sps數據是否已經處理完畢,當sps_done為0,表示沒有處理,處理完成后不為0 44 uint8_t sps_seen = 0, sps_offset = 0; //sps_seen:是否有sps數據 sps_offset:sps數據的偏移,為0 45 uint8_t pps_seen = 0, pps_offset = 0; //pps_seen:是否有pps數據 pps_offset:pps數據的偏移,因為pps數據在sps后面,所以其偏移就是所有sps數據長度+sps的特征碼所占字節數 46 47 static const uint8_t start_code[4] = {0,0,0,1}; //記錄start code 48 49 const uint8_t* extradata = codec_extradata + 4; //擴展數據的前4字節無用,跳過 50 int length_size = ((*extradata++)&0x3) + 1; //第一個字節的后兩位存放NALULengthSizeMinusOne字段的值(0,1,2,3)+1=(1,2,3,4)其中3不被使用;這個字段要被返回的 51 52 sps_offset = pps_offset = -1; 53 54 //先獲取SPS的個數 大小1字節,在后5位中 55 unit_nb = (*extradata++)&0x1f; 56 if(!unit_nb){ //沒有SPS 57 goto pps; //就直接去獲取PPS數據 58 }else{ //有SPS數據 59 sps_offset = 0; //SPS是最開始的,不需要偏移 60 sps_seen = 1; //表示有SPS數據 61 } 62 63 while(unit_nb--){ //開始處理SPS、PPS類型的每一個數據,一般SPS、PPS都是1個 64 int err; 65 //先讀取2個字節的數據,用來表示SPS/PPS的數據長度 66 unit_size = (extradata[0] << 8) | extradata[1]; 67 total_size += unit_size + 4; //+4是加開始碼,注意:total_size是累加了每一次獲取SPS、PPS的數據量 68 if(total_size > INT_MAX - padding){ //防止數據溢出AVPacket的data大小 69 av_log(NULL,AV_LOG_ERROR,"Too big extradata size, corrupted stream or invalid MP4/AVCC bitstream\n"); 70 av_free(out); 71 return AVERROR(EINVAL); 72 } 73 //判斷數據是否越界 74 if(extradata+2+unit_size>codec_extradata+codec_extradata_size){ 75 av_log(NULL,AV_LOG_ERROR,"Packet header is not contained in global extradata, corrupted stream or invalid MP4/AVCC bitstream\n"); 76 av_free(out); //釋放前面的空間 77 return AVERROR(EINVAL); 78 } 79 //開始為out指針分配空間 80 if((err = av_reallocp(&out,total_size+padding))<0) //reallocp是在原來空間上擴充,已經存在的數據不會被丟棄 81 return err; 82 memcpy(out+total_size-unit_size-4,start_code,4); //先拷貝start code到out中 83 memcpy(out+total_size-unit_size,extradata+2,unit_size); //拷貝對應的SPS、PPS數據 84 extradata += unit_size+2; //注意多加2,前面沒有跳過長度信息 85 86 pps: //獲取完成SPS后,會開始從這里更新PPS的信息到上面的unit_nb中 87 if(!unit_nb && !sps_done++){ //當SPS獲取完成以后,unit_nb=0;!sps_done=1; 注意,sps_done++,導致不為0,獲取一次PPS之后,后面就不會在進入這里 88 unit_nb = *extradata++; //當讀取了所有SPS數據以后,再讀取一個字節,用來表示PPS的個數,然后再循環去獲取PPS的數據 89 if(unit_nb){ //PPS存在 90 pps_offset = total_size;//表示前面的SPS已經獲取完成,后面偏移寫入PPS數據即可 91 pps_seen = 1; //表示獲取了PPS數據 92 } 93 } 94 } 95 96 if(out) //開始進行數據0填充 97 memset(out+total_size,0,padding); 98 if(!sps_seen) //沒有獲取到SPS數據 99 av_log(NULL,AV_LOG_WARNING,"Warning: SPS NALU missing or invalid. The resulting stream may not play.\n"); 100 101 if(!pps_seen) //沒有獲取到PPS數據 102 av_log(NULL,AV_LOG_WARNING,"Warning: PPS NALU missing or invalid. The resulting stream may not play.\n"); 103 //將數據賦值給AVPacket中返回 104 out_extradata->data = out; 105 out_extradata->size = total_size; 106 107 return length_size; //返回前綴長度 108 } 109 110 //負責將H264格式的本地mp4文件從AVCC格式轉為實時流AnnexB格式 111 int h264_mp4toannexb(AVFormatContext * fmt_ctx, AVPacket* in,FILE* dst_fd) 112 { 113 AVPacket* out = NULL; //設置輸入包信息 114 AVPacket spspps_pkt; //用來存放SPS、PPS信息,對於AnnexB,我們需要在所有I幀前面加上SPS、PPS數據 115 116 int len; //保存fwrite返回寫入的數據長度 117 uint8_t unit_type; //存放NALU的header,長度為8bits 118 119 uint8_t nal_size_len; //AVCC格式數據采用NALU長度(固定字節,一般為4字節,取決與extradata中的NALULengthSizeMinusOne字段)分隔NALU 120 int32_t nal_size; //由nal_size_len可以知道,保存NALU長度一般可以取(1、2、4字節),我們這里取4字節,兼容所有 121 122 uint32_t cumul_size = 0;//存放當前包中已經處理多少字節數據,當==buf_size表示都處理完了,退出循環 123 uint32_t buf_size; //存放in中數據data的大小 124 const uint8_t* buf; //采訪in中數據的起始地址(注意:使用uint_8,按單字節增長) 125 const uint8_t* buf_end; //采訪in中數據的結束地址 126 127 int ret = 0,i; //存放返回值,以及循環變量i 128 129 buf = in->data; //指向AVPacket數據data開頭 130 buf_end = in->data + in->size; //指向AVPacket數據data的末尾 131 buf_size = in->size; //記錄AVPacket數據data的大小 132 133 //我們是將AVCC格式數據轉為AnnexB格式,所以首先去讀取SPS、PPS數據,因為AVCC格式數據保存在extradata中。 134 //而且AVCC格式用於存儲,比如MP4,並非實時流,所以SPS、PPS不會在中間被修改,所以我們獲取一次即可!!!! 135 nal_size_len = h264_extradata_to_annexb(fmt_ctx->streams[in->stream_index]->codec->extradata, 136 fmt_ctx->streams[in->stream_index]->codec->extradata_size, 137 &spspps_pkt, 138 AV_INPUT_BUFFER_PADDING_SIZE 139 ); //獲取SPS、PPS數據,並且返回前綴值 140 if(nal_size_len<0) 141 return -1; 142 143 out = av_packet_alloc(); //為out AVPacket分配數據空間 144 145 do{ 146 ret = AVERROR(EINVAL); //初始一個返回錯誤碼,無效參數值(不用管) 147 if(buf + nal_size_len > buf_end) 148 goto fail; //我們輸入數據是AVCC格式,該格式數據前4字節用於存放NALU長度信息,如果連這個4字節都不存在,則返回錯誤 149 //先假設nal_size_len=4 150 for(nal_size=0,i=0;i<nal_size_len;i++){ //開始獲取NALU長度信息 151 nal_size = (nal_size<<8) | buf[i]; //注意:視頻數據存放時,是大端格式,我們要**讀取**長度信息,需要進行相應處理(如果只是單純寫入,就不需要處理,但是我們需要去讀取長度)!!! 152 } 153 154 //buf指針后移,指向NALU數據的header部分 155 buf += nal_size_len; //跳過NALU長度部分數據,進入NALU主要數據區域 156 unit_type = (*buf) & 0x1f; //header長度為1字節,前3bits影響不大,我們獲取后面5bits,去獲取NALU類型下信息 157 158 if(nal_size>buf_end-buf||nal_size<0) //檢查長度,是否有效 159 goto fail; 160 161 //開始判斷NALU單元的類型,是否為關鍵幀,如果是關鍵幀,我們需要在其前面加入SPS、PPS信息 162 if(unit_type == 5){ 163 FILE* sp = fopen("spspps.h264","ab"); 164 165 len = fwrite(spspps_pkt.data,1,spspps_pkt.size,sp); 166 167 fflush(sp); 168 fclose(sp); 169 //先寫入start code和SPS和PPS數據,都被保存在前面spspps_pkt的data中,我們轉放入out中 170 if((ret=alloc_and_copy(out,spspps_pkt.data,spspps_pkt.size,buf,nal_size))<0) //這里是將前面的startcode+SPS+PPS+NALU數據傳入函數中,使得函數在NALU 前面加入startcode;這里的startcode不是4字節,而是3字節 171 goto fail; 172 }else{ //對於非關鍵幀,不需要SPS、PPS數據 173 if((ret=alloc_and_copy(out,NULL,0,buf,nal_size))<0) //這里是將前面的NALU數據傳入函數中,使得函數在NALU 前面加入startcode 174 goto fail; 175 } 176 177 //將上面的數據,無論是關鍵幀、非關鍵幀 都組織好,輸出到目標文件中去 178 len = fwrite(out->data,1,out->size,dst_fd); 179 if(len != out->size){ 180 av_log(NULL,AV_LOG_DEBUG,"Warning, length of writed data isn`t equal pkt.size(%d,%d)\n",len,out->size); 181 } 182 183 fflush(dst_fd); 184 //開始判斷下一個nalu 185 buf += nal_size; 186 cumul_size += nal_size + nal_size_len; //算上前綴長度才能對應 187 }while(cumul_size<buf_size); //循環繼續條件 188 189 fail: //進行統一錯誤處理 190 av_packet_free(&out); 191 return ret; 192 } 193 194 int main(int argc,char* argv[]) 195 { 196 int err_code; //獲取返回值 197 char errors[1024]; //獲取ffmpeg返回根據錯誤碼返回的錯誤信息 198 char* src = NULL; //輸入文件路徑 199 char* dst = NULL; //輸出文件路徑 200 201 av_log_set_level(AV_LOG_INFO); //設置日志級別 202 203 if(argc<3){ //無法獲取src,dst,則返回錯誤 204 av_log(NULL,AV_LOG_ERROR,"The number of parameters must be greater than 3!!!\n"); 205 return -1; 206 } 207 208 //設置文件路徑 209 src = argv[1]; 210 dst = argv[2]; 211 if(!src || !dst){ 212 av_log(NULL,AV_LOG_ERROR,"the file path of src or dst can`t be empty!!\n"); 213 return -1; 214 } 215 216 av_register_all(); //初始化libavformat並注冊所有muxer、demuxer和協議 217 218 //打開輸入多媒體文件,獲取上格式下文 219 AVFormatContext* fmt_ctx = NULL; 220 err_code = avformat_open_input(&fmt_ctx,src,NULL,NULL); 221 if(err_code<0){ 222 av_strerror(err_code,errors,1024); 223 av_log(NULL,AV_LOG_ERROR,"open media %s file failure : %d,(%s)!!!\n",src,err_code,errors); 224 return -1; 225 } 226 227 //獲取找到其中最佳的一路視頻流 228 int video_idx; 229 //媒體文件句柄 / 流類型 / 請求的流編號(-1則自動去找) / 相關流索引號(比如音頻對應的視頻流索引號),不指定則-1 / 如果非空,則返回所選流的解碼器(指針獲取) / flag當前未定義 230 video_idx = av_find_best_stream(fmt_ctx,AVMEDIA_TYPE_VIDEO,-1,-1,NULL,0); 231 if(video_idx<0){ 232 av_log(NULL,AV_LOG_DEBUG,"Can`t find %s stream in input file (%s)!!!\n", 233 av_get_media_type_string(AVMEDIA_TYPE_VIDEO),src); //去獲取AVMEDIA_TYPE_VIDEO對應的string 234 avformat_close_input(&fmt_ctx); //釋放前面的空間 235 return -1; 236 } 237 238 //輸出我們獲取的流的元信息 239 err_code = avformat_find_stream_info(fmt_ctx, 0); //獲取流詳細信息,0表示沒有額外參數 240 if(err_code<0){ 241 av_log(NULL,AV_LOG_WARNING,"Can`t get detail stream information!\n"); 242 } 243 //打印我們要獲取的流的元數據 244 av_dump_format(fmt_ctx,video_idx,src,0); ////video_idx是流的索引值,,0表示輸入/輸出流,由於是輸入文件,所以為0 245 246 //打開目標文件 247 FILE* dst_fd = fopen(dst,"wb"); 248 if(!dst_fd){ 249 av_log(NULL,AV_LOG_ERROR,"Can`t open destination file(%s)\n",dst); 250 avformat_close_input(&fmt_ctx); //釋放前面的空間 251 return -1; 252 } 253 254 //開始從流中讀取數據包 255 //初始化包結構 256 AVPacket pkt; 257 av_init_packet(&pkt); 258 pkt.data = NULL; 259 pkt.size = 0; 260 261 while(av_read_frame(fmt_ctx,&pkt)>=0){ //循環獲取下一個包 262 if(pkt.stream_index == video_idx){ //是我們想要的數據包 263 h264_mp4toannexb(fmt_ctx,&pkt,dst_fd); //開始進行包寫入,先將AVCC格式數據轉為AnnexB格式,然后寫入目標文件中去 264 } 265 //對每一個獲取的包進行減引用 266 av_packet_unref(&pkt); 267 } 268 269 //開始進行空間釋放 270 avformat_close_input(&fmt_ctx); 271 if(dst_fd){ 272 fclose(dst_fd); 273 } 274 275 return 0; 276 }
(三)程序測試
gcc ffmpeg_av.c -o fav -I /usr/local/ffmpeg/include/ -L /usr/local/ffmpeg/lib/ -lavutil -lavformat -lavcodec
./fav gfxm.mp4 out.h264
八:多媒體格式轉換---將MP4轉成FLV格式(數據與參數不變)
FFmpeg hevc codec_tag兼容問題:https://juejin.cn/post/6854573210579501070
(一)基礎函數了解
avformat_alloc_output_context2():在基於FFmpeg的音視頻編碼器程序中,該函數通常是第一個調用的函數(除了組件注冊函數av_register_all())。avformat_alloc_output_context2()函數可以初始化一個用於輸出的AVFormatContext結構體。
AVFormatContext :
unsigned int nb_streams; 記錄stream通道數目。
AVStream **streams; 存儲stream通道。
avformat_new_stream() 在 AVFormatContext 中創建 Stream 通道。之后,我們就可以自行設置 AVStream 的一些參數信息。
AVStream 即是流通道。例如我們將 H264 和 AAC 碼流存儲為MP4文件的時候,就需要在 MP4文件中增加兩個流通道,一個存儲Video:H264,一個存儲Audio:AAC。(假設H264和AAC只包含單個流通道)。 AVStream包含很多參數,用於記錄通道信息,其中最重要的是 : AVCodecParameters * codecpar :用於記錄編碼后的流信息,即通道中存儲的流的編碼信息。 AVRational time_base :AVStream通道的時間基,時間基是個相當重要的概念。 需要注意的是:現在的 ffmpeg 3.1.4版本已經使用AVCodecParameters * codecpar替換了原先的CodecContext* codec !
AVStream :
int index; 在AVFormatContext 中所處的通道索引
avcodec_parameters_copy() 在new stream之后,還需要把相應的參數拷貝過去。比如SPS、PPS中的參數
avformat_write_header() 生成多媒體文件頭 av_write_frame()/av_interleaved_write_frame() 后者用得多,用來寫入數據 av_write_trailer() 寫多媒體文件尾部(有的文件是包含尾部信息的)
(二)代碼實現
補充:時間基的轉換FFmpeg學習(四)視頻基礎
1、打開輸入文件; 2、創建並打開一個空文件存儲 flv 格式音視頻數據; 3、遍歷輸入文件的每一路流,每個輸入流對應創建一個輸出流,並將輸入流中的編解碼參數直接拷貝到輸出流中; 4、寫入新的多媒體文件的頭; 5、在循環遍歷輸入文件的每一幀,對每一個packet進行時間基的轉換; 6、寫入新的多媒體文件; 7、給新的多媒體文件寫入文件尾; 8、釋放相關資源。

#include <libavutil/log.h> #include <libavformat/avformat.h> #include <libavutil/timestamp.h> static void log_packet(const AVFormatContext *fmt_ctx, const AVPacket *pkt, const char *tag) { AVRational *time_base = &fmt_ctx->streams[pkt->stream_index]->time_base; printf("%s: pts:%s pts_time:%s dts:%s dts_time:%s duration:%s duration_time:%s stream_index:%d\n", tag, av_ts2str(pkt->pts), av_ts2timestr(pkt->pts, time_base), av_ts2str(pkt->dts), av_ts2timestr(pkt->dts, time_base), av_ts2str(pkt->duration), av_ts2timestr(pkt->duration, time_base), pkt->stream_index); } int main(int argc,char* argv[]) { AVOutputFormat* ofmt = NULL; //輸出格式 AVFormatContext* ifmt_ctx = NULL,*ofmt_ctx=NULL; //輸入、輸出上下文 AVPacket pkt; //數據包 const char* in_filename,*out_filename; int ret,i; int stream_idx = 0; int* stream_mapping = NULL; //數組:用來存放各個流通道的新索引值(對於不要的流,設置-1,對於需要的流從0開始遞增 int stream_mapping_size = 0; //輸入文件中流的總數量 av_log_set_level(AV_LOG_INFO); if(argc < 3){ av_log(NULL,AV_LOG_ERROR,"The number of parameters must be greater than 3!\n"); return -1; } av_register_all(); //設置文件路徑 in_filename = argv[1]; out_filename = argv[2]; //打開輸入多媒體文件,獲取上下文格式 ret = avformat_open_input(&ifmt_ctx,in_filename,NULL,NULL);//第三個參數強制指定AVFormatContext中AVInputFormat,一般設置為NULL,自動檢測。第四個為附加選項 if(ret<0){ av_log(NULL,AV_LOG_ERROR,"Can`t open input file %s \n",in_filename); goto fail; } //檢索輸入文件的流信息 ret = avformat_find_stream_info(ifmt_ctx,NULL); if(ret<0){ av_log(NULL,AV_LOG_ERROR,"Fail to retrieve input stream information!\n"); goto fail; } //打印關於輸入或輸出格式的詳細信息,例如持續時間,比特率,流,容器,程序,元數據,邊數據,編解碼器和時基。 av_dump_format(ifmt_ctx,0,in_filename,0); //第一個0表示流的索引值,第二個0表示是輸入文件 //為輸出上下文環境分配空間 avformat_alloc_output_context2(&ofmt_ctx,NULL,NULL,out_filename); //第二個參數:指定AVFormatContext中的AVOutputFormat,用於確定輸出格式。如果指定為NULL,可以設定后兩個參數(format_name或者filename)由FFmpeg猜測輸出格式。第三個參數為文件格式比如.flv,也可以通過第四個參數獲取 if(!ofmt_ctx){ av_log(NULL,AV_LOG_ERROR,"Can`t create output context!\n"); ret = AVERROR_UNKNOWN; goto fail; } //記錄輸入文件的stream通道數目 stream_mapping_size = ifmt_ctx->nb_streams; //為數組分配空間,sizeof(*stream_mapping)是分配了一個int空間,為stream_mapping分配了stream_mapping_size個int空間 stream_mapping = av_mallocz_array(stream_mapping_size,sizeof(*stream_mapping)); if(!stream_mapping){ ret = AVERROR(ENOMEM); //內存不足 goto fail; } //輸出文件格式 ofmt = ofmt_ctx->oformat; //遍歷輸入文件中的每一路流,對於每一路流都要創建一個新的流進行輸出 for(i=0;i<stream_mapping_size;i++){ AVStream* out_stream = NULL; //輸出流 AVStream* in_stream = ifmt_ctx->streams[i]; //輸入流獲取 AVCodecParameters* in_codecpar = in_stream->codecpar; //獲取輸入流的編解碼參數 //只保留音頻、視頻、字母流;對於其他流丟棄(實際上是設置對應的數組值為-1) if(in_codecpar->codec_type != AVMEDIA_TYPE_AUDIO && in_codecpar->codec_type != AVMEDIA_TYPE_VIDEO && in_codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE){ stream_mapping[i] = -1; continue; } //對於輸出流的index重新編號,從0開始,寫入stream_mapping數組對應空間中去 stream_mapping[i] = stream_idx++; //重點:為輸出格式上下文,創建一個對應的輸出流 out_stream = avformat_new_stream(ofmt_ctx,NULL); //第二個參數為對應的視頻所需要的編碼方式,為NULL則自動推導 if(!out_stream){ av_log(NULL,AV_LOG_ERROR,"Failed to allocate output stream\n"); ret = AVERROR_UNKNOWN; goto fail; } //直接將輸入流的編解碼參數拷貝到輸出流中 ret = avcodec_parameters_copy(out_stream->codecpar,in_codecpar); if(ret<0){ av_log(NULL,AV_LOG_ERROR,"Failed to copy codec parameters\n"); goto fail; } //詳見:https://juejin.cn/post/6854573210579501070 //avformat_write_header寫入封裝容器的頭信息時,會檢查codec_tag:若AVStream->codecpar->codec_tag有值,則會校驗AVStream->codecpar->codec_tag是否在封裝格式(比如MAP4)支持的codec_tag列表中,若不在,就會打印錯誤信息; //若AVStream->codecpar->codec_tag為0,則會根據AVCodecID從封裝格式的codec_tag列表中,找一個匹配的codec_tag。 out_stream->codecpar->codec_tag = 0; } //打印要輸出多媒體文件的詳細信息 av_dump_format(ofmt_ctx,0,out_filename,1); //1表示輸出文件 if(!(ofmt->flags&AVFMT_NOFILE)){ //查看文件格式狀態,如果文件不存在(未打開),則開啟文件 ret = avio_open(&ofmt_ctx->pb,out_filename,AVIO_FLAG_WRITE); //打開文件,可寫 if(ret<0){ av_log(NULL,AV_LOG_ERROR,"Can`t open output file %s!\n",out_filename); goto fail; } } //開始寫入新的多媒體文件頭部 ret = avformat_write_header(ofmt_ctx,NULL); //NULL為附加選項 if(ret<0){ av_log(NULL,AV_LOG_ERROR,"Can`t write format header into file: %s\n",out_filename); goto fail; } //循環寫入多媒體數據 while(1){ AVStream* in_stream,* out_stream; //獲取輸入輸出流 //循環讀取每一幀 ret = av_read_frame(ifmt_ctx,&pkt); if(ret<0){ //讀取完成,退出喜歡 break; } //獲取輸入流在stream_mapping中的數組值,看是否保留 in_stream = ifmt_ctx->streams[pkt.stream_index]; //先獲取所屬的流的信息 if(pkt.stream_index>=stream_mapping_size||stream_mapping[pkt.stream_index]<0){ //判斷是否是我們想要的音頻、視頻、字幕流,不是的話就跳過 av_packet_unref(&pkt); continue; } //需要對流進行重新編號(因為原來輸入流部分被跳過),輸出流編號應該從0開始遞增;索引就是我們上面保存的數組值 pkt.stream_index = stream_mapping[pkt.stream_index]; //按照輸出流的編號對pakcet進行重新編號 //根據上面的索引,獲取ofmt_cxt輸出格式上下文對應的輸出流,進行處理 out_stream = ofmt_ctx->streams[pkt.stream_index]; //開始對pakcet進行時間基的轉換,因為音視頻的采用率不同,所以不進行轉換,會導致時間不同步。最終使得音頻對應音頻刻度,視頻對應視頻刻度 //PTS(Presentation Time Stamp, 顯示時間戳),是渲染用的時間戳,播放器會根據這個時間戳進行渲染播放 //DTS(Decoding Time Stamp, 解碼時間戳),解碼時間戳,在視頻packet進行解碼成frame的時候會使用到 pkt.pts = av_rescale_q_rnd(pkt.pts,in_stream->time_base,out_stream->time_base,AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX); pkt.dts = av_rescale_q_rnd(pkt.dts,in_stream->time_base,out_stream->time_base,AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX); pkt.duration = av_rescale_q(pkt.duration,in_stream->time_base,out_stream->time_base); pkt.pos = -1; log_packet(ofmt_ctx,&pkt,"out"); //將處理好的packet寫入輸出文件中 ret = av_interleaved_write_frame(ofmt_ctx,&pkt); if(ret<0){ av_log(NULL,AV_LOG_ERROR,"Error muxing packet\n"); break; } av_packet_unref(&pkt); } av_write_trailer(ofmt_ctx); //寫入文件尾部 fail: //關閉輸入文件格式上下文 avformat_close_input(&ifmt_ctx); //關閉輸出文件 if(ofmt_ctx&&!(ofmt_ctx->flags&AVFMT_NOFILE)) avio_closep(&ofmt_ctx->pb); avformat_free_context(ofmt_ctx); //關閉輸出格式上下文 av_freep(&stream_mapping); //釋放數組空間 if(ret<0&&ret!=AVERROR_EOF){ //異常退出 av_log(NULL,AV_LOG_ERROR,"Error occurred: %s\n",av_err2str(ret)); return 1; } return 0; }
(三)程序測試
gcc ffmpeg_flv.c -o fflv -I /usr/local/ffmpeg/include/ -L /usr/local/ffmpeg/lib/ -lavutil -lavformat -lavcodec
九:音視頻裁剪
(一)基礎函數了解
FFmpeg提供了一個seek函數,原型如下:
int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp, int flags);
參數說明:
s:操作上下文;
stream_index:基本流索引,表示當前的seek是針對哪個基本流,比如視頻或者音頻等等。
timestamp:要seek的時間點,以time_base或者AV_TIME_BASE為單位。
Flags:seek標志,可以設置為按字節,在按時間seek時取該點之前還是之后的關鍵幀,以及不按關鍵幀seek等,詳細請參考FFmpeg的avformat.h說明。基於FFmpeg的所有track mode幾乎都是用這個函數來直接或間接實現的。
(二)代碼實現

#include <libavutil/log.h> #include <libavformat/avformat.h> #include <libavutil/timestamp.h> int cut_video(char* in_filename,char* out_filename,int starttime,int endtime){ AVOutputFormat* ofmt = NULL; //輸出格式 AVFormatContext* ifmt_ctx = NULL,*ofmt_ctx=NULL; //輸入、輸出上下文 AVPacket pkt; //數據包 int ret,i; int stream_idx = 0; int* stream_mapping = NULL; //數組:用來存放各個流通道的新索引值(對於不要的流,設置-1,對於需要的流從0開始遞增 int stream_mapping_size = 0; //輸入文件中流的總數量 //打開輸入多媒體文件,獲取上下文格式 ret = avformat_open_input(&ifmt_ctx,in_filename,NULL,NULL);//第三個參數強制指定AVFormatContext中AVInputFormat,一般設置為NULL,自動檢測。第四個為附加選項 if(ret<0){ av_log(NULL,AV_LOG_ERROR,"Can`t open input file %s \n",in_filename); goto fail; } //檢索輸入文件的流信息 ret = avformat_find_stream_info(ifmt_ctx,NULL); if(ret<0){ av_log(NULL,AV_LOG_ERROR,"Fail to retrieve input stream information!\n"); goto fail; } //打印關於輸入或輸出格式的詳細信息,例如持續時間,比特率,流,容器,程序,元數據,邊數據,編解碼器和時基。 av_dump_format(ifmt_ctx,0,in_filename,0); //第一個0表示流的索引值,第二個0表示是輸入文件 //為輸出上下文環境分配空間 avformat_alloc_output_context2(&ofmt_ctx,NULL,NULL,out_filename); //第二個參數:指定AVFormatContext中的AVOutputFormat,用於確定輸出格式。如果指定為NULL,可以設定后兩個參數(format_name或者filename)由FFmpeg猜測輸出格式。第三個參數為文件格式比如.flv,也可以通過第四個參數獲取 if(!ofmt_ctx){ av_log(NULL,AV_LOG_ERROR,"Can`t create output context!\n"); ret = AVERROR_UNKNOWN; goto fail; } //記錄輸入文件的stream通道數目 stream_mapping_size = ifmt_ctx->nb_streams; //為數組分配空間,sizeof(*stream_mapping)是分配了一個int空間,為stream_mapping分配了stream_mapping_size個int空間 stream_mapping = av_mallocz_array(stream_mapping_size,sizeof(*stream_mapping)); if(!stream_mapping){ ret = AVERROR(ENOMEM); //內存不足 goto fail; } //輸出文件格式 ofmt = ofmt_ctx->oformat; //遍歷輸入文件中的每一路流,對於每一路流都要創建一個新的流進行輸出 for(i=0;i<stream_mapping_size;i++){ AVStream* out_stream = NULL; //輸出流 AVStream* in_stream = ifmt_ctx->streams[i]; //輸入流獲取 AVCodecParameters* in_codecpar = in_stream->codecpar; //獲取輸入流的編解碼參數 //只保留音頻、視頻、字母流;對於其他流丟棄(實際上是設置對應的數組值為-1) if(in_codecpar->codec_type != AVMEDIA_TYPE_AUDIO && in_codecpar->codec_type != AVMEDIA_TYPE_VIDEO && in_codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE){ stream_mapping[i] = -1; continue; } //對於輸出流的index重新編號,從0開始,寫入stream_mapping數組對應空間中去 stream_mapping[i] = stream_idx++; //重點:為輸出格式上下文,創建一個對應的輸出流 out_stream = avformat_new_stream(ofmt_ctx,NULL); //第二個參數為對應的視頻所需要的編碼方式,為NULL則自動推導 if(!out_stream){ av_log(NULL,AV_LOG_ERROR,"Failed to allocate output stream\n"); ret = AVERROR_UNKNOWN; goto fail; } //直接將輸入流的編解碼參數拷貝到輸出流中 ret = avcodec_parameters_copy(out_stream->codecpar,in_codecpar); if(ret<0){ av_log(NULL,AV_LOG_ERROR,"Failed to copy codec parameters\n"); goto fail; } //詳見:https://juejin.cn/post/6854573210579501070 //avformat_write_header寫入封裝容器的頭信息時,會檢查codec_tag:若AVStream->codecpar->codec_tag有值,則會校驗AVStream->codecpar->codec_tag是否在封裝格式(比如MAP4)支持的codec_tag列表中,若不在,就會打印錯誤信息; //若AVStream->codecpar->codec_tag為0,則會根據AVCodecID從封裝格式的codec_tag列表中,找一個匹配的codec_tag。 out_stream->codecpar->codec_tag = 0; } //打印要輸出多媒體文件的詳細信息 av_dump_format(ofmt_ctx,0,out_filename,1); //1表示輸出文件 if(!(ofmt->flags&AVFMT_NOFILE)){ //查看文件格式狀態,如果文件不存在(未打開),則開啟文件 ret = avio_open(&ofmt_ctx->pb,out_filename,AVIO_FLAG_WRITE); //打開文件,可寫 if(ret<0){ av_log(NULL,AV_LOG_ERROR,"Can`t open output file %s!\n",out_filename); goto fail; } } //開始寫入新的多媒體文件頭部 ret = avformat_write_header(ofmt_ctx,NULL); //NULL為附加選項 if(ret<0){ av_log(NULL,AV_LOG_ERROR,"Can`t write format header into file: %s\n",out_filename); goto fail; } //--------------seek定位--------- ret = av_seek_frame(ifmt_ctx,-1,starttime*AV_TIME_BASE,AVSEEK_FLAG_ANY); if(ret<0){ av_log(NULL,AV_LOG_ERROR,"Can`t seek input file: %s\n",in_filename); goto fail; } //循環寫入多媒體數據 while(1){ AVStream* in_stream,* out_stream; //獲取輸入輸出流 //循環讀取每一幀 ret = av_read_frame(ifmt_ctx,&pkt); if(ret<0){ //讀取完成,退出喜歡 break; } //獲取輸入流在stream_mapping中的數組值,看是否保留 in_stream = ifmt_ctx->streams[pkt.stream_index]; //先獲取所屬的流的信息 if(pkt.stream_index>=stream_mapping_size||stream_mapping[pkt.stream_index]<0){ //判斷是否是我們想要的音頻、視頻、字幕流,不是的話就跳過 av_packet_unref(&pkt); continue; } //---------判斷是否到結束時間---------- if(av_q2d(in_stream->time_base)*pkt.pts>endtime){ //av_q2d獲取該流的時間基 av_free_packet(&pkt); break; } //需要對流進行重新編號(因為原來輸入流部分被跳過),輸出流編號應該從0開始遞增;索引就是我們上面保存的數組值 pkt.stream_index = stream_mapping[pkt.stream_index]; //按照輸出流的編號對pakcet進行重新編號 //根據上面的索引,獲取ofmt_cxt輸出格式上下文對應的輸出流,進行處理 out_stream = ofmt_ctx->streams[pkt.stream_index]; //開始對pakcet進行時間基的轉換,因為音視頻的采用率不同,所以不進行轉換,會導致時間不同步。最終使得音頻對應音頻刻度,視頻對應視頻刻度 //PTS(Presentation Time Stamp, 顯示時間戳),是渲染用的時間戳,播放器會根據這個時間戳進行渲染播放 //DTS(Decoding Time Stamp, 解碼時間戳),解碼時間戳,在視頻packet進行解碼成frame的時候會使用到 pkt.pts = av_rescale_q_rnd(pkt.pts,in_stream->time_base,out_stream->time_base,AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX); pkt.dts = av_rescale_q_rnd(pkt.dts,in_stream->time_base,out_stream->time_base,AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX); pkt.duration = av_rescale_q(pkt.duration,in_stream->time_base,out_stream->time_base); pkt.pos = -1; //將處理好的packet寫入輸出文件中 ret = av_interleaved_write_frame(ofmt_ctx,&pkt); if(ret<0){ av_log(NULL,AV_LOG_ERROR,"Error muxing packet\n"); break; } av_packet_unref(&pkt); } av_write_trailer(ofmt_ctx); //寫入文件尾部 fail: //關閉輸入文件格式上下文 avformat_close_input(&ifmt_ctx); //關閉輸出文件 if(ofmt_ctx&&!(ofmt_ctx->flags&AVFMT_NOFILE)) avio_closep(&ofmt_ctx->pb); avformat_free_context(ofmt_ctx); //關閉輸出格式上下文 av_freep(&stream_mapping); //釋放數組空間 if(ret<0&&ret!=AVERROR_EOF){ //異常退出 av_log(NULL,AV_LOG_ERROR,"Error occurred: %s\n",av_err2str(ret)); return 1; } return 0; } int main(int argc,char* argv[]) { av_log_set_level(AV_LOG_INFO); if(argc < 5){ av_log(NULL,AV_LOG_ERROR,"The number of parameters must be greater than 5!\n"); return -1; } av_register_all(); //設置文件路徑 int starttime = atoi(argv[3]); int endtime = atoi(argv[4]); cut_video(argv[1],argv[2],starttime,endtime); return 0; }
(三)程序測試
gcc ffmpeg_seek.c -o fs -I /usr/local/ffmpeg/include/ -L /usr/local/ffmpeg/lib/ -lavutil -lavformat -lavcodec
./fs gfxm.mp4 gfxm_2.mp4 10 20
ffplay gfxm_2.mp4