FFmpeg API的簡單實踐應用


0. 前言

利用 FFmpeg 編譯鏈接生成的可執行程序本身可以實現很多特定的功能,但如果我們有自己的個性化需求,想要在自己開發的項目中使用 FFmpeg 的一些功能,就需要理解並應用其已經實現好的API,以寫代碼的方式調用這些API來完成對媒體文件的操作。

既然是調用 FFmpeg 中實現的API,就是將其作為我們的庫來使用,首先需要將 FFmpeg 安裝到指定路徑。具體安裝步驟可以參考我之前的博客或者直接參考官方的編譯指南:

1. CMake 編譯文件配置

主要配置 FFmpeg 安裝路徑,包括頭文件路徑和鏈接庫路徑。其余的就是非常簡單的項目編譯配置,如編譯方式、項目名稱等。

CMAKE_MINIMUM_REQUIRED(VERSION 3.0)

PROJECT(MYPLAYER_TEST)

SET(CMAKE_BUILD_TYPE RELEASE)
SET(CMAKE_CXX_STANDARD 11)
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wall")
SET(FFmpeg_DIR "/Users/phillee/ffmpeg")

INCLUDE_DIRECTORIES(${FFmpeg_DIR}/ffmpeg-bin/include)
LINK_DIRECTORIES(${FFmpeg_DIR}/ffmpeg-bin/lib)

# Messages to show for the user
MESSAGE(STATUS "** Customized settings are shown as below **")
MESSAGE(STATUS "\tCMAKE BUILD TYPE: ${CMAKE_BUILD_TYPE}")
MESSAGE(STATUS "\tFFmpeg include directory: ${FFmpeg_DIR}/ffmpeg-bin/include")
MESSAGE(STATUS "\tFFmpeg library directory: ${FFmpeg_DIR}/ffmpeg-bin/lib")

ADD_EXECUTABLE(myplayer_test main.cc)

TARGET_LINK_LIBRARIES(myplayer_test
    avcodec
    avformat
    avutil
    postproc
    swresample
    swscale
    )

2. 包含頭文件

由於 FFmpeg 是用C99標准寫成的,有些功能在 C++ 中可能無法直接編譯或者使用。

不過多數情況下,在 C++ 中包含 FFmpeg 頭文件還是相當直接的。

首先,顯示聲明頭文件為 C 格式文件

extern "C" {
#include <libavutil/imgutils.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
}

另外,如果編譯時出現類似 UINT64_C was not declared in this scope 的報錯,可以嘗試在 CXXFLAGS 標志位中添加 -D__STDC_CONSTANT_MACROS

參考解決方案

3. 簡單調用API實踐

第一部分是按照步驟2中的格式包含頭文件,這里添加了編譯時的平台限制。

#include <cstdio>
#include <inttypes.h>

#ifdef __APPLE__
extern "C" {
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libswresample/swresample.h"
#include "libavutil/pixdesc.h"
}
#endif

第二部分是關於從媒體流的原始數據獲取數據幀和將數據幀保存到 PGM 格式文件的兩個封裝函數的聲明,其實現放在了 main 函數后面。

static int decode_packet(AVPacket *pPacket, AVCodecContext *pCodecContext, AVFrame *pFrame);
void write_to_pgm(const char* file_path, AVFrame* decoded_frame);
static int decode_packet(AVPacket *pPacket, AVCodecContext *pCodecContext, AVFrame *pFrame)
{
    int ret = avcodec_send_packet(pCodecContext, pPacket);
    if (ret < 0)
    {
        printf("Error while sending a packet to the decoder: %s", av_err2str(ret));
        return ret;
    }

    while (ret >= 0) {
        ret = avcodec_receive_frame(pCodecContext, pFrame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            break;
        } else if (ret < 0) {
            printf("Error while receiving a frame from the decoder: %s", av_err2str(ret));
            return ret;
        } else if (ret >= 0) {
            printf("Frame %d: type=%c, size=%d bytes, format=%d, pts=%d key_frame %d [DTS %d]\n", pCodecContext->frame_number, av_get_picture_type_char(pFrame->pict_type), pFrame->pkt_size, pFrame->format, pFrame->pts, pFrame->key_frame, pFrame->coded_picture_number);
            char file_to_write[100];
            sprintf(file_to_write, "/Users/gcxyb/phillee/misc_codes/ffmpeg_based_test/build/frame-%02d.pgm", pCodecContext->frame_number);
            if (pFrame->format==AV_PIX_FMT_YUV420P) {
                write_to_pgm(file_to_write, pFrame);
            }
        }
    }
}

