当前位置: 首页 > news >正文

如何给 FFmpeg 添加自定义 Codec 编码器

介绍

ffmpeg是一个很强大的框架,包含众多的编解码器、提供很多方便的函数用于解析或生成各种媒体文件。

大部分情况下,开发者使用ffmpeg开发应用程序,然而有时也有开发ffmpeg本身的需求,例如添加私有的编解码器,让应用程序开发者能够方便的使用上该codec。

这里介绍如何在ffmpeg中添加一个codec,以H264 Encoder为例。

这里使用的ffmpeg版本为0e56321,commit的时间为2017.09.26。之所以这里明确说明版本号,是因为ffmpeg经历过一次较大的API变化。

网络上的例子和代码说明的文章,大部分都是针对以前旧的API,例如雷神的文章。

新的API主要是以avcodec_send_frame和avcodec_receive_packet代替了之前的avcodec_encode_video2和avcodec_decode_video2。

源码libavcodec/avcodec.h中的注释给出了一些说明,如下:

This API replaces the following legacy functions:
- avcodec_decode_video2() and avcodec_decode_audio4():
  Use avcodec_send_packet() to feed input to the decoder, then use
  avcodec_receive_frame() to receive decoded frames after each packet.
  Unlike with the old video decoding API, multiple frames might result from
  a packet. For audio, splitting the input packet into frames by partially
  decoding packets becomes transparent to the API user. You never need to
  feed an AVPacket to the API twice (unless it is rejected with 
  AVERROR(EAGAIN) - then no data was read from the packet).
  Additionally, sending a flush/draining packet is required only once.
- avcodec_encode_video2()/avcodec_encode_audio2():
  Use avcodec_send_frame() to feed input to the encoder, then use
  avcodec_receive_packet() to receive encoded packets.
  Providing user-allocated buffers for avcodec_receive_packet() is not
  possible.
- The new API does not handle subtitles yet.

编译

在添加新的codec之前,可以先编译一下ffmpeg。

当然也可以直接添加,然后再编译,因为添加codec会修改核心文件,会导致几乎所有文件都重新编译,而编译一次ffmpeg需要较多时间。

这里创建一个编译脚本build.sh,内容如下:

#!/bin/bash

PREFIX=$HOME/ffmpeg_build
INC_PATH="-I$PREFIX/include"
LINK_PATH="-L$PREFIX/lib"

EXTERNAL_LIBS+="-lEGL -lGLESv2"
DEBUG_OPTS="--disable-stripping"

do_config() {
 PKG_CONFIG_PATH="$PREFIX/lib/pkgconfig" \
  ./configure \
  --prefix="$PREFIX" \
  --pkg-config-flags="--static" \
  --extra-cflags="-g $INC_PATH" \
  --extra-ldflags="$LINK_PATH " \
  --extra-ldlibflags="$LINK_PATH" \
  --extra-libs="$EXTERNAL_LIBS" \
  --enable-shared \
  --disable-static \
  $DEBUG_OPTS \
  --enable-gpl \
  --enable-libx264 \
  --enable-nonfree
}

print_usage() {
 echo "Usage: config, make, install"
}

case "$1" in
 "config") do_config;;
 "make") make -j 4;;
 "example") make examples;;
 "install") make install;;
 *)
  print_usage
esac

编译 配置

首先进行配置,执行如下命令

$ ./build.sh config

如果出现如下错误

nasm/yasm not found or too old. Use –disable-x86asm for a crippled build.

则需要安装汇编器nasm

$ sudo apt install nasm

因为是使能了libx264,如果出现如下错误

ERROR: libx264 not found

需要安装libx264的开发文件

$ sudo apt install libx264-dev

编译安装

配置完成之后,执行编译和安装命令

$ ./build.sh make
$ ./build.sh install

最终,ffmpeg的头文件、库文件和其他文件将安装在build.sh中定义的PREFIX(这里为$HOME/ffmpeg_build)目录下。

如果需要编译doc/examples目录下的测试程序。可以执行下面的命令:

$ ./build.sh example

添加codec

关于一个codec文件内容如何、怎样组织,可以参考析代码libavcodec/nvenc.c。

该codec使用nvenc库,利用Nvidia GPU完成H264编码。

另外,也可以参考FFmpeg codec HOWTO。这篇文章同样介绍了如何在ffmpeg中添加一个新的codec。

作为例子,这里假设添加一个名为new的、将NV12编码为H264 encoder。

添加codec源码

创建libavcodec/new_enc_h264.h,定义codec的私有数据结构。内容为:

#ifndef AVCODEC_NEW_ENC_H264_H
#define AVCODEC_NEW_ENC_H264_H
 
#include "config.h"
#include "avcodec.h"
 
typedef struct _NewEncCtx {

}NewEncCtx;
 
#endif

创建libavcodec/new_enc_h264.c,包含该codec的回调函数和codec结构体。内容为:

#include "new_enc_h264.h"
 
