欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-07-13 12:52:44
...

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类的头文件,在终端执行:

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

生成出来的头文件叫 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录屏并利用FFmpeg转换成gif(三) 在Android中使用ffmpeg命令

其中,红框内的几个文件是从源码中拷贝过来的,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:[email protected]

微信:724360018