void write_to_pgm(const char* file_path, AVFrame* decoded_frame) {
    FILE *fout = fopen(file_path, "w");
    fprintf(fout, "P5\n%d %d\n%d\n", decoded_frame->width, decoded_frame->height, 255);
    for (int line_id=0; line_id<decoded_frame->height; ++line_id) {
        int ret = fwrite(decoded_frame->data[0]+line_id*decoded_frame->linesize[0], 1, decoded_frame->width, fout);
        if (ret < 0)
            exit(1);
    }
    fclose(fout);
}

avcodec_send_packet 函數將原始的 AVPacket 數據包送到解碼器,然后調用 avcodec_receive_frame 函數從解碼器解碼出 AVFrame 的幀數據信息。得到幀數據之后可以進行其他操作。

PGM 的格式非常簡單,只有 ASCII 碼形式的幾個頭部標示信息和二進制的數據,所以直接寫入就可以了。

第三部分是主要流程和相應 API 的調用。

(a) 首先是 FFmpeg 的注冊協議

需要用到網絡的操作時應該將網絡協議部分注冊到 FFmpeg 框架,以便后續再去查找對應的格式。其實這里的 av_register_all 在新近的 FFmpeg 版本中被標注為 attribute_deprecated ,在后面的測試中我把該語句注釋掉好像也能正常工作,如果是面向最新版本的應用應該是可以不用加了。

    avformat_network_init();
    av_register_all();

網絡協議部分的注冊是可選項,但官方建議是最好加上,防止中間出現隱式設置的開銷(翻譯不到位,建議看下面的原文理解)。

Do global initialization of network components. This is optional, but recommended, since it avoids the overhead of implicitly doing the setup for each session.

(b) 接着打開媒體流文件並讀取基本信息

    AVFormatContext *formatCtx = avformat_alloc_context();
    AVCodec *pCodec = NULL;
    AVCodecParameters *pCodecParam = NULL;
    int video_stream_index = -1;

    if (avformat_open_input(&formatCtx, argv[1], NULL, NULL) < 0) {
        printf("Error: cannot open the input file!\n");
        exit(1);
    }
    if (avformat_find_stream_info(formatCtx, NULL) < 0) {
        printf("Error: fail to find stream info!\n");
        exit(1);
    }

函數 avformat_open_input 會根據提供的文件路徑判斷文件格式,然后決定使用什么樣的解封裝器。
avformat_find_stream_info 方法的作用就是把所有 Stream 流的 MetaData 信息填充好。方法內部會先查找對應的解碼器,打開解碼器,利用 Demuxer 中的 read_packet 函數讀取一段數據進行解碼,解碼的信息越多分析出的流信息就越准確。
這一段代碼之后的 for 循環主要是將分析的結果輸出到屏幕。

(c) 解析幀數據信息

    AVCodecContext *pCodecContext = avcodec_alloc_context3(pCodec);
    if(avcodec_parameters_to_context(pCodecContext, pCodecParam) < 0) {
        printf("Failed to fill the codec context!\n");
        exit(1);
    }
    if(avcodec_open2(pCodecContext, pCodec, NULL) < 0) {
        printf("Failed to initialize the avcodec context!\n");
        exit(1);
    }
    AVFrame *pFrame = av_frame_alloc();
    AVPacket *pPacket = av_packet_alloc();
    int packet_cnt = 10;
    while(av_read_frame(formatCtx, pPacket)>=0) {
        if (pPacket->stream_index == video_stream_index) {
            printf("AVPacket pts: %" PRId64 "\n", pPacket->pts);
            if (decode_packet(pPacket, pCodecContext, pFrame)<0)
                break;
            if (--packet_cnt<=0)
                break;
        }
        av_packet_unref(pPacket);
    }

