FFmpeg 接口使用 - 基礎和轉封裝


作為開發者,使用 FFmpeg 主要分兩部分:命令行工具和接口使用,本文講解如何在 macOS 上交叉編譯 FFmpeg,再將其集成到 Xcode 中,再初步介紹 FFmpeg 接口使用時會用到的常用結構,最后實際編寫音視頻文件轉封裝的代碼。

Editing Video

交叉編譯和集成

利用如下腳本交叉編譯 ffmpeg 2.3 + libfdk_aac 0.1.5 + x264-snapshot-20160814-2245-stable:

iOS 支持的架構配置信息如下,上面的交叉編譯就需要編譯支持的架構:

Xcode Architectures

Build Settings 里面的 Architectures 選項。Architectures 指的是該 App 支持的指令集,一般情況下,在 Xcode 中新建一個項目,其默認的 Architectures 選項值是 Standard architectures(armv7、arm64),表示該 App 僅支持 armv7 和 arm64 的指令集;Valid Architectures 選項指即將編譯的指令集,一般設置為 armv7、armv7s、arm64,表示一般會編譯這三個指令集;Build Active Architecture Only 選項表示是否只編譯當前適用的指令集,一般情況下在 Debug 的時候設置為 YES,以便可以更加快速、高效地調試程序,而在 Release 的情況下設置為 NO,以便 App 在各個機器上都能夠以最高效率運行,因為 Valid Architectures 選擇的對應指令集是 armv7、armv7s 和 arm64,在 Release 下會為各個指令集編譯對應的代碼,因此最后的 ipa 體積基本上翻了 3 倍。

把交叉編譯好的頭文件和庫文件拷貝到如下目錄:

DTCamera ThirdParty

頭文件搜索配置:

Xcode Header Search Paths

庫文件搜索配置:

Xcode Library Search Paths

交叉編譯 FFmpeg 挺麻煩的,有人已經整理了一個項目 mobile-ffmpeg 來解決這個問題,我還沒有使用過。

接口使用

官方文檔

代碼和資料

庫的介紹

下圖中,實線是強制依賴,虛線是選擇依賴。

FFmpeg Libraries Dependencies

  • libavformat 封裝模塊
  • libavcodec 編解碼模塊
  • libavfilter 濾鏡模塊
  • libswscale 視頻圖像縮放、顏色轉換模塊
  • libswresample 音頻采樣率轉換模塊
  • libavutil 多媒體編程工具模塊
  • libavdevice 多媒體設備輸入輸出模塊

常用結構

FFmpeg Common Structs

AVFormatContext 是 API 層直接接觸到的結構體,它會進行格式的封裝與解封裝,它的數據部分由底層提供,底層使用了 AVIOContext,這個 AVIOContext 實際上就是為普通的 I/O 增加了一層 Buffer 緩沖區,再往底層就是 URLContext,也就是到達了協議層,協議層的具體實現有很多,包括 rtmp、http、hls、file 等。

AVFormatContext 中的 AVInputFormat 對應於解封裝時的輸入容器格式,AVOutputFormat 對應於封裝時的輸出容器格式,同一時間,兩者只能有一個存在。

AVFormatContext 就是對容器或者說媒體文件層次的一個抽象,該文件中(或者說在這個容器里面)包含了多路流(音頻流、視頻流、字幕流等),對流的抽象就是 AVStream;在每一路流中都會描述這路流的編碼格式,對編解碼格式以及編解碼器的抽象就是 AVCodecContextAVCodec;對於編碼器或者解碼器的輸入輸出部分,也就是壓縮數據以及原始數據的抽象就是 AVPacketAVFrame

音視頻文件轉封裝

示例代碼

extern "C" 的解釋

