1 . FFmpeg裁剪移植
之前我们简单地讲解了下如何在Linux系统中编译FFmpeg,但是编译出来的so体积太大,而且得到的多个so不便于使用。本节在此基础上,将详细讲解在编译FFmpeg时如何对相关模块作裁剪以精简so的体积,并且编译只生成一个so文件。首先,我们来看下在配置编译选项时,configure
的具体配置信息,可以进入FFmpeg源码根目录执行./configure --help
命令可得到,部分配置选项如下:
- Standar Options
--logfile=FILE 指定日志文件输出路径[ffbuild/config.log]
--disable-logging 不记录配置调试信息
--fatal-warnings 如果配置出现警告,就认为失败
--prefix=PREFIX 编译得到的库文件输出路径[/usr/local]
--bindir=DIR 二进制文件输出路径 [PREFIX/bin]
--datadir=DIR 数据输出路径[PREFIX/share/ffmpeg]
--docdir=DIR 文档输出路径[PREFIX/share/doc/ffmpeg]
--libdir=DIR libs输出路径[PREFIX/lib]
--shlibdir=DIR 动态库输出路径 [LIBDIR]
--incdir=DIR 头文件输出路径 [PREFIX/include]
--mandir=DIR 帮助文档输出路径 [PREFIX/share/man]
--pkgconfigdir=DIR pkg-config文件输出路径[LIBDIR/pkgconfig]
- Configuration options
--disable-static 禁止编译静态库
--enable-shared 开启编译动态库
--enable-small 开启优化大小
--disable-runtime-cpudetect 禁用在运行时检测CPU功能
--enable-gray 启用全灰度支持(slower color)
--disable-swscale-alpha 禁止在swscale中支持alpha通道
--disable-all 禁止编译所有组件(components)、库(libraries)、程序(programs)
--disable-autodetect 禁用自动检测到的外部库
这里用得较多的是--disable-static
、--enable-shared
和--enable-small
选项,其中,--disable-static
用于是否使能编译静态库文件(.a);--enable-shared
用于使能编译动态库文件(.so)。
- Program options
--disable-programs 禁止编译programs
--disable-ffmpeg 禁止编译ffmpeg
--disable-ffplay 禁止编译ffplay
--disable-ffprobe 禁止编译ffprobe
通常,我们会使用禁止编译ffmpeg、ffplay和ffprobe,其中,ffplay
是一个使用了FFmpeg和SDL库的、简单的、可移植的媒体播放器;ffprobe
用于查看多媒体文件的信息。
【腾讯文档】FFmpegWebRTCRTMPRTSPHLSRTP播放器-音视频流媒体高级开发-资料领取
https://docs.qq.com/doc/DYU5ORlBOdkpCUkNxhttps://docs.qq.com/doc/DYU5ORlBOdkpCUkNx
- Component options
--disable-avdevice 禁止编译libavdevice模块
--disable-avcodec 禁止编译libavcodec模块
--disable-avformat 禁止编译libavformat模块
--disable-swresample 禁止编译libswresample模块
--disable-swscale 禁止编译libswscale模块
--disable-postproc 禁止编译libpostproc模块
--disable-avfilter 禁止编译libavfilter模块
--enable-avresample 该模块已被弃用
--disable-pthreads 禁止pthreads [autodetect]
--disable-w32threads 禁止Win32 threads [autodetect]
--disable-os2threads 禁止OS/2 threads [autodetect]
--disable-network 禁止network支持
--disable-dct 禁止DCT代码模块
--disable-dwt DWT代码模块
--disable-error-resilience error resilience code
--disable-lsp 禁止LSP代码模块
--disable-lzo 禁止LZO decoder代码模块
--disable-mdct 禁止MDCT代码模块
--disable-rdft 禁止RDFT代码模块
--disable-fft 禁止FFT代码模块
--disable-faan 禁止floating point AAN (I)DCT代码模块
--disable-pixelutils 禁止libavutil模块中的pixel工具
这部分类似一个全局开关,用于对模块进行管控,假如我们非常明确编译的ffmpeg有明确的功能(不考虑未来扩展),那么,就可以对某些模块进行裁剪,以最大化精简so的大小、功能。
- Individual component options
--disable-everything 禁止所有的组件,就是下面列出来的这些
--disable-encoder=NAME 禁用名称为NAME的编码器
--enable-encoder=NAME 使能名称为NAME的编码器
--disable-encoders 禁用所有编码器,可通过指定NAME具体开启
--disable-decoder=NAME 禁用名称为NAME的解码器
--enable-decoder=NAME 使能名称为NAME的解码器
--disable-decoders 禁用所有解码器,可通过指定NAME具体开启
--disable-hwaccel=NAME 禁用名称为NAME的hwaccel
--enable-hwaccel=NAME 使能名称为NAME的hwaccel
--disable-hwaccels 禁用所有hwaccel,可通过指定NAME具体开启
--disable-muxer=NAME muxer NAME
--enable-muxer=NAME enable muxer NAME
--disable-muxers 禁用所有复用器,可通过指定NAME具体开启
--disable-demuxer=NAME demuxer NAME
--enable-demuxer=NAME enable demuxer NAME
--disable-demuxers 禁用所有解复用器,可通过指定NAME具体开启
--enable-parser=NAME enable parser NAME
--disable-parser=NAME parser NAME
--disable-parsers 禁用所有解析器,可通过指定NAME具体开启
--enable-bsf=NAME enable bitstream filter NAME
--disable-bsf=NAME bitstream filter NAME
--disable-bsfs 禁用所有位流过滤器,可通过指定NAME具体开启
--enable-protocol=NAME enable protocol NAME
--disable-protocol=NAME protocol NAME
--disable-protocols 禁用所有协议,可通过指定NAME具体开启
--enable-indev=NAME enable input device NAME
--disable-indev=NAME input device NAME
--disable-indevs 禁用所有输入设备,可通过指定NAME具体开启
--enable-outdev=NAME enable output device NAME
--disable-outdev=NAME output device NAME
--disable-outdevs 禁用所有输出设备,可通过指定NAME具体开启
--disable-devices 禁用所有设备,包括输入、输出
--enable-filter=NAME enable filter NAME
--disable-filter=NAME filter NAME
--disable-filters 禁用所有过滤器,可通过指定NAME具体开启
本部分的配置主要是选择那些组件需要编译,比如编码器、解码器、复用器、解复用器等等。举个栗子:
--disable-encoders
--enable-encoder=h263
--enable-encoder=libx264
--enable-encoder=aac
--enable-encoder=mpeg4
--enable-encoder=mjpeg
--enable-encoder=png
--enable-encoder=gif
--enable-encoder=bmp
--disable-muxers
--enable-muxer=h264
--enable-muxer=flv
--enable-muxer=gif
--enable-muxer=mp3
--enable-muxer=dts
--enable-muxer=mp4
--enable-muxer=mov
--enable-muxer=mpegts
--disable-decoders
--enable-decoder=aac
--enable-decoder=aac_latm
--enable-decoder=mp3
--enable-decoder=h263
--enable-decoder=h264
--enable-decoder=mpeg4
--enable-decoder=mjpeg
--enable-decoder=gif
--enable-decoder=png
--enable-decoder=bmp
--enable-decoder=yuv4
--disable-demuxers
--enable-demuxer=image2
--enable-demuxer=h263
--enable-demuxer=h264
--enable-demuxer=flv
--enable-demuxer=gif
--enable-demuxer=aac
--enable-demuxer=ogg
--enable-demuxer=dts
--enable-demuxer=mp3
--enable-demuxer=mov
--enable-demuxer=m4v
--enable-demuxer=concat
--enable-demuxer=mpegts
--enable-demuxer=mjpeg
--enable-demuxer=mpegvideo
--enable-demuxer=rawvideo
--enable-demuxer=yuv4mpegpipe
--enable-demuxer=rtsp
--disable-parsers
--enable-parser=aac
--enable-parser=ac3
--enable-parser=h264
--enable-parser=mjpeg
--enable-parser=png
--enable-parser=bmp
--enable-parser=mpegvideo
--enable-parser=mpegaudio
--disable-protocols
--enable-protocol=file
--enable-protocol=hls
--enable-protocol=concat
--enable-protocol=rtp
--enable-protocol=rtmp
--enable-protocol=rtmpt
--disable-filters
--disable-filters
--enable-filter=aresample
--enable-filter=asetpts
--enable-filter=setpts
--enable-filter=ass
--enable-filter=scale
--enable-filter=concat
--enable-filter=atempo
--enable-filter=movie
--enable-filter=overlay
--enable-filter=rotate
--enable-filter=transpose
--enable-filter=hflip
- External library support
--enable-libopencv enable video filtering via libopencv [no]
--enable-libopenh264 enable H.264 encoding via OpenH264 [no]
--enable-libopenjpeg enable JPEG 2000 de/encoding via OpenJPEG [no]
--enable-libx264 enable H.264 encoding via x264 [no]
--enable-libx265 enable HEVC encoding via x265 [no]
--enable-librtmp enable RTMP[E] support via librtmp [no]
...
FFmpeg框架中集成了非常多的第三方库,本部分选项主要是开启是否使用某些第三方库,以完成特定的功能。
- Toolchain options
--arch=ARCH 指定架构
--cpu=CPU 指定CPU型号
--cross-prefix=PREFIX 交叉编译工具的前缀(PREFIX)
--progs-suffix=SUFFIX program name suffix []
--enable-cross-compile 使能交叉编译
--sysroot=PATH root of cross-build tree
--sysinclude=PATH cross-build系统头文件路径
--target-os=OS 指定编译的系统类型
--target-exec=CMD 指定在系统上运行可执行程序的命令
--target-path=DIR 指定系统上查看编译路径
--target-samples=DIR 指定系统上samples的目录
--toolchain=NAME 根据NAME设置工具默认值
(gcc-asan, clang-asan, gcc-msan, clang-msan,
gcc-tsan, clang-tsan, gcc-usan, clang-usan,
valgrind-massif, valgrind-memcheck,
msvc, icl, gcov, llvm-cov, hardened)
--nm=NM 指定nm工具,名称为NM
--ar=AR 指定ar工具,名称为ARuse archive tool AR [ar]
--as=AS 指定汇编程序assembler AS []
--ln_s=LN_S 指定符号连接工具 LN_S [ln -s -f]
--strip=STRIP 指定strip工具STRIP [strip]
--windres=WINDRES 指定windows资源编译器WINDRES [windres]
--x86asmexe=EXE 指定nasm-compatible汇编EXE [nasm]
--cc=CC 指定C编译器use C compiler CC [gcc]
--cxx=CXX use C compiler CXX [g++]
--objcc=OCC use ObjC compiler OCC [gcc]
--dep-cc=DEPCC use dependency generator DEPCC [gcc]
--nvcc=NVCC use Nvidia CUDA compiler NVCC [nvcc]
--ld=LD use linker LD []
--pkg-config=PKGCONFIG use pkg-config tool PKGCONFIG [pkg-config]
--pkg-config-flags=FLAGS pass additional flags to pkgconf []
--extra-cflags=ECFLAGS add ECFLAGS to CFLAGS []
--extra-cxxflags=ECFLAGS add ECFLAGS to CXXFLAGS []
--extra-objcflags=FLAGS add FLAGS to OBJCFLAGS []
--extra-ldflags=ELDFLAGS add ELDFLAGS to LDFLAGS []
--extra-ldexeflags=ELDFLAGS add ELDFLAGS to LDEXEFLAGS []
--extra-ldsoflags=ELDFLAGS add ELDFLAGS to LDSOFLAGS []
--extra-libs=ELIBS add ELIBS []
--extra-version=STRING version string suffix []
...
这部分用于配置编译选项,比如配置交叉编译工具、指定编译架构、CPU型号以及其他编译参数等。对于比较常见的选项,我大概列举了一下,具体介绍如下:
--arch=ARCH
用于指定CPU的架构,常见的架构有arm
、arm64
、x86
等,其中,arm对应的CPU型号分为armv7-a
、armv5te
、armv6
等;arm64
对应的CPU型号为armv8-a
。
--cpu
用于指定CPU的型号,比如armv7-a
、armv5te
、armv8-a
等。
--target-os
用于指定编译的系统平台,比如linux、win32等。
--cross-prefix
用于指定编译工具前缀,比如--cross-prefix=/home/jiangdg/opt/android-ndk-r14b/toolchains/arm-linux-androideabi-4.9
/prebuilt/linux-x86_64/bin/arm-linux-androideabi
-
--sysroot
用于指定Android平台的目录,便于在编译过程中需要引用相关的库或者头文件,就会在--sysroot
指定的目录下去搜索,如:--sysroot=/home/jiangdg/opt/android-ndk-r14b/platforms/android-21
/arch-arm
/,当然,如果需要编译不同的架构,arch-arm
可能会不同,比如arm-arm64
、arch-x86
、arch-mips
等。
--cc
用于指定gcc
工具,根据编译的架构不同而不一样,如--cc=/home/jiangdg/opt/android-ndk-r14b/toolchains/arm-linux-androideabi-4.9
/prebuilt/linux-x86_64/bin/arm-linux-androideabi
-gcc。
--nm
用于指定nm
工具,根据编译的架构不同而不一样,如--cc=/home/jiangdg/opt/android-ndk-r14b/toolchains/arm-linux-androideabi-4.9
/prebuilt/linux-x86_64/bin/arm-linux-androideabi
-nm。
--extra-cxxflags
用于指定C++编译器(g++)的选项,比如--extra-cxxflags="-D__thumb__ -fexceptions -frtti"
,其中,-fexceptions
参数用于开启编译器异常捕获;-frtti
参数用于为每个有虚函数的类添加一些信息以支持rtti特性。
--extra-cflags
用于指定C编译器(gcc)的选项,比如--extra-cflags="-march=armv7-a -mfloat-abi=softfp -mfpu=neon -Os -fPIC -DANDROID -Wfatal-errors -Wno-deprecated"
,其中,-march
参数用于针对不同的CPU使用对应的CPU指令;-mfloat-abi
参数用于指定浮点;-Os
参数用于开启代码空间优化。
--extra-ldflags
用于指定库文件的位置,比如--extra-ldflags="L/home/jiangdg/opt/android-ndk-r14b/platforms/
android-21/
arch-arm/usr/lib"
。
- Optimization options(experts only)
--disable-asm 禁用所有程序集优化
--disable-altivec 禁用AltiVec优化
--disable-vsx 禁用AltiVec优化
--disable-power8 禁用POWER8优化
--disable-amd3dnow 禁用POWER8优化
--disable-amd3dnowext 禁用3 dnow !扩展优化
--disable-mmx 禁用MMX优化
--disable-mmxext 禁用MMXEXT优化
--disable-sse 禁用SSE优化
--disable-sse2 禁用SSE2优化
--disable-sse3 禁用SSE3优化
--disable-ssse3 禁用SSSE3优化
--disable-sse4 禁用SSE4优化
--disable-sse42 禁用SSE4.2
--disable-avx 禁用AVX优化
--disable-xop 禁用XOP优化
--disable-fma3 禁用FMA3优化
--disable-fma4 禁用FMA4优化
--disable-avx2 禁用AVX2优化
--disable-avx512 禁用AVX-512优化
--disable-aesni 禁用AESNI优化
--disable-armv5te 禁用armv5te优化
--disable-armv6 禁用armv6优化
--disable-armv6t2 禁用armv6t2优化
...
这部分选项仅限对ffmpeg框架非常熟悉的开发者使用,用于作某种优化,如果不熟悉轻易使用,可能会出现我们无法预知的异常。在编写脚本时,只有--disable-asm
选项用得比较多,即禁止所有程序集优化。
考虑文章篇幅原因,我的编译脚本就不贴了,有兴趣的可以前往github上下载:build_configure.sh。
2. 利用FFmpeg保存网络流到文件
2.1 重要结构体、函数
- AVDictionary
// AVDictionary结构体
struct AVDictionary {
int count;
AVDictionaryEntry *elems;
};
// AVDictionaryEntry结构体
typedef struct AVDictionaryEntry {
char *key;
char *value;
} AVDictionaryEntry;
解析:AVDictionary
结构体用于存储一系列key-value键值对,这些选项参数值将影响某一函数的操作,比如读取超时、传输协议选择(TCP/UDP)等。其中,count
字段表示key-value键值对的数量;elems
存储一系列key-value键值对,每个元素的类型为AVDictionaryEntry结构体。
- 函数:av_dict_get
/**
* 设置选项参数,如果之前存在则覆盖
* @param pm 指向AVDictionary结构体指针的指针变量
* @param key entry
* @param value entry value
* @param flags 可以设为不同的选项的组合,包含AV_DICT_MATCH_CASE时表示key的匹配是要区分大小写的
* 默认是不区分大小写;
* @return >= 0 设置选项参数成功
*/
int av_dict_set(AVDictionary **pm, const char *key, const char *value, int flags);
解析:av_dict_set函数用于为某一操作设置选项参数,比如对于解协议来说,rtsp_transport
参数用于设置传输协议且值可为"TCP"或"UDP";stimeout
参数用于设置超时时限(毫秒)等等。flags参数可以设置不同的选项组合(通常设0),来限定某些行为,主要有以下几个值:
AV_DICT_MATCH_CASE 区分大小写,默认不区分 AV_DICT_IGNORE_SUFFIX
AV_DICT_DONT_STRDUP_KEY
AV_DICT_DONT_STRDUP_VAL
AV_DICT_DONT_OVERWRITE 不覆盖已存在的key-value AV_DICT_APPEND 如果key-value存在,则值追加 AV_DICT_MULTIKEY 允许字典中存储相同的key
- 函数:avformat_alloc_output_context2
/**
* 为输出格式(output format)分配一个AVFormatCotext
* 注:使用avformat_free_context()释放分配的资源
*
* @param *ctx 要创建的输出格式AVFormatCotext;
* @param oformat 指定分配context的格式,如果为NULL则使用format_name和filename指定;
* @param format_name 指定音视频的格式,比如'mpegts';
* @param filename 音视频文件路径;
* @return >= 0 成功
*/
int avformat_alloc_output_context2(AVFormatContext **ctx,
AVOutputFormat *oformat,
const char *format_name,
const char *filename);
解析:avformat_alloc_output_context2
函数用于为指定输出文件格式创建(分配)一个AVFormatContext对象,我们可以直接通过oformat
对象指定输出格式(音视频文件格式),也可以通过format_name
来指定。其中,format_name
指的是输出文件封装格式,比如mpegts
(MP4)、flv
、mov
等(其他格式详见源文件\ffmpeg-4.0.2\libavformat\allformats.c)。
- 函数:avio_open2
/**
* 创建并初始化一个AVIOContext对象,该对象用于访问URL指定的资源,即用于打开FFmpeg的输
* 入输出文件,声明在libavformat\avio.h头文件中
*
* @param s 将要被创建的AVIOContext对象;
* @param url 资源URL地址;
* @param flags 打开URL方式,可以选择只读、只写或者读写;
* @param int_cb 中断回调接口,暂时没用到;
* @param options 设置选项参数,暂时没用到;
* @return >= 0 成功
*/
int avio_open2(AVIOContext **s, const char *url, int flags,
const AVIOInterruptCB *int_cb, AVDictionary **options);
解析:avio_open2
函数用于打开FFmpeg的输入\输出文件,当函数调用成功后,会为该文件创建一个对应的AVIOContext,通过AVIOContext来访问文件资源。其中,flags
用于指定打开输入\输出文件的方式,如AVIO_FLAG_READ_WRITE(读写)、AVIO_FLAG_READ(只读)、AVIO_FLAG_WRITE(只写)(被声明在libavformat\avio.h头文件中)。
- 函数:avcodec_copy_context
/**
* 拷贝源AVCodecContext信息到目标AVCodecContext
* 该函数被声明在libavcodec\avcodec.h头文件中
*
* @param dest 目标编解码器上下文(codec context)AVCodecContext
* @param src 源编解码器上下文(codec context)AVCodecContext
* @return 0 成功
*/
int avcodec_copy_context(AVCodecContext *dest, const AVCodecContext *src);
解析:avcodec_copy_context
函数的作用是将源AVCodecContext的设置拷贝到目标AVCodecContext,需要注意的是,在拷贝之前,我们需要使用avcodec_alloc_context3
或avformat_alloc_output_context2
函数初始化目标AVCodecContext,即创建和分配内存。另外,该函数已经被废弃了,虽然可用,但还是建议使用avcodec_parameters_from_context()
或avcodec_parameters_to_context()
函数。
- 函数:avformat_write_header
/**
* 为流的private data分配内存,同时将流头部写到输出文件中。
* 该函数被声明在libavformat\avformat.h头文件中
*
* @param s 用于输出的AVFormatContext;
* @param options 可选项参数,暂未用到;
*
* @return >= 成功
*/
int avformat_write_header(AVFormatContext *s, AVDictionary **options);
解析:avformat_write_header()
函数的作用是写输出视频文件的头部,其中,s
是输出文件的AVFormatContext,因此在调用该函数之前,我们需要为该AVFormatContext分配内存,并获得输出文件对应的AVIOContext对象。
- 函数:av_packet_rescale_ts
/**
* 将AVPacket包中的有效计时字段(时间戳/持续时间)从一个转换为一个时基到另一个。
* 该函数被声明在libavcodec\avcodec.h头文件中。
* @param pkt 将被处理的数据包(存储的是编码后的数据)
* @param tb_src 原始时间基
* @param tb_dst 目标时间基
*/
void av_packet_rescale_ts(AVPacket *pkt, AVRational tb_src, AVRational tb_dst);
解析:av_packet_rescale_ts
函数用于将将AVPacket包中的有效计时字段(时间戳/持续时间)从一个转换为一个时基到另一个,以确保音视频数据同步(因素之一)。时间基的作用就是要将PTS(Presentation TimeStamp,渲染时间戳
)或DTS(Decodeing TimeStamp,解码时间戳
)转换成以秒为单位的时间,其中,PTS用于视频渲染;DTS用于视频解码。ffmpeg中包含以下三种时间基:
tbr:是我们通常所说的帧率。time base of rate tbn:视频流的时间基。time base of stream tbc:视频解密的时间基。time base of codec
- 函数:av_interleaved_write_frame
/**
* 将数据包写入到输出文件中
* 该函数被声明在libavformat\avformat.h头文件中。
*
* @param s 输出文件的AVFormatContext
* @param pkt 将要被写入的数据包
*
* @return 0 成功
*/
int av_interleaved_write_frame(AVFormatContext *s, AVPacket *pkt);
解析:av_interleaved_write_frame
函数用于将AVPacket中的压缩数据写入到输出文件中,它与av_write_frame
函数不同的就是,前者适用于多个流或单一数据流情况,后者只适用于单一流。
2.2 实现原理
(1) 初始化FFmpeg引擎
为了使FFmpeg正常工作,我们首先要初始化FFmpeg引擎,主要包括初始化所有muxers、demuxers、protocol以及编解码器等,因为在保存网络流过程中,需要解协议(解封装)得到里面的音视频数据、获取编码器信息、重新封装等。其中,av_register_all
函数的作用就是初始化libavformat库和所有muxers、demuxers、protocol;avcodec_register_all
函数的作用是初始化所有编解码器。(注:所谓所有,即FFmpeg裁剪后保留下来的。)
void initFFmpeg() {
av_register_all();
avcodec_register_all();
// 设置FFmpeg引擎日志等级
av_log_set_level(AV_LOG_VERBOSE);
// 为一个AVPacket分配内存
// 用于临时存储解协议得到的数据包
g_save.avPacket = (AVPacket *) av_malloc(sizeof(AVPacket));
}
注:g_save
的类型为SaveStream结构体,该结构体为自定义,具体如下:
typedef struct SaveStream{
AVFormatContext *inputCtx;
AVFormatContext *outputCtx;
AVPacket *avPacket;
}FFmpegSaveStream;
// 声明一个全局FFmpegSaveStream变量
extern FFmpegSaveStream g_save;
(2) 打开输入URL
在FFmpeg从入门到入魔(1):初探FFmpeg框架_irainsa的博客-CSDN博客一文中,我们介绍到了AVFormatContext结构体描述了一个多媒体文件或流的构成和基本信息,是FFmpeg中最为基本的一个结构体,也是其他所有结构的根。因此,我们首先需要调用avformat_alloc_context()
函数为输入的URL分配一个AVFormatContext结构体。然后,调用avformat_open_input()
函数打开输入流和读取头部信息并将其存储到AVFormatContext。接着,调用avformat_find_stream_info
函数读取一部分视音频数据并且获得一些相关的信息,通俗来说,就是探测流格式信息,比如编码宽高等。
int openInput(char *input_url){
MLOG_I_("#### open url = %s", input_url);
if(! input_url) {
MLOG_E("#### input url is null in openInput function.");
return -100;
}
// 初始化输入URL的AVFormatContext
g_save.inputCtx = avformat_alloc_context();
if(! g_save.inputCtx) {
MLOG_E("#### alloc input AVFormatContext failed.");
return -99;
}
AVDictionary *opts = NULL;
av_dict_set(&opts, "rtsp_transport","tcp", 0); //设置tcp or udp,默认一般优先tcp再尝试udp
av_dict_set(&opts, "stimeout", "3000000", 0); //设置超时3秒
// 打开URL,初始化输入文件的g_save.inputCtx
int ret = avformat_open_input(&g_save.inputCtx, input_url, NULL, &opts);
if(ret < 0) {
MLOG_E_("#### open input url failed,err=%d(timesout?)", ret);
return ret;
}
// 探测流的格式信息
ret = avformat_find_stream_info(g_save.inputCtx, NULL);
if(ret < 0) {
MLOG_E_("#### find stream failed,err=%d", ret);
return ret;
}
return ret;
}
(3) 打开输出文件
同输入文件一样,对于输出文件我们也需要为其创建一个AVFormatContext结构体,但通过调用avformat_alloc_output_context2()
函数实现,并且需要指定输出文件的封装格式,比如“mpegts
”(MP4)、mov
、mkv
等。然后,调用avio_open2()
函数创建并初始化一个AVIOContext来访问url表示的资源;接着,根据输入文件流信息为输出文件创建相应的stream(avformat_new_stream()
),同时将输入文件流的编码器信息写入到输出文件的AVCodecContext(avcodec_copy_context
);最后,调用avformat_write_header()
函数写视频文件头,即完成对输出文件的初始化。
int openOutput(char *out){
MLOG_I_("#### open output file = %s", out);
// 初始化输出文件AVFormatContext
int ret = avformat_alloc_output_context2(&g_save.outputCtx, NULL, "mpegts", out);
if(ret < 0) {
MLOG_E_("#### Allocate an AVFormatContext for an output format failed,err=%d", ret);
closeOutput();
return ret;
}
// 创建并初始化一个AVIOContext,用于访问url资源
// app需要给存储权限,否则ret=-13
ret = avio_open2(&g_save.outputCtx->pb, out, AVIO_FLAG_WRITE, NULL, NULL);
if(ret < 0) {
MLOG_E_("#### Create and initialize a AVIOContext failed,err=%d", ret);
closeOutput();
return ret;
}
// 根据inputCtx,为输出文件创建流
// 获取每个流的编码器信息,为输出流复制一份
int num_streams = g_save.inputCtx->nb_streams;
for(int i = 0; i < num_streams; i++){
AVStream * stream = avformat_new_stream(g_save.outputCtx,
g_save.inputCtx->streams[i]->codec->codec);
ret = avcodec_copy_context(stream->codec, g_save.inputCtx->streams[i]->codec);
if(ret < 0)
{
av_log(NULL, AV_LOG_ERROR, "copy coddec context failed");
}
}
// 为流的private data分配空间
// 并将stream header写到输出文件中
ret = avformat_write_header(g_save.outputCtx, NULL);
if(ret < 0) {
MLOG_E_("#### write the stream header to"
" an output media file failed,err=%d", ret);
return ret;
}
return ret;
}
(4) 从输入网络流读取视/音频数据
从输入文件流中读取压缩数据很简单,只需要调用av_read_frame()
函数即可实现将视频或音频读入到AVPacket中缓存起来,需要注意的是,每次读取最好是调用av_init_packet()
函数初始化这个临时的AVPacket。另外,AVPacket存储的是压缩数据,且对于视频数据来说,存储的是一帧视频数据,而对于音频来说,可能存储了多帧音频数据。
int readAvPacketFromInput(){
if(! g_save.avPacket) {
return -99;
}
// 初始化临时AVPacket变量
av_init_packet(g_save.avPacket);
int ret = av_read_frame(g_save.inputCtx, g_save.avPacket);
if(ret < 0) {
MLOG_I("#### read frame error or end of file");
return ret;
}
MLOG_I("----->read a frame");
return ret;
}
(5) 写入数据到输出文件
将读出的压缩数据写入到输出文件中,是通过调用av_interleaved_write_frame()
函数实现的,相比av_write_frame
来说,前者允许复用器muxers提前获取将要处理的packets相关信息。但是,在写入之前需要调用av_packet_rescale_ts()
函数将AVPacket中的原始时间基转换为目标时间基,以确保音视频同步。
int writeAvPacketToOutput() {
int ret = -99;
if(! g_save.avPacket) {
return ret;
}
AVStream *inputStream = g_save.inputCtx->streams[g_save.avPacket->stream_index];
AVStream *outputStream = g_save.outputCtx->streams[g_save.avPacket->stream_index];
if(inputStream && outputStream) {
// 处理同步
av_packet_rescale_ts(g_save.avPacket, inputStream->time_base, outputStream->time_base);
// 写入数据到输出文件
ret = av_interleaved_write_frame(g_save.outputCtx, g_save.avPacket);
if(ret < 0) {
MLOG_E_("#### write a packet to an output media file failed,err=%d", ret);
return ret;
}
}
MLOG_I("----->write a frame");
return ret;
}
(6) 释放FFmpeg引擎资源
关闭流,释放分配的内存资源。
void releaseFFmpeg(){
closeOutput();
closeInput();
if(g_save.avPacket) {
av_packet_unref(g_save.avPacket);
}
}
void closeInput() {
if(g_save.inputCtx) {
avformat_close_input(&g_save.inputCtx);
avformat_free_context(g_save.inputCtx);
}
}
void closeOutput() {
if(g_save.outputCtx) {
for(int i = 0 ; i < g_save.outputCtx->nb_streams; i++) {
AVStream * avStream = g_save.outputCtx->streams[i];
if(avStream) {
AVCodecContext *codecContext = avStream->codec;
avcodec_close(codecContext);
}
}
avformat_close_input(&g_save.outputCtx);
avformat_free_context(g_save.outputCtx);
}
}
2.3 实战案例
本节将在上节的基础上,演示Android平台如何使用FFmpeg引擎将网络流(rtsp、rtmp等)保存到本地文件中,且封装格式为mp4。为了不影响Android主线程的运行,在native层我们创建一个子线程来处理。FFmpeg的具体处理流程如下图所示:
(1) 注册native方法
static JNINativeMethod g_methods[] = {
{"nativeStart","(Ljava/lang/String;Ljava/lang/String;Lcom/jiangdg/natives/SaveStreamUtil$OnInitCallBack;)I",
(void *)save_start},
{"nativeStop", "()I", (void *)save_stop}
};
extern "C"
JNIEXPORT jint JNI_OnLoad(JavaVM *jvm, void *reserved)
{
JNIEnv *env;
// 缓存JavaVM,获取JNIEnv实例
g_jvm = jvm;
if(jvm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK) {
MLOG_E("##### get JNIEnv object failed.");
return JNI_ERR;
}
// 获取Java Native类
jclass clazz = env->FindClass("com/jiangdg/natives/SaveStreamUtil");
// 注册Natives方法,NELEM获得方法的数量
if(env->RegisterNatives(clazz, g_methods, NELEM(g_methods)) < 0) {
MLOG_E("##### register natives failed.");
return JNI_ERR;
}
return JNI_VERSION_1_4;
}
与以往在native层生成Java方法映射函数不同的是,本示例将在JNI_OnLoad()
函数中通过调用JNIEnv$RegisterNatives()
函数来实现,这种方法的好处就是我们无需在像以前样为每个Java native方法进行声明,并且JNI_OnLoad()函数在so被Java层加载时(System.loadLibrary(so)
)就会被调用,方便我们处理一些全局信息,如缓存JavaVM实例等。JNIEnv$RegisterNatives()
需要传入三个参数,即Java层native方法类信息、JNINativeMethod
类型的数组以及数组的元素个数,其中,JNINativeMethod类型数组存储的是Java层方法与native层函数的映射信息,该类型是一种结构体,包含三个成员变量,即Java层native方法、native方法签名、映射函数。JNINativeMethod结构体如下:
typedef struct {
const char* name; // native方法名
const char* signature; // native方法签名
void* fnPtr; // native方法的映射函数
} JNINativeMethod;
(2) 启动保存子线程
static jint save_start(JNIEnv *env, jobject thiz, jstring _url, jstring _out, jobject callback)
{
g_quit = 0;
if(!_url || !_out) {
MLOG_E("#### save_start: url or output path can not be null");
return -1;
}
c_url = jstring_to_string(env, _url);
c_out = jstring_to_string(env, _out);
g_callbackobj = env->NewGlobalRef(callback);
// 启动子线程
// sizeof(params)得到的是指针变量大小,固定占4字节
params = (ThreadParams *)malloc(sizeof(ThreadParams));
params->url = c_url;
params->out = c_out;
pthread_create(&id_save_thread, NULL, save_thread, params);
return 0;
}
为了不影响Android主线程正常运行,我们在nativeStart
映射函数save_start
中创建一个子线程来处理具体的业务,需要注意的是,考虑到在Java语言中对象作为参数在函数中传递总是传递的是对象实体而不是对象引用,因此,假如我们传入到nativeStart方法的_url
或_out
是一个局部变量,当调用nativeStart
的某个Java方法执行完毕后,也就是不等待save_start
执行完毕,此时_url
或_out
对象的引用将会被释放,而传入的对象就会直接"裸奔
",容易被GC回收,从而导致底层save_start函数还未用出现访问异常情况。因此,我们需要对其在底层进行缓存再使用,当然,对于开辟的新内存注意合适的时候进行释放操作。jstring_to_string
函数处理如下:
char * jstring_to_string(JNIEnv *env, jstring j_str) {
const char * c_str = env->GetStringUTFChars(j_str, JNI_FALSE);
jsize len = env->GetStringLength(j_str);
char * ret = NULL;
// char * 默认末尾有'/0'
if(len > 0) {
ret = (char *) malloc((len+1) * sizeof(char));11
memset(ret, 0, (len+1));
memcpy(ret, c_str, len);
ret[len] = 0;
}
env->ReleaseStringUTFChars(j_str, c_str);
return ret;
}
(3) 初始化FFmpeg,处理数据
// 子线程函数入口
void *save_thread(void *args) {
pthread_detach(pthread_self());
JNIEnv *env = NULL;
jmethodID methodId = NULL;
// 将当前线程绑定到JavaVM,从JVM中获取JNIEnv*
// 并得到回调接口方法
if(g_jvm) {
if(g_jvm->GetEnv(reinterpret_cast<void **>(env), JNI_VERSION_1_4)>0) {
MLOG_E("Get JINEnv object failed.");
return NULL;
}
if(JNI_OK != g_jvm->AttachCurrentThread(&env, NULL)) {
MLOG_E("Get JINEnv object failed.");
return NULL;
}
jclass cbClz = env->GetObjectClass(g_callbackobj);
methodId = env->GetMethodID(cbClz, "onResult", "(I)V");
}
// 初始化FFmpeg引擎
initFFmpeg();
ThreadParams *params = (ThreadParams *)args;
if(! params) {
MLOG_E("#### get thread parms failed in save_thread.");
if(env) {
env->DeleteGlobalRef(g_callbackobj);
g_jvm->DetachCurrentThread();
}
return NULL;
}
// 打开输入流
int ret = openInput(params->url);
if(ret < 0) {
MLOG_E_("#### open input url failed,err=%d", ret);
if(g_jvm && methodId) {
env->CallVoidMethod(g_callbackobj, methodId, -1);
env->DeleteGlobalRef(g_callbackobj);
g_jvm->DetachCurrentThread();
}
closeInput();
return NULL;
}
// 打开输出文件
ret = openOutput(params->out);
if(ret < 0) {
MLOG_E_("#### open out file failed,err=%d", ret);
if(g_jvm && methodId) {
env->CallVoidMethod(g_callbackobj, methodId, -2);
env->DeleteGlobalRef(g_callbackobj);
g_jvm->DetachCurrentThread();
}
closeOutput();
return NULL;
}
if(methodId) {
env->CallVoidMethod(g_callbackobj, methodId, 0);
}
// 循环读取
bool is_reading = false;
while (! g_quit) {
if(readAvPacketFromInput() == 0) {
writeAvPacketToOutput();
MLOG_I("##### write a packet data");
}
if(! is_reading) {
is_reading = true;
env->CallVoidMethod(g_callbackobj, methodId, 1);
}
}
// 释放各种资源
releaseFFmpeg();
if(params) {
free(params);
}
if(c_url) {
free(c_url);
}
if(c_out) {
free(c_out);
}
if(g_jvm) {
env->CallVoidMethod(g_callbackobj, methodId, 2);
env->DeleteGlobalRef(g_callbackobj);
g_jvm->DetachCurrentThread();
}
MLOG_I("save stream success.");
// void * 必须要返回NULL
// 否则会报libc: Fatal signal 5 (SIGTRAP)错误
return NULL;
}
为了便于获取native层的处理情况,我们需要通过在native层调用Java层回调接口将处理结果反馈给Java层。需要注意的是,native层调用Java层接口、对象、方法等都是需要用到JNIEnv的函数,但是JNIEnv只对当前线程(一般为主线程)有效(全局缓存也没用,也只是对当前线程有效
),在其他子线程是无法直接获取JNIEnv,因此,需要调用JavaVM$AttachCurrentThread()
函数将该线程绑定到JavaVM(解绑使用JavaVM$DetachCurrentThread()
),然后获取对应的JNIEnv。
注:JavaVM$GetEnv() < 0时,表示获取JNIEnv指针成功。