Android录屏并利用FFmpeg转换成gif(三) 在Android中使用ffmpeg命令

Android录屏并利用FFmpeg转换成gif(三)

写博客时经常会希望用一段动画来演示app的行为,目前大多数的做法是在电脑上开模拟器,然后用gif录制软件录制模拟器屏幕,对于非开发人员来讲这种方式还是比较困难的。本来我以为应该也有能直接在手机上录屏并生成gif文件这样的app,下载一个这样的APP来录gif要方便得多。结果发现目前几乎没有此类APP,我就想能不能自己写一个,然后查了查资料,感觉应该能做出来,于是就撸起袖子干起来了。总的来讲要实现这个功能可以分成两个部分(当然,如果有更好的实现方式欢迎大家提出来,谢谢!):

  1. 录屏,生成mp4文件
  2. 利用ffmpeg开源软件将mp4转换成gif

第一点比较容易实现,已有现成的开源代码供参考。难点在第二点,涉及到NDK开发相关的知识,及FFmpeg的集成,这方面知识我之前从未接触过,还是比较有挑战性的。

功能虽然很简单,但要讲解起来感觉还是要费点篇幅的,所以我分成了4篇文章来介绍,分别是:

  1. Android录屏并利用FFmpeg转换成gif(一) 录屏,讲讲怎样录屏生成mp4文件

  2. Android录屏并利用FFmpeg转换成gif(二) 交叉编译FFmpeg源码,说说如何根据我们的需求裁剪FFmepg并编译出可在android下运行的so包

  3. Android录屏并利用FFmpeg转换成gif(三) 在Android中使用ffmpeg命令,说说如何在Android中使用ffmpeg命令,简化C代码的编写难度

  4. Android录屏并利用FFmpeg转换成gif(四) 将mp4文件转换成gif文件,将2、3两步生成的so文件集成到android工程中,实现将mp4文件转换成gif文件,完成最终的工程。

本篇介绍如何将已经交叉编译好的FFmpeg相关的so包集成到app中来,至于so包是怎么编译的,请参看 Android录屏并利用FFmpeg转换成gif(二) 交叉编译FFmpeg源码。so包的集成流程也简单,遵循一般的NDK开发流程就可以了,不过说起来简单,做起来还是有不少细节要注意的,我就在集成的时候搞了很久才搞成功。大概有以下几个步骤:

  1. 把交叉编译FFmpeg源码生成的7个so文件拷过来

  2. 写一个带native方法的java类,作为调c代码的接口,并在该方法中传入字符串数组类型的ffmpeg命令。

  3. 写一个实现native方法的C类,在该类里调FFmpeg.c类中的main方法,并将从java传入的ffmpeg命令传给main方法,从而达到执行ffmppeg命令的目的。其中FFmpeg.c这个类是从ffmpeg源码中拷过来的,还关联到几个其它的c文件及头文件,都是要从ffmpeg源码中拷过来,就是这些文件在编译的时候老是报错花了我很多时间。

  4. 再就是写一个CMakeLists.txt文件,用来规定cmake如何进行编译。里面的内容主要包括指定引用的so包的路径,头文件的路径,要编译的源文件的路径等等

  5. 最后就是在app目录下的build.gradle文件中对NDK编译做点配置,等下会详细说

大概就这么个流程吧,顺利的话做完这些后就能把工程跑起来了,但是,但是,但是一般都没那么顺利的,嗷。。。

下面就按照以上几个步骤详细地挨个介绍一下,并将我遇到问题的地方指出来,避免再走弯路。

2.1 拷贝FFmpeg相关的so包

在main目录下新建jniLibs/armeabi-v7a目录,然后将

libavutil-55.so

libavcodec-57.so

libavformat-57.so

libavdevice-57.so

libswresample-2.so

libswscale-4.so

libavfilter-6.so

七个包拷贝进来。

注意:这几个包就是在 Android录屏并利用FFmpeg转换成gif(二) 交叉编译FFmpeg源码 一文中准备好的。

2.2 写一个带native方法的java类

这个就非常简单,在java目录下新建一个java类,文件名随便取,一般为了方便识别会带有“jni”字样,我这里的文件名叫“FFmpegJni.java”,再编写一个带native的方法。代码如下:

package com.example.hm.gifrecoder;

import android.text.TextUtils;
import android.util.Log;

/**
 * 执行ffmpeg命令行的类
 *
 * @author hm  17-12-11
 */
public class FFmpegJni {
    private static final String TAG = "FFmpegJni";