const enum AVPixelFormat ff_new_enc_pix_fmts[] = {
    AV_PIX_FMT_NV12,
    AV_PIX_FMT_NONE
};

static av_cold int ff_new_enc_init(AVCodecContext *avctx)
{
    av_log(avctx, AV_LOG_VERBOSE, "%s\n", __func__);
 
    return 0;
}
 
static av_cold int ff_new_enc_close(AVCodecContext *avctx)
{
    av_log(avctx, AV_LOG_VERBOSE, "NewEnc unloaded\n");
 
    return 0;
}
 
static int ff_new_enc_receive_packet(AVCodecContext *avctx, AVPacket *pkt)
{
    av_log(avctx, AV_LOG_WARNING, "Not implement.\n");
    return AVERROR(EAGAIN);
}
 
static int ff_new_enc_send_frame(AVCodecContext *avctx, const AVFrame *frame)
{
    av_log(avctx, AV_LOG_WARNING, "Not implement.\n");
    return AVERROR(EAGAIN);
}

AVCodec ff_h264_new_encoder = {
    .name           = "new_enc",
    .long_name      = NULL_IF_CONFIG_SMALL("New H264 Encoder"),
    .type           = AVMEDIA_TYPE_VIDEO,
    .id             = AV_CODEC_ID_H264,
    .priv_data_size = sizeof(NewEncCtx),
    .init           = ff_new_enc_init,
    .close          = ff_new_enc_close,
    .receive_packet = ff_new_enc_receive_packet,
    .send_frame     = ff_new_enc_send_frame,
    .pix_fmts       = ff_new_enc_pix_fmts,
};

添加到libavcodec

为了将new_enc加到libavcodec中,让外部程序调用,需要添加注册语句。

首先,在libavcodec/allcodecs.c中的函数avcodec_register_all里注册new_enc,添加语句REGISTER_ENCODER(H264_NEW, h264_new);。

如果是注册decoder,使用REGISTER_DECODER;如果是同时注册encoder和decoder,使用REGISTER_ENCDEC。

然后,在libavcodec/Makefile中加入新的codec:

OBJS-$(CONFIG_H264_NEW_ENCODER) += new_enc_h264.o

重新编译

修改源码之后,需要重新进行配置./build.sh config。之后可以在ffbuild/config.mak中确认CONFIG_H264_NEW_ENCODER是否使能。

然后执行编译和安装。为了使用编译出来的库,需要设置库的搜索路径LD_LIBRARY_PATH:

$ ./build.sh make
$ ./build.sh install
$ export LD_LIBRARY_PATH=$HOME/ff_build/lib

运行

$ ./ffmpeg -encoders | grep new

将打印当前的支持的encoders,可以看到有new_enc。

实现

以上其实已经介绍完成了如何添加新的codec。

只不过代码只是空架子,并没有实现实际的功能。要让codec工作起来,需要将各个回调函数实现,这涉及到如何使用ffmpeg内部API。

ffmpeg的内部API,和外部API一样,并没有很好的文档说明,更多的还是需要开发者自己看实现源码、读注释以及参考其他实现。

这里假设new_enc内部使用类似openMAX IL的接口来实现实际编码功能,包括:

  • GetEmptyBuffer: 获取空闲内存,用于存放原始数据

  • EmptyThisBuffer: 将原始数据提交给编码器

  • GetFilledBuffer: 获取编码之后的内存

  • FillThisBuffer: 返回编码内存

priv_data

结构体NewEncCtx,作为new_enc的私有结构,里面存放着和具体实现相关的数据,作为AVCodecContext的priv_data存放,在各个回调函数中获取得到。

#define to_NewEncCtx(avctx) ((NewEncCtx *)(avctx)->priv_data)
NewEncCtx *ctx = to_NewEncCtx(avctx);

init & close

int ff_new_enc_init(AVCodecContext *avctx)

该回调函数实现初始化工作,可能包括对编码器的硬件初始化、内存分配、私有数据的初始化等等。

int ff_new_enc_close(AVCodecContext *avctx)

释放init中的分配的资源。

send_frame

int ff_new_enc_send_frame(AVCodecContext *avctx, const AVFrame *frame)

该函数通过AVFrame,传递一帧视频原始数据(这里为NV12格式)给codec,在该函数内部实现对该帧的处理。通常的处理过程为:

  1. 使用GetEmptyBuffer获取空闲内存

  2. 将@frame中的数据放入到空闲内存中。因为frame数据量较大,且视频格式多样,这一步可能涉及到调用特定接口实现加速拷贝。

  3. 使用EmptyThisBuffer将填充后的内存放入到编码器中进行编码。

receive packet

int ff_new_enc_receive_packet(AVCodecContext *avctx, AVPacket *pkt)

该回调函数负责将编码后的数据放入到AVPacket中。ffmpeg使用AVPacket存放编码后的数据,与AVFrame相对应。

