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

Android集成IJKFFMPEG总结

Android集成IJKFFMPEG总结

目的:

Android端实现视频小窗预览功能。

方案对比

  • 硬解方案
    mediaExtrator + mediaCodec,速度快,但是不支持分片视频,且高profile H264 编码不支持。
  • 软解方案
    1、FFmpeg重写取帧逻辑,过程繁复需要手动读写文件,解封装等,效率低,并且没有扩展性
    2、复用FFmpeg命令行功能,打通调用路径,可以自由使用FFmpeg现有的能力。

实现方式:

Android端整理一套FFmpeg命令,传递给ffmpeg.c main 函数并执行。

实现基本步骤:

1、编写Android端FFmpeg命令

_**例如:**_

/**
     * screenshot from video, you could assign the specific time
     *
     * @param inputPath  input file
     * @param offset     which time you want to shot
     * @param outputPath output file
     * @return screenshot success or not
     */
    public static String[] shotFrame(String inputPath, float offset, String outputPath) {
        // ffmpeg -ss %f -i %s -f image2 -vframes 1 -an %s
        String screenShotCmd = "ffmpeg -ss %f -i -f image2 -vframes 1 -s 148*83 -an";
        screenShotCmd = String.format(Locale.getDefault(), screenShotCmd, offset);
        return insert(screenShotCmd.split(" "), 4, inputPath, outputPath);
    }

    /**
     * cut video, you could assign the startTime and duration which you want to
     *
     * @param inputPath  input file
     * @param startTime  startTime in the video(unit is second)
     * @param duration   duration
     * @param outputPath output file
     * @return cut video success or not
     */
    public static String[] cutVideo(String inputPath, float startTime, float duration, String outputPath) {
        // -map 0 -codec copy (copy all tracks)
        // -map 0:v -vcodec copy (copy track of video)
        // -map 0:a -acodec copy (copy all tracks of audio)
        // -map 0:s -scodec copy (copy all tracks of subtitle)
        // ffmpeg -ss %f -accurate_seek -t %f -i %s -map 0 -codec copy -avoid_negative_ts 1 %s
        String cutVideoCmd = "ffmpeg -ss %f -accurate_seek -t %f -i -map 0 -codec copy -avoid_negative_ts 1";
        cutVideoCmd = String.format(Locale.getDefault(), cutVideoCmd, startTime, duration);
        return insert(cutVideoCmd.split(" "), 7, inputPath, outputPath);
    }

    /**
     * add watermark with image to video, you could assign the location and bitRate
     *
     * @param inputPath  input file
     * @param imgPath    the path of the image
     * @param location   the location in the video(1:top left 2:top right 3:bottom left 4:bottom right)
     * @param bitRate    bitRate
     * @param offsetXY   the offset of x and y in the video
     * @param outputPath output file
     * @return add watermark success or not
     */
    public static String[] addWaterMarkImg(String inputPath, String imgPath, int location, int bitRate,
                                           int offsetXY, String outputPath) {
        String mBitRate = bitRate + "k";
        String overlay = obtainOverlay(offsetXY, offsetXY, location);
        String waterMarkCmd = "ffmpeg -i -i -b:v %s -filter_complex %s -preset:v superfast";
        waterMarkCmd = String.format(waterMarkCmd, mBitRate, overlay);
        return insert(waterMarkCmd.split(" "), 2, inputPath, 4, imgPath, outputPath);
    }


2、JNI方法调用

** Java端:**
    public static int execute(final String[] commands) {
        //call JNI interface to execute FFmpeg cmd
        return executeCmd(commands);
    }

    private native static int executeCmd(String[] commands);


 **Native端:函数的主要作用就是将 Java 端传递过来的 jobjectArray 类型的 FFmpeg 命令,
 转换为 main 函数所需要的参数 argc 和 argv ,然后调用执行。**

    JNIEXPORT jobject JNICALL
Java_com_v_video_ffmpegUtils_FFmpegCmd_executeCmd(JNIEnv *env, jobject thiz, jobjectArray commands)  {
    int argc = (*env)->GetArrayLength(env, commands);
    char **argv = (char **) malloc(argc * sizeof(char *));
    for (int i = 0; i < argc; i++) {
        jstring jstr = (jstring) (*env)->GetObjectArrayElement(env, commands, i);
        char *temp = (char *) (*env)->GetStringUTFChars(env, jstr, 0);
        argv[i] = malloc(INPUT_SIZE);
        strcpy(argv[i], temp);
        (*env)->ReleaseStringUTFChars(env, jstr, temp);
    }

    //execute ffmpeg cmd
    int result = main(argc, argv);

    //release memory
    for (int i = 0; i < argc; i++) {
        free(argv[i]);
    }
    free(argv);

    return result;
}