函數 avcodec_open2 用來打開編解碼器,無論是編碼過程還是解碼過程都會用到。輸入參數有三個,第一個是 AVCodecContext,解碼過程由 FFmpeg 引擎填充。第二個參數是解碼器,第三個參數一般會傳遞 NULL
使用 av_read_frame 讀取出來的數據是 AVPacket,在早期版本中開放給開發者的是 av_read_packet 函數,但需要開發者自己來處理 AVPacket 中的數據不能被解碼器完全處理完的情況,即需要把未處理完的壓縮數據緩存起來的問題,所以現在提供了該函數。對於視頻流,一個AVPacket只包含一個AVFrame,最終將得到一個 AVPacket 的結構體。

main.cc 全部代碼如下

#include <cstdio> // fopen, fclose, fwrite
#include <inttypes.h>

#ifdef __APPLE__
extern "C" {
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libswresample/swresample.h"
#include "libavutil/pixdesc.h"
}
#endif

static int decode_packet(AVPacket *pPacket, AVCodecContext *pCodecContext, AVFrame *pFrame);
void write_to_pgm(const char* file_path, AVFrame* decoded_frame);

int main(const int argc, char* argv[])
{
    avformat_network_init();
    av_register_all();

    if (argc < 2) {
        printf("Please set the file path to open...\n");
        exit(1);
    }
    printf("Open file %s...\n", argv[1]);

    AVFormatContext *formatCtx = avformat_alloc_context();
    AVCodec *pCodec = NULL;
    AVCodecParameters *pCodecParam = NULL;
    int video_stream_index = -1;

    if (avformat_open_input(&formatCtx, argv[1], NULL, NULL) < 0) {
        printf("Error: cannot open the input file!\n");
        exit(1);
    }
    if (avformat_find_stream_info(formatCtx, NULL) < 0) {
        printf("Error: fail to find stream info!\n");
        exit(1);
    }

    printf("There are %d streams in the given file\n", formatCtx->nb_streams);
    for (int i = 0; i < formatCtx->nb_streams; ++i) {
        AVStream *stream = formatCtx->streams[i];
        AVCodecParameters *pLocalCodecParam = stream->codecpar;
        AVCodec *pLocalCodec = avcodec_find_decoder(pLocalCodecParam->codec_id);
        if (NULL==pLocalCodec) {
            printf("Error: unsupported codec found!\n");
            continue;
        }
        if (pLocalCodecParam->codec_type == AVMEDIA_TYPE_VIDEO) {
            if (-1==video_stream_index) {
                video_stream_index = i;
                pCodec = pLocalCodec;
                pCodecParam = pLocalCodecParam;
            }
            printf("Info of video stream:\n");
            printf("\tCodec %s ID %d bit_rate %lld kb/s\n", pLocalCodec->name, pLocalCodec->id, pLocalCodecParam->bit_rate/1024);
            printf("\tAVStream->r_frame_rate: %d/%d\n", stream->r_frame_rate.num, stream->r_frame_rate.den);
            printf("\tResolution=%dx%d\n", pLocalCodecParam->width, pLocalCodecParam->height);
        } else if (pLocalCodecParam->codec_type == AVMEDIA_TYPE_AUDIO) {
            printf("Info of audio stream:\n");
            printf("\tCodec %s ID %d bit_rate %lld kb/s\n", pLocalCodec->name, pLocalCodec->id, pLocalCodecParam->bit_rate/1024);
            printf("\tchannels=%d\n\tsample_rate=%d\n", pLocalCodecParam->channels, pLocalCodecParam->sample_rate);
        }
        printf("\tAVStream->time_base: %d/%d\n", stream->time_base.num, stream->time_base.den);
        printf("\tAVStream->start_time: %" PRId64 "\n", stream->start_time);
        printf("\tAVStream->duration: %" PRId64 "\n", stream->duration);
    }
    
    AVCodecContext *pCodecContext = avcodec_alloc_context3(pCodec);
    if(avcodec_parameters_to_context(pCodecContext, pCodecParam) < 0) {
        printf("Failed to fill the codec context!\n");
        exit(1);
    }
    if(avcodec_open2(pCodecContext, pCodec, NULL) < 0) {
        printf("Failed to initialize the avcodec context!\n");
        exit(1);
    }
    AVFrame *pFrame = av_frame_alloc();
    AVPacket *pPacket = av_packet_alloc();
    int packet_cnt = 10;
    while(av_read_frame(formatCtx, pPacket)>=0) {
        if (pPacket->stream_index == video_stream_index) {
            printf("AVPacket pts: %" PRId64 "\n", pPacket->pts);
            if (decode_packet(pPacket, pCodecContext, pFrame)<0)
                break;
            if (--packet_cnt<=0)
                break;
        }
        av_packet_unref(pPacket);
    }

    avformat_close_input(&formatCtx);
    av_packet_free(&pPacket);
    av_frame_free(&pFrame);
    avcodec_free_context(&pCodecContext);

    return 0;
}

