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

2023-11-05

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指针成功。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

FFmpeg从入门到入魔(2):保存流到本地MP4 的相关文章

  • 用于获取特定用户 ID 和进程数的 Bash 脚本

    我需要 bash 脚本来计算特定用户或所有用户的进程 我们可以输入 0 1 或更多参数 例如 myScript sh root deamon 应该像这样执行 root 92 deamon 8 2 users has total proces
  • FFmpeg av_read_frame 无法正确读取帧?

    好吧 我已经下载了一些 yuv 格式的原始 UHD 序列 并在 mp4 容器中使用 ffmpeg 对其进行编码 h264 4 4 4 100 质量 25fps 当我使用 ffprobe 找出编码了多少帧时 我得到 600 所以这是 24 秒
  • 在嵌入式系统上将内核控制台发送到哪里?

    我正在开发一个嵌入式系统 该系统当前通过串行端口 1 上的控制台输出启动 Linux 使用启动加载程序中的控制台启动参数 然而 最终我们将使用这个串行端口 内核控制台输出的最佳解决方案是什么 dev null 能否以某种方式将其放在 pty
  • 如何启用 FFMPEG 日志记录?

    我想调试 ffmpeg 我添加以下代码来打印日志 av log s AV LOG PANIC fmt or printf msg 但这行不通 没有任何调试信息 然后我启用调试构建选项 export COMMON FF CFG FLAGS C
  • 如何回忆上一个 bash 命令的参数?

    Bash 有没有办法回忆上一个命令的参数 我通常这样做vi file c其次是gcc file c Bash 有没有办法回忆上一个命令的参数 您可以使用 or 调用上一个命令的最后一个参数 Also Alt can be used to r
  • id3 图像编辑后播放 mp3 时遇到问题

    由于硬件限制 我们生产的软件试图确保导入到其库中的任何音频文件 准备复制到硬件上 都是可接受的比特率 最近 我们开始使用 FFmpeg 将许多不同的音频类型转换为 mp3 以便在我们的硬件上导入和使用它们 虽然转换工作正常并且 mp3 文件
  • 如果输入被重定向则执行操作

    我想知道如果我的输入被重定向 我应该如何在 C 程序中执行操作 例如 假设我有已编译的程序 prog 并且我将输入 input txt 重定向到它 我这样做 prog lt input txt 我如何在代码中检测到这一点 一般来说 您无法判
  • 为什么无论 -rdynamic 如何,backtrace 都不包含 Objective-C 符号?

    Update 我正在 Linux 上使用 GNU 运行时 问题是not发生在带有 Apple 运行时的 MacOS 上 更新2 我在 MacOS 上编译了 GNU 运行时并用它构建了示例 该错误确实not发生在带有 GNU 运行时的 Mac
  • 当在 python linux 中执行命令 os.system() 时,在 python 中给出响应 yes/no

    考虑一个像这样的命令 yum install boto 当我在终端中执行时 要继续 会询问我是 否 我可以像这样用 python 回应它吗 os system yum install boto Next Yes 将通过相同的 python
  • 无法连接到 Azure Ubuntu VM - 公钥被拒绝

    我们在 Azure 上使用 Ubuntu VM 一段时间了 很少遇到任何问题 然而 其中一台虚拟机最近出现了问题 出乎意料的是 Ubuntu VM 开始拒绝公钥 ssh i azure key email protected cdn cgi
  • 安装 JDK 时出错:keytool 命令需要已安装的 proc fs (/proc)。 Linux 的 Windows 子系统

    我尝试在 Linux 的 Windows 子系统 Ubuntu 14 04 上安装 Oracle JDK 1 7 但出现以下错误 the keytool command requires a mounted proc fs proc Jav
  • 通过名称获取进程ID

    我想在 Linux 下获得一个给定其名称的进程 ID 有没有一种简单的方法可以做到这一点 我还没有在 C 上找到任何可以轻松使用的东西 如果追求 易于使用 char buf 512 FILE cmd pipe popen pidof s p
  • GCC 详细模式输出解释

    我是 Linux 新手 谁能向我解释一下我的 hello world 程序的以下详细模式输出 另外 这些文件是做什么用的crt1 o crti o crtend o crtbegin o and crtn o and lc and lgcc
  • Linux 中的 Windows NAmed Pipes 替代品

    我们正在将现有的 Windows 代码移植到 Linux 我们使用 ACE 作为抽象层 我们使用 Windows 命名管道与多个客户端进行通信并执行重叠操作 linux 下这个相当于什么 我检查了linux命名管道 FIFO 但它们似乎只支
  • 在 MacOS 上构建需要 net461 的 dotnet SDK 项目的最简单方法

    我有一个 dotnet SDK sln and a build proj with
  • 使用 Python 将阿拉伯语或任何从右到左书写系统的字符串打印到 Linux 终端

    非常简单的例子是 city print city 我期望输出是 但实际上输出是相反的字符串 字母看起来有点不同 因为它们有开始 中间和结束形式 我无法将其粘贴到此处 因为复制粘贴会再次更正字符串的顺序 如何在 Linux 终端上正确打印阿拉
  • 如何在C程序中直接改变显存映射来绘制像素(无需库函数)

    是否可以通过使用 C 程序更改 RAM 中屏幕 视频即监视器 内存映射中的值来显示黑点 我不想使用任何库函数 因为我的主要目标是学习如何开发简单的操作系统 我尝试访问起始屏幕内存映射 即 0xA0000 在 C 中 我尝试运行该程序 但由于
  • “./somescript.sh”和“. ./somescript.sh”有什么区别

    今天我按照一些说明在 Linux 中安装软件 有一个需要首先运行的脚本 它设置一些环境变量 指令告诉我执行 setup sh 但是我执行时犯了一个错误 setup sh 所以环境没有设置 最后我注意到了这一点并继续进行 我想知道这两种调用脚
  • PHP 日志文件颜色

    我正在编写一个 PHP 日志文件类 但我想为写入文件的行添加颜色 我遇到的问题是颜色也会改变终端的颜色 我想要实现的是仅更改写入日志文件的行的颜色 class logClass extends Singleton private funct
  • 使用 hcitool 扫描低功耗蓝牙?

    当我运行此命令时 BLE 设备扫描仅持续 5 秒 sudo timeout 5s hcitool i hci0 lescan 输出显示在终端屏幕中 但是 当我将输出重定向到文件以保存广告设备的地址时 每次运行该命令时 我都会发现该文件是空的