通常的处理过程如下:

  1. 调用GetFilledBuffer接口函数,获取编码内存

  2. 调用ff_alloc_packet2,给@pkt分配足够的空间存放编码后的数据。

  3. 从编码内存中拷贝数据到@pkt。这一步数据量通常较小,不需要特殊处理。

  4. 调用FillThisBuffer将处理后的编码内存返回给编码器。

测试

完成以上回调函数之后,就能实现基本的编码功能了。

重新编译之后,可以使用ffmpeg程序快速的进行验证。

以下的命令将NV12格式的文件raw_nv12_1920x1080,使用new_enc编码器进行编码,输出到output.mp4文件中。

$ ./ffmpeg -f rawvideo -pixel_format nv12 -framerate 30 -video_size 1920x1080 -i raw_nv12_1920x1080 -c:v new_enc output.mp4

如果想要以代码的形式,编写应用程序调用ffmpeg库进行测试,可以参考这个例子。

总结

要在ffmpeg中真正地实现一个codec,除了以上一些基本的工作之外,仍然需要对ffmpeg和视频格式更多的理解才行。虽然ffmpeg的文档缺少和不怎么更新一直被人诟病,但是代码结构还是比较清晰的,因此比起GStreamer来,我还是愿意开发ffmpeg。

参考

  1. https://wiki.multimedia.cx/index.php/FFmpeg_codec_HOWTO

来源:http://ericnode.info/post/how_to_write_ffmpeg_codec_zh/


技术交流,欢迎加我微信:ezglumes ,拉你入技术交流群。

推荐阅读:

音视频面试基础题

OpenGL ES 学习资源分享

一文读懂 YUV 的采样与格式

OpenGL 之 GPUImage 源码分析

推荐几个堪称教科书级别的 Android 音视频入门项目

觉得不错,点个在看呗~

相关文章:

  • FFmpeg从入门到精通——进阶篇,SEI那些事儿
  • iOS音频采集技术解读:如何实现男女变声的音效?
  • MediaCodec 、x264、faac 实现音视频编码并通过 rtmp 协议实现推流
  • 从《黑神话:悟空》的爆火,浅谈当前游戏从业者面临的机遇与挑战
  • 面试官: 说一下你做过哪些性能优化?
  • NDK系列-如何使用C/C++编写带EGL功能的NativeActivity
  • 短视频 SDK 开发 (一) 开发一款短视频 SDK 需要具备哪些知识?
  • 滴滴AR实景导航背后的技术
  • 国庆假期归来,音视频继续搞起,WebRTC送书活动来啦~~~
  • 「字节跳动直播研发团队」是如何每天护航百万直播间的?
  • FFmpeg代码架构
  • 播放器性能优化干货
  • WebRTC 送书活动获奖人员名单公布啦~~~
  • 为什么那些学好音视频的人,能够月薪50K+?
  • 绝密计划:我在阿里打黑工
  • 时间复杂度分析经典问题——最大子序列和
  • [LeetCode] Wiggle Sort
  • 2017届校招提前批面试回顾
  • 3.7、@ResponseBody 和 @RestController
  • AngularJS指令开发(1)——参数详解
  • Docker容器管理
  • EOS是什么
  • github从入门到放弃(1)
  • Javascript 原型链
  • Javascript编码规范
  • Java的Interrupt与线程中断
  • js ES6 求数组的交集,并集,还有差集
  • Promise面试题,控制异步流程
  • vue数据传递--我有特殊的实现技巧
  • webpack4 一点通
  • 给第三方使用接口的 URL 签名实现
  • 关于springcloud Gateway中的限流
  • 理清楚Vue的结构
  • 爬虫模拟登陆 SegmentFault
  • 前端之Sass/Scss实战笔记
  • 使用Envoy 作Sidecar Proxy的微服务模式-4.Prometheus的指标收集
  • 微信公众号开发小记——5.python微信红包
  • 源码安装memcached和php memcache扩展
  • ​虚拟化系列介绍(十)
  • # .NET Framework中使用命名管道进行进程间通信
  • # 数论-逆元
  • #常见电池型号介绍 常见电池尺寸是多少【详解】
  • #调用传感器数据_Flink使用函数之监控传感器温度上升提醒
  • (10)STL算法之搜索(二) 二分查找
  • (delphi11最新学习资料) Object Pascal 学习笔记---第5章第5节(delphi中的指针)
  • (pt可视化)利用torch的make_grid进行张量可视化
  • (板子)A* astar算法,AcWing第k短路+八数码 带注释
  • (超详细)2-YOLOV5改进-添加SimAM注意力机制
  • (二)pulsar安装在独立的docker中,python测试
  • (二)七种元启发算法(DBO、LO、SWO、COA、LSO、KOA、GRO)求解无人机路径规划MATLAB
  • (四) Graphivz 颜色选择
  • (四)JPA - JQPL 实现增删改查
  • (转) RFS+AutoItLibrary测试web对话框
  • (转)C#调用WebService 基础
  • (转)从零实现3D图像引擎:(8)参数化直线与3D平面函数库