FFmpeg从入门到入魔(2):保存流到本地MP4

news2024/11/25 8:13:59

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/DYU5ORlBOdkpCUkNxicon-default.png?t=M85Bhttps://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的架构,常见的架构有armarm64x86等,其中,arm对应的CPU型号分为armv7-aarmv5tearmv6等;arm64对应的CPU型号为armv8-a

--cpu

 用于指定CPU的型号,比如armv7-aarmv5tearmv8-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-arm64arch-x86arch-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)、flvmov等(其他格式详见源文件\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_context3avformat_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)、movmkv等。然后,调用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指针成功。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/718819.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Flutter 组件】005-基础组件:单选、开关和复选框

【Flutter 组件】005-基础组件&#xff1a;单选、开关和复选框 文章目录 【Flutter 组件】005-基础组件&#xff1a;单选、开关和复选框一、概述二、基本使用1、开关代码示例运行结果 2、复选框代码示例运行结果 3、多个选项单选代码示例运行结果 4、多个选项多选代码示例运行结…

一次源码编译安装PostgreSql失败

需要perl&#xff1b;之前博文已提到&#xff1b;之前有一种编程语言叫perl&#xff0c;此perl应该不是那个&#xff1b;可到其官网下载&#xff0c;Perl Download - www.perl.org 安装时添加到环境变量&#xff1b; 可能是一个东西&#xff1b;有编程语言和工具&#xff1b;大…

怎样愉快的使用串口发送16进制数据并读取串口内容

像雷达 imu 陀螺仪一类的传感器&#xff0c;一般都是用的usb转串口和主机连接&#xff0c;然后通过串口读取传感器数据&#xff0c;串口是我们绕不过的一道坎&#xff0c;那我们就来继续手撕串口。 串口连接主机问题看上篇&#xff1a; 怎样愉快的连接使用usb转串口设备_JT_B…

WalxPlugin免Root框架使用详解和示例代码

2023年7月4日首发 WalxPlugin框架&#xff08;以下简称WP框架&#xff09;是一个不需要root权限就能使用的插件化模块&#xff0c;能够轻松实现在非root设备hook其它应用的调用和访问进程数据等功能。目前该框架已发布测试版。 一.WP框架工具包提供了以下几个模块&#xff1a…

5.8.5 TCP可靠传输(一)序号确认机制

5.8.5 TCP可靠传输&#xff08;一&#xff09;序号确认机制 TCP是可靠的传输层协议&#xff0c;主要通过序号确认机制、超时重传机制、定时器三个方面实现可靠传输。 一、序号确认机制 TCP将所要传送的整个的应用层报文看成是一个一个字节组成的数据流&#xff0c;并对每一个…

Cyclo(-Ala-Tyr),21754-26-7,环(丙氨酸-酪氨酸)二肽,由两个氨基酸通过肽键环合形成

&#xff08;文章资料汇总&#xff1a;陕西新研博美生物科技有限公司小编MISSwu&#xff09;​ 【产品描述】 Cyclo(-Ala-Tyr)&#xff0c;环(丙氨酸-酪氨酸)二肽&#xff0c;环二肽由两个氨基酸通过肽键环合形成&#xff0c;在氢键相互作用驱动下具有较强的自组装倾向&#x…

python的作用域、globals()-全局变量 和 locals()-局部变量

目录 查看全局变量和局部变量 变量解析规则 变量生存周期 在python中&#xff0c;函数会创建一个自己的作用域&#xff0c;也称为为命名空间。当我们在函数内部访问某个变量时&#xff0c;函数会优先在自己的命名空间中寻找。 我们自己定义的全局变量均在python内建的globa…

java项目linux启动文件

更改jar包名称和jar包所在目录 JAR包名称 替换成自己的项目包名称 JAR包所在目录替换成自己的jar包所在的目录 这里面的字符建议手打到服务器文件内&#xff0c;复制粘贴的话可能存在特殊符号 ps -ef|grep java SERVICE_PID$(ps aux | grep JAR包名称 | grep -v grep | awk…

influxDB聚合类函数

influxDB聚合类函数 1&#xff09;count()函数 返回一个&#xff08;field&#xff09;字段中的非空值的数量。 SELECT COUNT(<field_key>) FROM <measurement_name> [WHERE <stuff>] [GROUP BY <stuff>] 例子1 计算非空water_level数量SELECT COUN…

Web3 处理智能合约部署到本地区块链,并在本地进行测试