随机推荐

  • python做兼职收入_创业点子 小哥用Python兼职月入过万,用Python做项目有多挣钱?...

    今天我想和大家分享一些Python项目两个主兼职工作 老板想说 无论你是自学或者参加培训班 只要你学好Python 钱自然会来 问题 兼职工作和Python可以用来赚钱吗 1兼职费用够杂项费用 生活费用 在学校我碰巧接管一些外包 嗯 足够的
  • 自定义MVC框架优化

    目录 一 前言 二 优化问题 1 子控制器的初始化配置问题 2 页面跳转优化代码冗余问题 3 优化参数封装问题 三 进行优化 1 解决子控制器初始化配置 2 解决页面跳转的代码冗余问题 3 解决优化参数封装问题 4 中央控制器 一 前言 在
  • 爬下artstation关注的画师信息制作为json文件

    原始网站 https www artstation com Author xiaozhu sai 本文章仅供学习交流 请勿交流梯子以及版权问题 1 爬取数据目的 后续处理 方便后续对各个画师作品的个人下载 个人练习 用户关注 的推荐算法与其
  • kali Linux笔记

    第一章 kali Linux简介 1 Linux操作系统的基础知识 Linux 的起源和发展 了解 Linux 操作系统的起源 发展和主要的发行版 如Linux 内核的诞生 GNU项目的贡献以及常见的 Linux 发行版如Ubuntu De
  • 旋转图像(二维数组的旋转)——LeetCode数组算法题

    旋转图像
  • 利用无人机(手机)和Unity3D软件制作自己的VR全景软件

    市面上做全景的网站和公司有很多 这里不列举了 自己百度一下VR和 全景之类的就会跳出很多 一 全景相片制作 1 无人机制作全景相片 1 1大疆无人机全景相片制作 利用软件DJI GO4中一键720全景完成制作 1 2手动制作720全景完成制
  • OBS斗鱼直播弹幕插件效果

    我没有安装任何OBS插件 一样达到了美化版弹幕效果
  • windows10配置远程桌面多用户同时登录

    目录 一 单用户同时登录 二 多用户同时登录 一 单用户同时登录 系统属性 gt 远程 勾选以下选项 运行 gpedit msc 选择 计算机配置 gt 管理模板 gt Windows组件 gt 远程桌面服务 gt 远程桌面会话主机 gt
  • 秒杀系统架构优化思路

    秒杀系统架构优化思路 上周参加Qcon 有个兄弟分享秒杀系统的优化 其观点有些赞同 大部分观点却并不同意 结合自己的经验 谈谈自己的一些看法 一 为什么难 秒杀系统难做的原因 库存只有一份 所有人会在集中的时间读和写这些数据 例如小米手机每
  • logback.xml文件未被加载

    起初 将logback xml放到了src下面 结果运行后发现只能在控制台输出日志 而不能将日志输出到文件中 于是网上搜索 首先将logback xml root标签中的ALL改为OFF 再次运行程序 看是否能够加载logback xml文
  • Katex的markdown常用语法中一些关于Latex数学符号或公式等的笔记

    文章目录 数学符号 设变量时常用的希腊字母 大小关系 分数 开方 同余 一般符号 二项式 符号上下添加额外信息 上标符号 上下标 上下划线 箭头 集合 省略号 矩阵 小括号形式 中括号形式 行列式 带省略号的形式 带横线或竖线分隔的形式 逻
  • CentOS7编译安装Openvswitch 2.3.0 LTS

    1 安装依赖包 yum y install openssl devel wget kernel devel 2 安装开发工具 yum groupinstall Development Tools 3 添加用户 adduser ovswitc
  • c++ sdk框架_鸿蒙系统中的 JS 开发框架

    今天鸿蒙终于发布了 开发者们也终于 沸腾 了 源码托管在国内知名开源平台码云上 https gitee com openharmony 我也第一时间下载了源码 研究了一个晚上 顺带写了一个 hello world 程序 还顺手给鸿蒙文档提了
  • Flutter lottie开机启动动画

    一款app在启动预加载数据时 少不了采用开机启动动画方案 今天介绍lottie制作开机启动动画 Lottie官网地址 https lottiefiles com 项目源码 Flutter手机端 lottie实现开机启动动画源码 前端元素由前
  • ps背景不变换字_ps怎么把背景上面的字换掉

    1 怎么用ps把图片上的字换掉 换成自己想打的字 1 演示使用的设计软件为photoshop 版本为Adobe photoshop CC2017 以下简称PS 2 打开图像处理软件PS 然后加载一张用于演示换字的图片 3 首先选择工具栏中的
  • 静态代码和动态代码的区别_静态代码扫描方法及工具介绍

    来自 信安之路 微信号 xazlsec 本文作者 国勇 信安之路特约作者 静态扫描就是不运行程序 通过扫描源代码的方式检查漏洞 常见的方法也有多种 如把源代码生成 AST 抽象语法树 后对 AST 进行分析 找出用户可控变量的使用过程是否流
  • 限制服务器显示ip段,限制网段中的某段IP访问Samba服务器

    今天在百度知道看到这样一个问题 linux samba服务如何限制指定网段访问 例如 允许192 168 1 0网段访问 但不允许192 168 1 100 192 168 1 170访问 这个问题与我之前的一篇文章 Samba访问控制 貌
  • Android开发edittext输入监听

    在开发Android的过程中 对于edittext的使用频率还是挺高的 比如用户账号密码的输入 基本信息的填写 数据的填入等 一般都会通过button点击事件对其数据进行提取 不过在一些场景 需要实时监听或者当输入完毕之后要马上获取用户所输
  • 逻辑回归和SVM的区别

    1 LR采用log损失 SVM采用合页损失 2 LR对异常值敏感 SVM对异常值不敏感 3 在训练集较小时 SVM较适用 而LR需要较多的样本 4 LR模型找到的那个超平面 是尽量让所有点都远离他 而SVM寻找的那个超平面 是只让最靠近中间
  • FFmpeg从入门到入魔(2):保存流到本地MP4

    1 FFmpeg裁剪移植 之前我们简单地讲解了下如何在Linux系统中编译FFmpeg 但是编译出来的so体积太大 而且得到的多个so不便于使用 本节在此基础上 将详细讲解在编译FFmpeg时如何对相关模块作裁剪以精简so的体积 并且编译只