这里的main方法,调用至ffmpeg.c的命令行执行入口。这里需要添加ffmpeg.c 、ffmpeg.h 文件编译,除此以外,还需要增加ffmpeg.c调用所需的文件编译。需要从源码中拷贝 ffmpeg.h、ffmpeg.c、ffmpeg_opt.c、ffmpeg_filter.c、cmdutils.c、cmdutils.h 以及 cmdutils_common_opts.h 共 7 个文件到自己目录下。

并且添加至cMakeList中,编译出相应的so,在Java的Native入口类中加载即可实现调用。

add_library( # Sets the name of the library.
        mediahandle

        # Sets the library as a shared library.
        SHARED

        # Provides a relative path to your source file(s).
        ffmpeg/cmdutils.c
        ffmpeg/ffmpeg.c
        ffmpeg_cmd.c
        ffmpeg/ffmpeg_filter.c
        ffmpeg/ffmpeg_opt.c
        ffmpeg/cmdutils_common_opts.h
        )

        target_link_libraries( # Specifies the target library.
        mediahandle
        libijkffmpeg
        # Links the target library to the log library
        # included in the NDK.
        ${log-lib})

整体思路大致如上,过程中会遇到各种编译、各种native崩溃、包体大小等问题,逐个解决即可。

具体操作及问题处理