上文 Web3 在Truffle项目中编写出自己的第一个solidity智能合约我们演示了 在Truffle环境下写一个智能合约并编译的功能 编译出的文件夹中的这个JSON就非常重要了 我们就可以通过 它这个ABI链接到需要的智能合约程序上去 但这也仅仅是编译完了 我们的智能合约还没有部署到我们…

gma 2 教程(一)概述:2.GMA 安装

依赖环境 系统环境 【操作系统】&#xff1a;64位&#xff08;amd64&#xff09;Windows、Linux 【内存】&#xff1a;≥8 GBPython 环境 【Windows】&#xff1a;3.8.8~3.11 【Linux】&#xff1a;3.9~3.11 目前不支持MacOS和其他平台。建议安装Anaconda创建Python环境。Anac…

解决postman接口自动化测试中登录后401权限问题

大家在做接口自动化测试的时候&#xff0c;碰到有登录的系统应该都会遇到401权限的问题&#xff0c;下面我来说下如何解决这个问题。 首先需要了解问题的本质&#xff0c;是要解决什么&#xff1f;这样我们才好对症下药&#xff0c;报401是因为用户在登录的时候&#xff0c;服务…

亚马逊云科技让AI开发速度更快,门槛更低

过去只有大型科技公司、政府机构和大学才有能力和时间、金钱去部署生成式AI能力。近二十年来&#xff0c;云服务把计算的门槛降低了。亚马逊云科技全球产品副总裁Matt Wood博士表示&#xff1a;“我们希望在生成式AI上做同样的事&#xff0c;把技术提供到每个开发者和商业用户的…

kaggle免费服务器全攻略

1. kaggle服务器16G显卡一周40小时. 所以我们直接干一堆谷歌账号即可 2. 谷歌账号的注册: 我们需要FQcolab for windows可以做到. 然后我们注册好账号后.我们注册4个账号. 注册方法. 打开chrome 点最下面的添加按钮.然后一直下一步即可.无脑注册. 3. 为Chrome多账户添加单独的…

机器人制作开源方案 | 立式铣床模型

1. 功能说明 铣床主要是指用铣刀在工件上加工多种表面的机床&#xff0c;本文示例将通过程序控制模拟铣床的运动效果--模拟铣床进行加工时各个结构的运动方式。铣床的运动主要包括&#xff1a;主轴的旋转运动、立铣头的上下进给运动、工作台的前后左右进给运动。 2. 结构说明 该…

期权专业知识常用术语有哪些?虚值期权和实值期权怎么选择合理的执行价

【1】美式期权&#xff1a;每个交易日都可以行权的期权 【2】欧式期权&#xff1a;只有在到期日才可以执行的期权 【3】平值期权&#xff1a;内在价值为零或极小的期权&#xff0c;或行权价格等于或接近于标的物合约市场价格的期权。通常是指行权价格最接近标的合约市场价格的期…

JavaEE语法之第二章、多线程(初阶三)

目录 一、多线程带来的风险-线程安全 (重点) 1.1观察线程不安全 1.2线程安全的概念 1.3线程不安全的原因 1.3.1抢占式执行&#xff08;进程的随机调度&#xff09; 1.3.2多个线程修改同一个变量 1.3.3内存的可见性 1.3.4原子性 1.3.5指令重排序 二、解决之前的线程不…

Spring Boot 中的 @Query 注解是什么,原理,如何使用

Spring Boot 中的 Query 注解是什么&#xff0c;原理&#xff0c;如何使用 在 Spring Boot 中&#xff0c;Query 注解是一个非常常用的注解&#xff0c;用于定义自定义查询语句。本文将介绍 Query 注解的作用、原理和使用方法。 1. Query 注解的作用 在 Spring Boot 中&#…

【论文精读】《Classifying User Activities in the Encrypted WeChat Traffic》

Classifying User Activities in the Encrypted WeChat Traffic Authors:Chengshang Hou,Junzheng Shi,Cuicui Kang,Zigang Cao,Xiong Gang Journal:2018 IEEE 37th International Performance Computing and Communications Conference (IPCCC) (2018) 摘要 加密移动应用程序…

【算法】从记忆化搜索到递推——动态规划入门

文章目录 笔者说&#xff1a;我们为什么要学记忆化搜索&#xff1f;预备知识例题&#xff1a;198. 打家劫舍记忆化搜索 相关题目练习70. 爬楼梯记忆化搜索dp 746. 使用最小花费爬楼梯记忆化搜索dp 2466. 统计构造好字符串的方案数记忆化搜索dp 213. 打家劫舍 II记忆化搜索dp 笔…