    /**
     * 执行ffmpeg的本地方法
     *
     * @param commands ffmpeg命令行
     * @return
     */
    public native int exctFFmpeg(String[] commands);
}

其中代表ffmpeg命令的参数是个字符串数组。然后生成这个java类的头文件,在终端执行:

生成出来的头文件叫 com_example_hm_gifrecoder_FFmpegJni.h

这里要注意一下,这个java类是带包名的,要在com的一上层目录即java目录下执行javah命令,然后java文件要用全名,即"com.example.hm.gifrecoder.FFmpegJni",不然的话老是会报找不到文件之类的错误。

好,接下来写native代码。

2.3 写一个实现native方法的C类

这部分内容比较多,而且有些地方需要修改代码,大部分都参考了Android 集成 FFmpeg (二) 以命令方式调用 FFmpeg 这篇文章,感谢作者的分享。

前面说到生成了FFmpegJni.java的头文件,我们要利用这个头文件来写c文件。先在main目录下新建一个目录myjni,这个目录用来编译c文件,等编译完了就不要了,只要编译生成的so文件就可以了,所以这个目录一般来讲不要放在java目录下,当然这个不是强制性的,只要你愿意,放到任何地方都可以。再在myjni目录下建一个outputlibs目录用来放编译出来的so文件,其实这个目录并不是必需的,android studio会自动把编译好的so文件存放在app/build/intermediates/cmake/debug/obj/armeabi等几个目录下,如果不注意的话我们不会感觉到它的存在,所以特意建一个目录来存放编译结果,增加一点成就感。嗯,貌似说了很多废话,还是回过头来说写c文件的事,我们说的这个c文件就是要实现java中的native方法的c文件,把com_example_hm_gifrecoder_FFmpegJni.h头文件拷到myjni目录下来,重命名一下,取名为“FFmpegExcutor.c",然后打开文件,实现里面的Java_com_example_hm_gifrecoder_FFmpegJni_exctFFmpeg方法。

实现之前的代码:

JNIEXPORT jint JNICALL Java_com_example_hm_gifrecoder_FFmpegJni_exctFFmpeg(JNIEnv * env, jobject obj, jobjectArray commands);

实现之后的代码:

JNIEXPORT jint JNICALL Java_com_example_hm_gifrecoder_FFmpegJni_exctFFmpeg
  (JNIEnv * env, jobject obj, jobjectArray commands){
      LOGD("----------进入FFmpegExcutor---------");
      int argc = (*env)->GetArrayLength(env, commands);
      char *argv[argc];
      int i;
      for (i = 0; i < argc; i++) {
          jstring js = (jstring) (*env)->GetObjectArrayElement(env, commands, i);
          argv[i] = (char*) (*env)->GetStringUTFChars(env, js, 0);
      }
      LOGD("----------begin excute ffmpeg---------");
      return main(argc, argv);
  }

这里面仅仅打印了两行日志,然后将java传过来的参数(一个字符串数组)转换成c语言里的字符串数组,最后调用ffmpeg.c的main方法来执行ffmpeg命令。这个ffmpeg.c是在FFmpeg源码中的,不是我们自己写的,所以要把这个文件从源码中拷过来。由于ffmpeg.c还有一些依赖,仅仅拷这一个文件还不够,还要其它几个文件,拷完后最终有以下几个文件:

其中,红框内的几个文件是从源码中拷贝过来的,android_log.h是自己写的,用来将c代码中的日志从logcat中输出来,有助于跟踪c代码的运行情况。

注意:

具体要从源码中拷哪几个文件过来是个很让人头疼的事,至少对c语言不熟的人来说是这样的。我就在这里摸了很久,一度让我有放弃的想法。目前的这几个文件也不一定是最精简的,可能会有的文件是多余的。这个结果一方面是参考网上的资料,一方面是通过编译时的报错多次调整之后才形成的。

再一个就是FFmpeg源码版本不一样,所需要拷过来的文件也不一样,比如上面说的那篇参考文章中使用的源码文件就与我用的不一样,他用的是ffmpeg3.3.3版本,我用的是ffmpeg4.1版本,所以如果对c不熟悉的话,换个版本的源码就能让你歇菜。


2.3.1 源码修改———输出FFmpeg内部日志到Android Logcat

在执行命令过程中,FFmpeg 内部的日志系统会输出很多有用的信息,但是在 Android 的 logcat 中是看不到的,为了跟踪ffmpeg的内部日志,以便出现错误时更好地定位问题,有必要将内部日志转到logcat中输出,这就需要修改源码。