android编译mediahandle.so
  • 模块的 build.gradle中添加配置:

    android {
      compileSdkVersion project.compileSdkVersion
      buildToolsVersion project.buildToolsVersion
    
      defaultConfig {
          minSdkVersion project.minSdkVersion
          targetSdkVersion project.targetSdkVersion
          versionCode project.versionCode
          versionName project.versionName
         
          // 配置cmake构建参数
          externalNativeBuild {
              cmake {
                  abiFilters "arm64-v8a"
              }
          }
      }
     // 配置cmake构建脚本的路径
      externalNativeBuild {
          cmake {
              path "src/main/cpp/CMakeLists.txt"
              version "3.10.2"
          }
      }
    
  • 导入文件、编写CMakeLists.txt文件
    编译所需头文件
    avatar
    编译所需源码文件
    avatar

    编写CmakeLists.txt

     cmake_minimum_required(VERSION 3.4.1)
     # libijkffmpeg.so
     add_library(libijkffmpeg SHARED IMPORTED)
      set_target_properties(libijkffmpeg PROPERTIES IMPORTED_LOCATION ${PROJECT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libijkffmpeg.so)
    
     include_directories(./include)
    
    add_library( # Sets the name of the library.
          mediahandle
    
          # Sets the library as a shared library.
          SHARED
    
          # Provides a relative path to your source file(s).
          ffmpeg/cmdutils.c
          ffmpeg/ffmpeg.c
          ffmpeg_cmd.c
          ffmpeg/ffmpeg_filter.c
          ffmpeg/ffmpeg_opt.c
          ffmpeg/cmdutils_common_opts.h
          )
    
      find_library( # Sets the name of the path variable.
          log-lib
    
          # Specifies the name of the NDK library that
          # you want CMake to locate.
          log)
    
     target_link_libraries( # Specifies the target library.
          mediahandle
          libijkffmpeg
          # Links the target library to the log library
          # included in the NDK.
          ${log-lib})
    

FFmpeg命令运行分析-main函数

了解FFmpeg命令行源码,后续做定制修改
int main(int argc, char **argv)
{
    int i, ret;
    BenchmarkTimeStamps ti;	# 用于基准测试

    init_dynload();	# 初始化动态库加载路径,只在win32环境下调用SetDllDirectory(""),设定DLL加载路径为当前工作目录

    register_exit(ffmpeg_cleanup); 	# 注册清理函数,这时还没有调用ffmpeg_cleanup,在程序退出时才调用。
    								# 主要用于资源回收,比如释放过滤器、关闭输出/输入文件等。
    
    setvbuf(stderr,NULL,_IONBF,0); /* win32 runtime needs this */# 设置标准出错没有缓冲输出

    av_log_set_flags(AV_LOG_SKIP_REPEATED);	# 日志打印设置:跳过重复信息
    parse_loglevel(argc, argv, options);	# 从命令行参数及环境参数中获取打印级别,并设置

	# 如果ffmpeg命令后面紧跟着-d,将按照后台守护程序运行。
    if(argc>1 && !strcmp(argv[1], "-d")){
        run_as_daemon=1;
        av_log_set_callback(log_callback_null);
        argc--;
        argv++;
    }

#if CONFIG_AVDEVICE
    avdevice_register_all(); # 注册所有输入、输出设备
#endif
    avformat_network_init(); # 初始化网络

    show_banner(argc, argv, options);# 打印ffmpeg一些详细信息

	# 解析命令行参数,并打开所有输入、输出文件
    /* parse options and open all input/output files */
    ret = ffmpeg_parse_options(argc, argv);
    if (ret < 0)
        exit_program(1);# 如果打开失败,执行之前注册的ffmpeg_cleanup函数,最后调用exit函数退出

	# 如果没有输出、输入文件,打印提示信息,然后退出
    if (nb_output_files <= 0 && nb_input_files == 0) {
        show_usage();
        av_log(NULL, AV_LOG_WARNING, "Use -h to get full help or, even better, run 'man %s'\n", program_name);
        exit_program(1);
    }

	# 至少要有一个输出文件
    /* file converter / grab */
    if (nb_output_files <= 0) {
        av_log(NULL, AV_LOG_FATAL, "At least one output file must be specified\n");
        exit_program(1);
    }
	
	# 如果输出文件中有rtp,默认不输出sdp(为什么呢???)
	for (i = 0; i < nb_output_files; i++) {
        if (strcmp(output_files[i]->ctx->oformat->name, "rtp"))
            want_sdp = 0;
    }

	# 执行转换任务前,记录时间戳
    current_time = ti = get_benchmark_time_stamps();
	
	# 转换任务主函数
    if (transcode() < 0)
        exit_program(1);	# 转换错误,执行清理函数ffmpeg_cleanup,最后调用exit函数退出
	
	# 如果在命令行参数中有“benchmark”,执行基准测试,打印执行的时间信息
    if (do_benchmark) {
        int64_t utime, stime, rtime;
        current_time = get_benchmark_time_stamps();
        utime = current_time.user_usec - ti.user_usec;
        stime = current_time.sys_usec  - ti.sys_usec;
        rtime = current_time.real_usec - ti.real_usec;
        av_log(NULL, AV_LOG_INFO,
               "bench: utime=%0.3fs stime=%0.3fs rtime=%0.3fs\n",
               utime / 1000000.0, stime / 1000000.0, rtime / 1000000.0);
    }
    av_log(NULL, AV_LOG_DEBUG, "%"PRIu64" frames successfully decoded, %"PRIu64" decoding errors\n",
           decode_error_stat[0], decode_error_stat[1]);
	
	# 计算解码成功的比例,如果小于设置的最大错误率(默认是2.0/3)时,报错退出
    if ((decode_error_stat[0] + decode_error_stat[1]) * max_error_rate < decode_error_stat[1])
        exit_program(69);

	#文件转换成功,执行清理函数ffmpeg_cleanup,最后调用exit函数退出
    exit_program(received_nb_signals ? 255 : main_return_code);
    return main_return_code;
}
定制修改
//在 ffmpeg_cleanup函数执行结束前重新初始化:
static void ffmpeg_cleanup(int ret) {

  // 省略其他代码...

  nb_filtergraphs = 0;
  nb_output_files = 0;
  nb_output_streams = 0;
  nb_input_files = 0;
  nb_input_streams = 0;
}

// cmdutils.c 修改 exit_program函数:
void exit_program(int ret) {
  if (program_exit)
      program_exit(ret);
      exit(ret);
}

//改成:防止命令运行完,退出进程,导致应用崩溃
int exit_program(int ret)
{
 return ret;
}

// 修改ffmpeg_opt.c,注释掉设备相关代码,以及文件重复检查机制
static int opt_init_hw_device(void *optctx, const char *opt, const char *arg)
{
  if (!strcmp(arg, "list")) {
      enum AVHWDeviceType type = AV_HWDEVICE_TYPE_NONE;
      printf("Supported hardware device types:\n");
      while ((type = av_hwdevice_iterate_types(type)) !=
             AV_HWDEVICE_TYPE_NONE)
          printf("%s\n", av_hwdevice_get_type_name(type));
      printf("\n");
      exit_program(0);
  } else {
      // 1
      return 1;//hw_device_init_from_string(arg, NULL);
  }
}

static int opt_filter_hw_device(void *optctx, const char *opt, const char *arg)
{
  if (filter_hw_device) {
      av_log(NULL, AV_LOG_ERROR, "Only one filter device can be used.\n");
      return AVERROR(EINVAL);
  }
  // 2
  filter_hw_device = NULL;//hw_device_get_by_name(arg);
  if (!filter_hw_device) {
      av_log(NULL, AV_LOG_ERROR, "Invalid filter device %s.\n", arg);
      return AVERROR(EINVAL);
  }
  return 0;
}
// 文件重复检查机制修改
static void assert_file_overwrite(const char *filename)
{
  if (file_overwrite && no_file_overwrite) {
      fprintf(stderr, "Error, both -y and -n supplied. Exiting.\n");
      exit_program(1);
  }

  if (!file_overwrite) {
      const char *proto_name = avio_find_protocol_name(filename);
      if (proto_name && !strcmp(proto_name, "file") && avio_check(filename, 0) == 0) {
          if (stdin_interaction && !no_file_overwrite) {
              fprintf(stderr,"File '%s' already exists. Overwrite ? [y/N] ", filename);
              fflush(stderr);
              term_exit();
              signal(SIGINT, SIG_DFL);
              //  prevent exit due to not overwriting
              //if (!read_yesno()) {
              //    av_log(NULL, AV_LOG_FATAL, "Not overwriting - exiting\n");
              //    exit_program(1);
              //}

              term_init();
          }
          else {
              av_log(NULL, AV_LOG_FATAL, "File '%s' already exists. Exiting.\n", filename);
              // prevent exit
              //exit_program(1);
          }
      }
  }
}
取帧速度优化
  ffmpeg -i /home/下载/下载.mp4 -f image2 -ss 1000 -vframes 1 -s 148*83 -an /home/nj/2222/14.jpg
=> 改成 先seek再打开文件 取帧时间从10s级缩短至120ms
ffmpeg -ss 1000 -i /home/下载/下载.mp4 -f image2 -vframes 1 -s 148*83 -an /home/nj/2222/31.jpg
包体大小裁剪

方式:有限打开编解码器 文件config/module-lite.sh

   export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-encoders"
   export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-encoder=mjpeg"
   export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-muxer=image2"
   export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-filters"
   export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-filter=trim"
   export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-filter=scale"
   export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-filter=format"
  export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-filter=null"

根据编译报错打开相应开关,并且找到filter获取的源码,添加日志,打印出当前任务所需的所有编码器,解码器,滤镜等,打开开关即可
如:找到ffmpeg_filter.c avfilter_get_by_name方法,在while循环中打印出即可。

const AVFilter *avfilter_get_by_name(const char *name)
{
  const AVFilter *f = NULL;
  void *opaque = 0;

  if (!name)
      return NULL;

  while ((f = av_filter_iterate(&opaque)))
      if (!strcmp(f->name, name))
          return (AVFilter *)f;

  return NULL;
}

其他参照上述操作执行即可

视频末尾取帧失败问题

问题描述:视频取帧存在一些视频末尾帧无法取出问题。
问题视频链接:

https://apd-7e140ef9b89c86a1c10127e889334fee.v.smtcdns.com/omts.tc.qq.com/AbEUJiVdZBPsIB7TwsRaWJgru6vs07l30hJy7qxBDQD4/uwMROfz2r57BIaQXGdGnC2deOm7WRbkbfdWCxMUsemsF2Gfz/svp_50001/fkTItM-64nAykdNRmEZ5CWH2zdFnyKUPeMBcFATJR0X2fyUglOenoeHDY-WPB6E8MF8fLgol4nsTEEjHo3WYrywAqEGj-Oui04252-0xVh7XqNonCXej3S8kCc2vyI2ZCxe_xQ3C8zAkhrvTaGrYV9X3WRsphA_tL0Mt177xeLLLkRnriSYurg/szg_9332_50001_0bc3juacoaaauiaglhapifrvctode5gqaj2a.f304110.ts.m3u8?ver=4

问题分析:
通过本地使用FFmpeg命令行,报错:Output file is empty, nothing was encoded (check -ss / -t / -frames parameters if used)

查看文档:

Output file is empty, nothing was encoded (check -ss / -t / -frames parameters if used) The -ss option allows you to skip to a certain point. This message is often seen when the -ss option value is greater than the duration of the input. For example, if -ss 30 is used for a 15 second input you may see this message.

根据文档,怀疑是视频的时长传入超过了实际的视频时间导致

 ```c{.line-numbers}
   #EXTINF:9.560,
   /storage/emulated/0/下载/.video_cache_3/fb86c6a2ca616c834cf55bbccb3fd475/segments/ts_50.ts
   #EXTINF:8.800,
   /storage/emulated/0/下载/.video_cache_3/fb86c6a2ca616c834cf55bbccb3fd475/segments/ts_51.ts
   #EXTINF:9.560,
  /storage/emulated/0/下载/.video_cache_3/fb86c6a2ca616c834cf55bbccb3fd475/segments/ts_52.ts
  #EXTINF:13.877,
  /storage/emulated/0/下载/.video_cache_3/fb86c6a2ca616c834cf55bbccb3fd475/segments/ts_53.ts
  #EXT-X-ENDLIST
 ```

分析该链接m3u8,发现该视频正常的ts片段9s左右,而最后一片ts标注的时间是13.877s。明显超出正常片段数值5秒左右。因此取值传入至最后一秒时间,帧报该错误。

验证:

将该视频格式转换成mp4,视频最后一帧可以正常取出。

对比转换后的mp4视频最后一帧,与m3u8视频的倒数3秒的视频帧一致。

因此可以得出结论:m3u8标注的时长超过视频视频帧实际长度,导致ffmpeg传入的取帧时间超过视频时长,从而报错Output file is empty, nothing was encoded (check -ss / -t / -frames parameters if used),无法正常取帧。

参考文章:

https://blog.lxfpoe.work/jekyll/update/2020/05/18/ffmpeg-android.html
https://yhaolpz.github.io/ddeb5551/
https://github.com/jdpxiaoming/FFmpegTools

相关文章:

  • 【二叉树】最长同值路径
  • 使用缓冲区提高并发
  • Windows10环境下Python 开发环境搭建
  • JavaEE TCP协议
  • 51单片机DS18B20温度报警器proteus仿真设计_可调上下限
  • SSRF漏洞
  • 猿创征文|平凡的应届生四年学习之路
  • mysql8忘记密码如何重置(禅道的mysqlzt服务和mysql服务冲突)
  • Nginx 配置 SSL(HTTPS)
  • 用css实现简单的动画——“奔跑的小子”(有知识梳理和图片)
  • macbook m1芯片 实现vscode下debug(解决无法读入的问题)
  • 前端:下载文件(多种方法)
  • 猿创征文|【JavaSE】 Collection集合全家桶
  • 【Coppeliasim+Add-on】附加组件-喷涂路径自动生成及喷涂仿真
  • 简易下载并使用Jupyter(Anaconda)
  • 【编码】-360实习笔试编程题(二)-2016.03.29
  • 【前端学习】-粗谈选择器
  • Angular 响应式表单 基础例子
  • Angular6错误 Service: No provider for Renderer2
  • AngularJS指令开发(1)——参数详解
  • css选择器
  • Fundebug计费标准解释:事件数是如何定义的?
  • iOS动画编程-View动画[ 1 ] 基础View动画
  • javascript从右向左截取指定位数字符的3种方法
  • JS正则表达式精简教程(JavaScript RegExp 对象)
  • node和express搭建代理服务器(源码)
  • php ci框架整合银盛支付
  • Sass 快速入门教程
  • vue-router 实现分析
  • 官方解决所有 npm 全局安装权限问题
  • 实现菜单下拉伸展折叠效果demo
  • 通过git安装npm私有模块
  • 微信小程序--------语音识别(前端自己也能玩)
  • 消息队列系列二(IOT中消息队列的应用)
  • 关于Kubernetes Dashboard漏洞CVE-2018-18264的修复公告
  • #Linux(make工具和makefile文件以及makefile语法)
  • (办公)springboot配置aop处理请求.
  • (备忘)Java Map 遍历
  • (附源码)springboot“微印象”在线打印预约系统 毕业设计 061642
  • (九十四)函数和二维数组
  • (一)kafka实战——kafka源码编译启动
  • .net oracle 连接超时_Mysql连接数据库异常汇总【必收藏】
  • .NET平台开源项目速览(15)文档数据库RavenDB-介绍与初体验
  • :O)修改linux硬件时间
  • [ 2222 ]http://e.eqxiu.com/s/wJMf15Ku
  • [ Algorithm ] N次方算法 N Square 动态规划解决
  • [CareerCup] 6.1 Find Heavy Bottle 寻找重瓶子
  • [CLR via C#]11. 事件
  • [CSS] - 修正IE6不支持position:fixed的bug
  • [ES-5.6.12] x-pack ssl
  • [HTML API]HTMLCollection
  • [JavaWeb玩耍日记]Maven的安装与使用
  • [leetcode] Balanced Binary Tree
  • [NCTF 2022] web题解
  • [nlp] grad norm先降后升再降