0. 前言

利用 FFmpeg 编译链接生成的可执行程序本身可以实现很多特定的功能,但如果我们有自己的个性化需求,想要在自己开发的项目中使用 ffmpeg 的一些功能,就需要理解并应用其已经实现好的API,以写代码的方式调用这些API来完成对媒体文件的操作。

既然是调用 FFmpeg 中实现的API,就是将其作为我们的库来使用,首先需要将 FFmpeg 安装到指定路径。具体安装步骤可以参考我之前的博客 FFMPEG编译问题记录 或者参考官方的编译指南 FFmpeg Compilation Guide

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)。转载请注明出处!

限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。

.outter_box { text-align: center }
.button { background-color: rgba(83, 168, 73, 1); border: none; border-radius: 6px; color: rgba(255, 255, 255, 1); padding: 9px 24px; text-align: center; text-decoration: none; display: inline-block; font-size: 16px }
.img_box { border: none; color: rgba(255, 255, 255, 1); text-align: center; vertical-align: middle }
.demo { width: 208px; height: 260px; margin: 0 auto }
.demo img { -webkit-filter: drop-shadow(2px 2px 5px rgba(0,0,0,.5)); filter: drop-shadow(2px 2px 5px rgba(0,0,0,.5)) }

感谢您的支持

¥ 打赏

微信支付

FFmpeg API的简单实践应用的更多相关文章

  1. RESTful API 设计最佳实践

    背景 目前互联网上充斥着大量的关于RESTful API(为了方便,以后API和RESTful API 一个意思)如何设计的文章,然而却没有一个"万能"的设计标准:如何鉴权?API ...

  2. 学习FFmpeg API

    ffmpeg是编解码的利器,用了很久,以前看过dranger 的教程,非常精彩,受益颇多,是学习ffmpeg api很好的材料.可惜的是其针对的ffmpeg版本已经比较老了,而ffmpeg的更新又很快 ...

  3. ****RESTful API 设计最佳实践(APP后端API设计参考典范)

    http://blog.jobbole.com/41233/ 背景 目前互联网上充斥着大量的关于RESTful API(为方便,下文中“RESTful API ”简写为“API”)如何设计的文章,然而 ...

  4. RESTful API 设计最佳实践(转)

    摘要:目前互联网上充斥着大量的关于RESTful API(为了方便,以后API和RESTful API 一个意思)如何设计的文章,然而却没有一个”万能“的设计标准:如何鉴权?API格式如何?你的API ...

  5. RESTful API 设计最佳实践(转)

    背景 目前互联网上充斥着大量的关于RESTful API(为方便,下文中“RESTful API ”简写为“API”)如何设计的文章,然而却没有一个”万能“的设计标准:如何鉴权?API 格式如何?你的 ...

  6. Android 设计随便说说之简单实践(合理组合)

    上一篇(Android 设计随便说说之简单实践(模块划分))例举了应用商店设计来说明怎么做模块划分.模块划分主要依赖于第一是业务需求,具体是怎么样的业务.应用商店则包括两个业务,就是向用户展示appl ...

  7. FFmpeg资料来源简单分析:libswscale的sws_getContext()

    ===================================================== FFmpeg库函数的源代码的分析文章: [骨架] FFmpeg源码结构图 - 解码 FFmp ...

  8. 【转】学习FFmpeg API – 解码视频

    ffmpeg是编解码的利器,用了很久,以前看过dranger 的教程,非常精彩,受益颇多,是学习ffmpeg api很好的材料.可惜的是其针对的ffmpeg版本已经比较老了,而ffmpeg的更新又很快 ...

  9. [转] 阿里研究员谷朴:API 设计最佳实践的思考

    API是软件系统的核心,而软件系统的复杂度Complexity是大规模软件系统能否成功最重要的因素.但复杂度Complexity并非某一个单独的问题能完全败坏的,而是在系统设计尤其是API设计层面很多 ...

随机推荐

  1. javascript module system all in one

    javascript module system all in one AMD & CMD https://github.com/amdjs/amdjs-api/wiki/AMD http:/ ...

  2. React Hooks vs React Class vs React Function All In One

    React Hooks vs React Class vs React Function All In One React Component Types React Hooks Component ...

  3. Nestjs mongodb

    nestjs 文档 mongoose 文档 使用"@meanie/mongoose-to-json"转换查询后返回的json数据 将"_id"转为"i ...

  4. Renice INC:法国葡萄酒为什么独占世界鳌头?

    提起葡萄酒,许多人首先想到的就是法国.法国有着悠久的酿酒历史和精湛工艺,"82年的拉菲"几乎成了大众认识葡萄酒的代名词.市面上的进口葡萄酒琳琅满目,原产国众多,意大利.西班牙.美国 ...

  5. K8S部署Redis Cluster集群(三主三从模式) - 部署笔记

    一.Redis 介绍 Redis代表REmote DIctionary Server是一种开源的内存中数据存储,通常用作数据库,缓存或消息代理.它可以存储和操作高级数据类型,例如列表,地图,集合和排序 ...

  6. Mac mini M1使用简单体验(编程、游戏、深度学习)

    好久不见了各位! 前一阵子忍不住剁手买了M1芯片的mac mini,为了弥补自己的内疚感就卖了自己的旧的mbp2017款.数据也完全迁移到了新机器上,之前的工作也就由mbp2017彻底换成mac mi ...

  7. 09_MySQL数据库的索引机制

    CREATE TABLE t_message( id INT UNSIGNED PRIMARY KEY, content VARCHAR(200) NOT NULL, type ENUM(" ...

  8. 【翻译】Python PEP8编码规范(中文版)

    原文链接:http://legacy.python.org/dev/peps/pep-0008/ item detail PEP 8 Title Style Guide for Python Code ...

  9. 链表算法题二,还原题目,用debug调试搞懂每一道题

    文章简述 大家好,本篇是个人的第4篇文章. 承接第3篇文章<开启算法之路,还原题目,用debug调试搞懂每一道题>,本篇文章继续分享关于链表的算法题目. 本篇文章共有5道题目 一,反转链表 ...

  10. 由endl对printf和cout的思考

    [前言]二者的区别就不介绍了.二者使用方法: printf("%s",a); cout<<a<<endl; endl的作用是什么? 一.endl作用 众所周 ...