首先是新建上面提到的android_log.h文件,其内容如下:

#ifdef ANDROID
#include <android/log.h>
#ifndef LOG_TAG
#define  MY_TAG   "MYTAG"
#define  AV_TAG   "AVLOG"
#endif
#define LOGE(format, ...)  __android_log_print(ANDROID_LOG_ERROR, MY_TAG, format, ##__VA_ARGS__)
#define LOGD(format, ...)  __android_log_print(ANDROID_LOG_DEBUG,  MY_TAG, format, ##__VA_ARGS__)
#define XLOGD(...)  __android_log_print(ANDROID_LOG_INFO,AV_TAG,__VA_ARGS__)
#define XLOGE(...)  __android_log_print(ANDROID_LOG_ERROR,AV_TAG,__VA_ARGS__)
#else
#define LOGE(format, ...)  printf(MY_TAG format "\n", ##__VA_ARGS__)
#define LOGD(format, ...)  printf(MY_TAG format "\n", ##__VA_ARGS__)
#define XLOGE(format, ...)  fprintf(stdout, AV_TAG ": " format "\n", ##__VA_ARGS__)
#define XLOGI(format, ...)  fprintf(stderr, AV_TAG ": " format "\n", ##__VA_ARGS__)
#endif

文件中定义了几个输出日志的方法 LOGD,LOGE,XLOGD,XLOGE,并根据是否在android中运行在自定的方法中使用android的日志系统输出日志或使用C语言的日志输出系统输出日志,然后在ffmpeg.c文件中修改几个地方,调用这几个自定义方法。

具体修改有三个地方:

一是导入android_log.h文件

#include "android_log.h"

二是修改 log_callback_null 方法:(原方法为空)

static void log_callback_null(void *ptr, int level, const char *fmt, va_list vl)
{
    static int print_prefix = 1;
    static int count;
    static char prev[1024];
    char line[1024];
    static int is_atty;
    av_log_format_line(ptr, level, fmt, vl, line, sizeof(line), &print_prefix);
    strcpy(prev, line);
    if (level <= AV_LOG_WARNING){
        XLOGE("%s", line);
    }else{
        XLOGD("%s", line);
    }
}

三是设置日志回调方法为 log_callback_null:(main 函数开始处)

int main(int argc, char **argv)
{
    av_log_set_callback(log_callback_null);
    int i, ret;
    ......
}

注意:以上三处都是修改 ffmpeg.c 文件


2.3.2 源码修改———执行命令后清除数据(修改 ffmpeg.c)

由于 Android 端执行一条 FFmpeg 命令后并不需要结束进程,所以需要初始化相关变量,否则执行下一条命令时就会崩溃。首先找到 ffmpeg.c 的 ffmpeg_cleanup 方法,在该方法的末尾添加以下代码:

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

然后在 main 函数的最后调用 ffmpeg_cleanup 方法,如下:

int main(int argc, char **argv)
{
    ......
	ffmpeg_cleanup(0);
	return main_return_code;
}

2.3.3 源码修改———执行结束后不结束进程(修改 cmdutils.c、cmdutils.h)

FFmpeg 在执行过程中出现异常或执行结束后会自动销毁进程,而我们在 Android 中调用时,只想让它作为一个普通的方法,不需要销毁进程,只需要正常返回就可以了,这就需要修改 cmdutils.c 中的 exit_program 方法,源码中为:

void exit_program(int ret)
{
    if (program_exit)
        program_exit(ret);

    exit(ret);
}

修改为:

int exit_program(int ret)
{
   return ret;
}

此处修改了方法的返回值类型,所以还需要修改对应头文件中的方法声明,即将 cmdutils.h 中的:

void exit_program(int ret) av_noreturn;

修改为:

int exit_program(int ret);

到这里为止所有需要修改的源码都已修改完毕,其中输出日志那部分不是必须的,不过很有意义,推荐使用。

2.4 编写CMakeLists.txt文件

在myjni目录下新建CMakeLists.txt文件,注意,这个文件名是不能随便取的,必须是“CMakeLists.txt”,这里面的内容是用来规定cmake如何编译的,可以很简单也可以很复杂,看需要而定,具体怎么编写要学习一下cmake的语法,我也不是特别懂。现在把本项目中的CMakeLists.txt文件内容贴出来,然后稍微解释一下。

cmake_minimum_required(VERSION 3.4.1)
set(CMAKE_VERBOSE_MAKEFILE on)