static int decode_packet(AVPacket *pPacket, AVCodecContext *pCodecContext, AVFrame *pFrame)
{
    int ret = avcodec_send_packet(pCodecContext, pPacket);
    if (ret < 0)
    {
        printf("Error while sending a packet to the decoder: %s", av_err2str(ret));
        return ret;
    }

    while (ret >= 0) {
        ret = avcodec_receive_frame(pCodecContext, pFrame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            break;
        } else if (ret < 0) {
            printf("Error while receiving a frame from the decoder: %s", av_err2str(ret));
            return ret;
        } else if (ret >= 0) {
            printf("Frame %d: type=%c, size=%d bytes, format=%d, pts=%d key_frame %d [DTS %d]\n", pCodecContext->frame_number, av_get_picture_type_char(pFrame->pict_type), pFrame->pkt_size, pFrame->format, pFrame->pts, pFrame->key_frame, pFrame->coded_picture_number);
            char file_to_write[100];
            sprintf(file_to_write, "/Users/gcxyb/phillee/misc_codes/ffmpeg_based_test/build/frame-%02d.pgm", pCodecContext->frame_number);
            if (pFrame->format==AV_PIX_FMT_YUV420P) {
                write_to_pgm(file_to_write, pFrame);
            }
        }
    }
}

void write_to_pgm(const char* file_path, AVFrame* decoded_frame) {
    FILE *fout = fopen(file_path, "w");
    fprintf(fout, "P5\n%d %d\n%d\n", decoded_frame->width, decoded_frame->height, 255);
    for (int line_id=0; line_id<decoded_frame->height; ++line_id) {
        int ret = fwrite(decoded_frame->data[0]+line_id*decoded_frame->linesize[0], 1, decoded_frame->width, fout);
        if (ret < 0)
            exit(1);
    }
    fclose(fout);
}

文件夾結構

.
├── CMakeLists.txt
└── main.cc

編譯測試

$ mkdir build
$ cd build
$ cmake ..
$ make
$ ./myplayer_test /path/to/the/media/file

(全文完)


參考資料

[1] FFMPEG編譯問題記錄 https://www.cnblogs.com/phillee/p/13813156.html
[2] FFmpeg Compilation Guide https://trac.ffmpeg.org/wiki/CompilationGuide
[3] pgm http://netpbm.sourceforge.net/doc/pgm.html
[4] PGM example https://en.wikipedia.org/wiki/Netpbm#PGM_example

本文作者 :phillee
發表日期 :2021年4月1日
本文鏈接https://www.cnblogs.com/phillee/p/14605815.html
版權聲明 :自由轉載-非商用-非衍生-保持署名(創意共享3.0許可協議/CC BY-NC-SA 3.0)。轉載請注明出處!
限於本人水平,如果文章和代碼有表述不當之處,還請不吝賜教。

感謝您的支持

¥ 打賞

微信支付


免責聲明!

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



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