FFmpeg編程(一)FFmpeg初級開發


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;
}
日志輸出編程01log.c

編譯.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;
}
ffmpeg_io.c

編譯文件:

 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;
}
ffmpeg_io.c

編譯方案推薦第一種,雖然還沒搞明白,但是兼容性看來好些,不容易出錯!!!

三:目錄操作

(一)重要結構體

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
struct URLContext
typedef struct AVIODirContext {
    struct URLContext *url_context;
} AVIODirContext;
struct 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;
}
avio_open_dir

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;
struct 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;
}
avio_read_dir

(二)目錄信息編程

#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;                                        
}
adts_header頭信息添加
#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;
struct 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;
struct 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 }
View Code

(三)程序測試

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;
}
View Code

(三)程序測試

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;
}
View Code

(三)程序測試

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

 


免責聲明!

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



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