set(LINK_DIR ${CMAKE_CURRENT_LIST_DIR}/../jniLibs/armeabi-v7a)
link_directories(${LINK_DIR})

set(INCLUDE_DIR "/home/hm/ffmpeg/ffmpeg_source/ffmpeg-3.4.1")
#头文件目录设置为ffmpeg-3.4.1源码目录及CMakeLists.txt文件所在目录
#也可以不显式指定CMakeLists.txt文件所在目录,该目录默认包含在搜索目录中
include_directories(${CMAKE_CURRENT_LIST_DIR} ${INCLUDE_DIR})

#指定生成的so包的输出路径,注意此语句要在add_library语句之前,否则不能生效
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/outputlibs)

# 查找当前目录下的所有源文件
# 并将名称保存到 SOURCE_DIR 变量
aux_source_directory(${CMAKE_CURRENT_LIST_DIR} SOURCE_DIR)

add_library(ffmpeg SHARED
            ${SOURCE_DIR})

target_link_libraries(ffmpeg
                      avcodec-57
                      avdevice-57
                      avfilter-6
                      avformat-57
                      avutil-55
                      swresample-2
                      swscale-4
                      log)

第一行指定最低的cmake版本;

第二行规定在编译的时候列出各步骤实际调用的命令、参数,使我们能看到详细编译过程,默认只会显示一个进度。

第三、第四行设置了链接文件所在的目录为/jniLibs/armeabi-v7a的绝对路径,也就是对FFmpeg源码进行交叉编译出来的7个so文件所在的目录

第五、第八行已经有注释了,这里要提醒一下就是头文件目录一定要包含FFmpeg源码目录,因为我们从源码中拷进来的那些文件都要依赖FFmpeg源码中的头文件的。

第十四行,add_library语句,生成一个共享库文件,名字叫“ffmpeg”,实际输出的包名为“libffmpeg.so",前缀和后缀是cmake自动添加上去的,编译所用到的资源在SOURCE_DIR变量代表的目录里,这里代表了myjni目录。

最后一行,指定在生成ffmpeg共享库时要连接的其它库,我理解就是生成libffmpeg.so时要依赖的其它库,这里共有8个,前七个是FFmpeg相关的库,最后一个log库是NDK里面的日志库。

好,CMakeLists.txt文件的内容就全部介绍完了,接下来配置build.gradle文件。

2.5 配置build.gradle文件

在android节点下配置ndk及cmake相关的信息

android {
    compileSdkVersion 26
    buildToolsVersion "26.0.3"
    defaultConfig {
    ......

//        编译libffmpeg.so所需要的配置,编译完成后即可注释掉
          ndk{
             abiFilters 'armeabi-v7a'
          }
          externalNativeBuild{
              cmake{
                  arguments '-DANDROID_PLATFORM=android-21',
                          '-DANDROID_TOOLCHAIN=gcc', '-DANDROID_STL=gnustl_static'
              }
          }
    }

//    编译libffmpeg.so所需要的配置,编译完成后即可注释掉
      externalNativeBuild {
          cmake {
              path "src/main/myjni/CMakeLists.txt"
          }
      }
}

第一个是过滤cpu架构,我只选了一个常用的。

第二个是配置CMake的编译选项,这里有个地方要特别注意,’-DANDROID_TOOLCHAIN’一定要指定为“ gcc”,不能使用clang,不然会报错。

最后一个指定CMakeLists.txt路径。

改完之后同步一下,再运行,应该可以在myjni/outputlibs目录下看到libffmpeg.so文件了,如果输出了so文件,则将这个文件拷到jniLibs/armeabi-v7a目录下,然后在java中调native方法。

费了这么大劲就是为了这个libffmpeg.so文件,其实编译这个文件也可以不在android工程中进行,因为实际上我们的工程只要引用这几个so包而已,至于怎么编译这些so包并不在本工程的职责范围内。要在工程外编译请参考在命令行下用cmake交叉编译可在android中运行的so包cmake使用独立工具链交叉编译可在android中运行的so包

###最后,上源码:
https://github.com/MingHuang1024/GifRecorder

  • 注意:源码是完整的源码,即实现了从录屏到转换成gif的全部功能的,不仅仅是本文所讲到的源码。


由于水平有限,如果文中存在错误之处,请大家批评指正,欢迎大家一起来分享、探讨!

博客:http://blog.csdn.net/MingHuang2017

GitHub:https://github.com/MingHuang1024

Email:minghuang1024@foxmail.com

微信:724360018

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值