• 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 "	CMAKE BUILD TYPE: ${CMAKE_BUILD_TYPE}")
    MESSAGE(STATUS "	FFmpeg include directory: ${FFmpeg_DIR}/ffmpeg-bin/include")
    MESSAGE(STATUS "	FFmpeg 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]
    ", 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
    %d %d
    %d
    ", 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!
    ");
            exit(1);
        }
        if (avformat_find_stream_info(formatCtx, NULL) < 0) {
            printf("Error: fail to find stream info!
    ");
            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!
    ");
            exit(1);
        }
        if(avcodec_open2(pCodecContext, pCodec, NULL) < 0) {
            printf("Failed to initialize the avcodec context!
    ");
            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 "
    ", 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...
    ");
            exit(1);
        }
        printf("Open file %s...
    ", 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!
    ");
            exit(1);
        }
        if (avformat_find_stream_info(formatCtx, NULL) < 0) {
            printf("Error: fail to find stream info!
    ");
            exit(1);
        }
    
        printf("There are %d streams in the given file
    ", 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!
    ");
                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:
    ");
                printf("	Codec %s ID %d bit_rate %lld kb/s
    ", pLocalCodec->name, pLocalCodec->id, pLocalCodecParam->bit_rate/1024);
                printf("	AVStream->r_frame_rate: %d/%d
    ", stream->r_frame_rate.num, stream->r_frame_rate.den);
                printf("	Resolution=%dx%d
    ", pLocalCodecParam->width, pLocalCodecParam->height);
            } else if (pLocalCodecParam->codec_type == AVMEDIA_TYPE_AUDIO) {
                printf("Info of audio stream:
    ");
                printf("	Codec %s ID %d bit_rate %lld kb/s
    ", pLocalCodec->name, pLocalCodec->id, pLocalCodecParam->bit_rate/1024);
                printf("	channels=%d
    	sample_rate=%d
    ", pLocalCodecParam->channels, pLocalCodecParam->sample_rate);
            }
            printf("	AVStream->time_base: %d/%d
    ", stream->time_base.num, stream->time_base.den);
            printf("	AVStream->start_time: %" PRId64 "
    ", stream->start_time);
            printf("	AVStream->duration: %" PRId64 "
    ", stream->duration);
        }
        
        AVCodecContext *pCodecContext = avcodec_alloc_context3(pCodec);
        if(avcodec_parameters_to_context(pCodecContext, pCodecParam) < 0) {
            printf("Failed to fill the codec context!
    ");
            exit(1);
        }
        if(avcodec_open2(pCodecContext, pCodec, NULL) < 0) {
            printf("Failed to initialize the avcodec context!
    ");
            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 "
    ", 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]
    ", 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
    %d %d
    %d
    ", 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)。转载请注明出处!
    限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。

    感谢您的支持

    ¥ 打赏

    微信支付

  • 相关阅读:
    物料清单概述
    java开发webservice的1种方式
    java web service简单示例
    IOS证书过期
    Windows 2012 R2 安装net4.6.1
    sqlserver 性能调优脚本
    solidty--owner.sol
    ERC20-USDT
    EOS 公开节点及自有节点部署
    微信第三方平台授权流程
  • 原文地址:https://www.cnblogs.com/phillee/p/14605815.html
Copyright © 2020-2023  润新知