extern "C" { #include "libavformat/avformat.h" #include "libavcodec/avcodec.h" #include "libswresample/swresample.h" #include "libavutil/avutil.h" }

作為一種面向對象的語言,C++ 支持函數的重載,而面向過程的 C 語言是不支持函數重載的。同一個函數在 C++ 中編譯后與其在 C 中編譯后,在符號表中的簽名是不同的,假如對於同一個函數:

void decode(float position, float duration)

在 C 語言中編譯出來的簽名是 _decoder,而在 C++ 語言中,一般編譯器的生成則類似於 _decode_float_float。雖然在編譯階段是沒有問題的,但是在鏈接階段,如果不加 extern "C" 關鍵字的話,那么將會鏈接 _decoder_float_float 這個方法簽名;而如果加了 extern "C" 關鍵字的話,那么尋找的方法簽名就是 _decoder。而 FFmpeg 就是 C 語言書寫的,編譯 FFmpeg 的時候所產生的方法簽名都是 C 語言類型的簽名,所以在 C 中引用 FFmpeg 必須要加 extern "C" 關鍵字。

關鍵步驟

第一步,API 注冊

首先在使用 FFmpeg 接口之前,需要進行 FFmpeg 使用接口的注冊操作:

av_register_all();

第二步,構建輸入 AVFormatContext

注冊之后,打開輸入文件並與 AVFormatContext 建立關聯:

AVFormatContext *ifmt_ctx = NULL; if ((ret = avformat_open_input(&ifmt_ctx, in_file.c_str(), 0, 0)) < 0) { std::cerr << "Could not open input file " << in_file.c_str() << std::endl; exit(1); }

第三步,查找流信息

建立關聯之后,與解封裝操作類似,可以通過接口 avformat_find_stream_info 獲得流信息:

if ((ret = avformat_find_stream_info(ifmt_ctx, 0)) < 0) { std::cerr << "Failed to retrieve input stream information" << std::endl; exit(1); }

第四步,構建輸出 AVFormatContext

輸入文件打開完成之后,可以打開輸出文件並與 AVFormatContext 建立關聯:

AVFormatContext *ofmt_ctx = NULL; avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, out_file.c_str()); if (!ofmt_ctx) { std::cerr << "Could not create output context " << out_file.c_str() << std::endl; exit(1); }

第五步,申請 AVStream 和 stream 信息復制

建立關聯之后,需要申請輸入的 stream 信息與輸岀的 stream 信息,輸入的 stream 信息可以從 ifmt_ctx 中獲得,但是存儲至 ofmt_ctx 中的 stream 信息需要申請獨立內存空間,輸出的 stream 信息建立完成之后,需要從輸入的 stream 中將信息復制到輸出的 stream 中,由於只是轉封裝,所以 stream 的信息不變,僅僅是改變了封裝格式:

for (int i = 0; i < ifmt_ctx->nb_streams; i++) { AVStream *in_stream = ifmt_ctx->streams[i]; AVStream *out_stream = avformat_new_stream(ofmt_ctx, in_stream->codec->codec); if (!out_stream) { std::cerr << "Failed allocating output stream" << std::endl; exit(1); } ret = avcodec_copy_context(out_stream->codec, in_stream->codec); if (ret < 0) { std::cerr << "Failed to copy context from input to output stream codec context" << std::endl; exit(1); } out_stream->codec->codec_tag = 0; if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER) out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER; out_stream->time_base = out_stream->codec->time_base; }

第六步,打開輸出 AVFormatContext 的緩沖區

if (!(ofmt_ctx->flags & AVFMT_NOFILE)) { ret = avio_open(&ofmt_ctx->pb, out_file.c_str(), AVIO_FLAG_WRITE); if (ret < 0) { std::cerr << "Could not open output file " << out_file.c_str() << std::endl; exit(1); } }

第七步,寫文件頭信息

輸出文件打開之后,接下來可以進行寫文件頭的操作:

ret = avformat_write_header(ofmt_ctx, NULL); if (ret < 0) { std::cerr << "Error occurred when opening output file" << std::endl; exit(1); }

第八步,數據包讀取和寫入

輸入與輸岀均已經打開,並與對應的 AVFormatContext 建立了關聯,接下來可以從輸入格式中讀取數據包,然后將數據包寫入至輸出文件中,當然,隨着輸入的封裝格式與輸出的封裝格式的差別化,時間戳也需要進行對應的計算改變:

AVPacket pkt; while (1) { AVStream *in_stream, *out_stream; ret = av_read_frame(ifmt_ctx, &pkt); if (ret < 0) { break; } in_stream = ifmt_ctx->streams[pkt.stream_index]; out_stream = ofmt_ctx->streams[pkt.stream_index]; pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF); pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF); pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base); pkt.pos = -1; ret = av_interleaved_write_frame(ofmt_ctx, &pkt); if (ret < 0) { std::cerr << "Error muxing packet" << std::endl; exit(1); } av_free_packet(&pkt); }

第九步,寫文件尾信息

解封裝讀取數據並將數據寫入新的封裝格式的操作已經完成,接下來即可進行寫文件尾至輸出格式的操作:

av_write_trailer(ofmt_ctx);

第十步,收尾

關閉輸入格式,釋放輸出格式:

avformat_close_input(&ifmt_ctx); avformat_free_context(ofmt_ctx);

第十一步,測試驗證

測試了 mp4 和 flv 之間的轉換,mp4 的視頻流要是 h.264 格式,如果是 hevc 格式就會失敗。


免責聲明!

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



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