Android音视频开发笔记(二)--ffmpeg命令行的使用&相机预览

在上一篇文章中,咱们介绍了一些音视频的基础知识,而且编译了Android平台的ffmpeg。那么在这篇文章中,咱们将介绍如何将咱们编译好的ffmpeg库接入到咱们的Android项目中,并介绍移植ffmpeg强大的命令行工具到Android App里。另外咱们会介绍如何使用OpenGL ES来渲染咱们相机的实时预览画面。闲话少说,上干货java

建立项目

  1. 第一步,咱们打开咱们熟悉的Android Studio(2.2版本后,Android Studio支持了CMake的方式来管理咱们的c/c++代码)。

    首先咱们须要肯定NDK的版本,尽可能和ffmpeg编译时使用的版本一致c++

与建立其余 Android Studio 项目相似,不过还须要额外几个步骤git

(1).在向导的 Configure your new project 部分,选中 Include C++ Support 复选框。
(2).点击 Next。
(3).正常填写全部其余字段并完成向导接下来的几个部分
(4).在向导的 Customize C++ Support 部分,您可使用下列选项自定义项目: 
    1). C++ Standard:使用下拉列表选择您但愿使用哪一种 C++ 标准。选择 Toolchain Default 会使用默认的 CMake 设置。
    2). Exceptions Support:若是您但愿启用对 C++ 异常处理的支持,请选中此复选框。若是启用此复选框,Android Studio 会将 -fexceptions 标志添加到模块级 build.gradle 文件的 cppFlags 中,Gradle 会将其传递到 CMake。
    3). Runtime Type Information Support:若是您但愿支持 RTTI,请选中此复选框。若是启用此复选框,Android Studio 会将 -frtti 标志添加到模块级 build.gradle 文件的 cppFlags 中,Gradle 会将其传递到 CMake。
(5). 点击finish
复制代码

在点击完成后,咱们会发现Android视图中会多出两块github

在cpp目录下,Android Studio为咱们自动生成了一个native-lib.cpp文件,至关于一个hello wrold。 这里咱们主要看一下CMakeList.txt文件里的内容。咱们这里只作一下简单的介绍。数组

在build.gradle文件中也有一些变化架构

CMakeList.txt的文件路径

移植编译好的libffmpeg.so到项目中

  1. 指定编译的cpu架构app

    咱们打开module下的build.gradle目录,在defaultConfig节点下添加:ide

    ndk {
         abiFilters 'armeabi-v7a'
     }
    复制代码

    由于目前绝大多数Android设备都是使用arm架构,极少有使用x86架构的,因此咱们这里直接屏蔽x86。因为arm64-v8a是向下兼容的,因此咱们只需指定armeabi-v7a便可函数

  2. 拷贝相应源文件工具

    接下来咱们在cpp目录下建立一个thirdparty文件夹,而后在thirdparty目录下建立ffmpeg目录,将咱们编译好的头文件拷贝进来,以后再在thirdparty目录下建立prebuilt文件夹,在此目录下,建立一个armeabi-v7a目录,将咱们编译出的libffmpeg.so拷贝进来。 完整目录结构以下:

  3. cmake的配置

    在CMakeList.txt中是能够指定文件路径的,就是定义指定文件路径做为变量。我的认为,jni的相关代码最好和核心代码分开的好,因此咱们在src/main/目录下建立一个jni文件夹,在这个里面专门存放咱们的jni代码(不知道jni是什么的朋友,这系列的文章可能不太适合你,能够先去自行补课)。

    cmake_minimum_required(VERSION 3.4.1)
     #指定核心业务源码路径
     set(PATH_TO_VIDEOSTUDIO ${CMAKE_SOURCE_DIR}/src/main/cpp)
     #指定jni相关代码源码路径
     set(PATH_TO_JNI_LAYER ${CMAKE_SOURCE_DIR}/src/main/jni)
     #指定第三方库头文件路径
     set(PATH_TO_THIRDPARTY ${PATH_TO_VIDEOSTUDIO}/thirdparty)
     #指定第三方库文件路径
     set(PATH_TO_PRE_BUILT ${PATH_TO_THIRDPARTY}/prebuilt/${ANDROID_ABI})
    复制代码

    其中CMAKE_SOURCE_DIR是内置变量,指的是CMakeList.txt所在目录;ANDROID_ABI也是内置变量,对应咱们gradle中配置的cpu架构。

  4. 调用ffmpeg

    在jni目录下建立一个VideoStudio.cpp的c++源文件(也能够随本身的喜爱来起源文件名称)。内容以下:

    #include <cstdlib>
     #include <cstring>
     #include <jni.h>
     #ifdef __cplusplus
     extern "C" {
     #endif
     #include "libavformat/avformat.h"
     #include "libavcodec/avcodec.h"
     #ifdef __cplusplus
     }
     #endif
     
     // java文件对应的全类名
     #define JNI_REG_CLASS "com/xxxx/xxxx/VideoStudio"
     
     JNIEXPORT jstring JNICALL showFFmpegInfo(JNIEnv *env, jobject) {
         char *info = (char *) malloc(40000);
         memset(info, 0, 40000);
         av_register_all();
         AVCodec *c_temp = av_codec_next(NULL);
         while (c_temp != NULL) {
             if (c_temp->decode != NULL) {
                 strcat(info, "[Decoder]");
             } else {
                 strcat(info, "[Encoder]");
             }
             switch (c_temp->type) {
                 case AVMEDIA_TYPE_VIDEO:
                     strcat(info, "[Video]");
                     break;
                 case AVMEDIA_TYPE_AUDIO:
                     strcat(info, "[Audio]");
                     break;
                 default:
                     strcat(info, "[Other]");
                     break;
             }
             sprintf(info, "%s %10s\n", info, c_temp->name);
             c_temp = c_temp->next;
         }
         puts(info);
         jstring result = env->NewStringUTF(info);
         free(info);
         return result;
     }
    
     const JNINativeMethod g_methods[] = {
             "showFFmpegInfo", "()Ljava/lang/String;", (void *) showFFmpegInfo
     };
     
     JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
         JNIEnv *env = NULL;
         jclass clazz = NULL;
         if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK)
             return JNI_ERR;
         clazz = env->FindClass(JNI_REG_CLASS);
         if (clazz == NULL)
             return JNI_ERR;
         if (env->RegisterNatives(clazz, g_methods, NELEM(g_methods)) != JNI_OK)
             return JNI_ERR;
         return JNI_VERSION_1_4;
     }
     
     JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *) {
         JNIEnv *env = NULL;
         jclass clazz = NULL;
         if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK)
             return;
         clazz = env->FindClass(JNI_REG_CLASS);
         if (clazz == NULL)
             return;
         env->UnregisterNatives(clazz);
     }
    复制代码

    我这里是使用JNI_OnLoad的方式来作的JNI链接,固然也能够采用"Java_全类名_showFFmpegInfo"的方式。对应的,咱们须要建立对应的VideoStudio.java文件,以及编写对应的native方法。

    这里须要注意的是,咱们须要在CMakeList.txt中配置咱们的jni相关代码的源文件路径。

    # ffmpeg头文件路径
     include_directories(BEFORE ${PATH_TO_THIRDPARTY}/ffmpeg/include)
     # jni相关代码路径
     file(GLOB FILES_JNI_LAYER "${PATH_TO_JNI_LAYER}/*.cpp")
     add_library(
                 video-studio 
                 SHARED
                 ${FILES_JNI_LAYER})
     add_library(ffmpeg SHARED IMPORTED)
     set_target_properties(
                 ffmpeg
                 PROPERTIES IMPORTED_LOCATION
                 ${PATH_TO_PRE_BUILT}/libffmpeg.so)
     
     target_link_libraries( # Specifies the target library.
                 video-studio
                 ffmpeg
                 log)
    复制代码

一系列的配置完成后,应该就能够成功调用了,不出意外的话,是能够成功遍历出ffmpeg打开的全部的编码/解码器了。

添加命令行工具支持

ffmpeg有强大的命令行工具,能够完成一些常见的音视频功能,好比视频的裁剪、转码、图片转视频、视频转图片、视频水印添加等等。固然高级定制化的功能,仍是须要咱们开发者本身来写代码实现。

  1. 源文件及头文件的拷贝

    打开咱们下载的ffmpeg的源文件目录,找到config.h,拷贝到咱们cpp/thirdparty/ffmpeg/include/目录下,而后,在cpp/目录下新建cmd_line目录,在ffmpeg源码目录下找到cmdutils.c cmdutils.h ffmpeg.c ffmpeg.h ffmpeg_filter.c ffmpeg_hw.c ffmpeg_opt.c 拷贝到咱们的cmd_line目录下

  2. 稍做修改

    找到ffmpeg.c文件,将其内部的main函数改成你喜欢的名字,这里我把它改成ffmpeg_exec

    修改前:

    int main(int argc, char **argv) {
         ...
     }
    复制代码

    修改后:

    int ffmpeg_exec(int argc, char **argv) {
         ...
     }
    复制代码

    相应的,咱们须要在ffmpeg.h中添加函数的声明。

    找到cmdutils.c,找到exit_program函数,由于每次执行完这里会退出进程,在app中的表现就像闪退同样。因此,咱们稍加修改:

    修改前:

    void exit_program(int ret)
     {
         if (program_exit)
             program_exit(ret);
    
         exit(ret);
     }
    复制代码

    修改后:

    int exit_program(int ret)
     {
         if (program_exit)
             program_exit(ret);
     //    exit(ret);
         return ret;
     }
    复制代码

    相应的,咱们也须要在cmdutils.h中修改对应的函数声明。

    最后还有一点,为了不第二次调用命令行崩溃,咱们还须要的咱们ffmpeg.c中咱们修改过的ffmpeg_exec函数return以前加上这几行:

    nb_filtergraphs = 0;
     progress_avio = NULL;
    
     input_streams = NULL;
     nb_input_streams = 0;
     input_files = NULL;
     nb_input_files = 0;
    
     output_streams = NULL;
     nb_output_streams = 0;
     output_files = NULL;
     nb_output_files = 0;
    复制代码
  3. 调用

    在咱们的jni代码,VideoStudio.cpp中,添加函数:

    JNIEXPORT jint JNICALL executeFFmpegCmd(JNIEnv *env, jobject, jobjectArray commands) {
         int argc = env->GetArrayLength(commands);
         char **argv = (char **) malloc(sizeof(char *) * argc);
         for (int i = 0; i < argc; i++) {
             jstring string = (jstring) env->GetObjectArrayElement(commands, i);
             const char *tmp = env->GetStringUTFChars(string, 0);
             argv[i] = (char *) malloc(sizeof(char) * 1024);
             strcpy(argv[i], tmp);
         }
         try {
             ffmpeg_exec(argc, argv);
         } catch (int i) {
             LOGE("ffmpeg_exec error: %d", i);
         }
         for (int i = 0; i < argc; i++) {
             free(argv[i]);
         }
         free(argv);
         return 0;
     }
    复制代码

    在g_methods数组常量中添加:

    const JNINativeMethod g_methods[] = {
         "showFFmpegInfo", "()Ljava/lang/String;", (void *) showFFmpegInfo,
         "executeFFmpegCmd", "([Ljava/lang/String;)I", (void *) executeFFmpegCmd
     };
    复制代码

    在java中的调用:

    public int executeFFmpegCmd(String cmd) {
         String[] argv = cmd.split(" ");
         return VideoStudio.executeFFmpegCmd(argv);
     }
    复制代码

    到这里,咱们在Android App中调用ffmpeg命令行的集成工做已经完成了!

使用OpenGL ES预览相机画面

>> 咱们知道,相机Camera类(这里咱们只介绍Camera1的API,感兴趣的同窗能够自行尝试Camera2)是能够指定SurfaceHolder和SurfaceTexture做为预览载体来预览相机画面的。
那为何咱们要使用OpenGL ES来作这件事呢?前面咱们介绍过,OpenGL ES是搭载在Android系统中一个强大的三维(二维也能够)图像渲染库,在音视频开发工做中,咱们可使用OpenGL ES在实时的相机预览画面添加实时滤镜渲染,磨皮美白也能够作。
另外咱们也能够在预览画面上添加任意咱们想渲染的元素。这些是直接使用SurfaceView/TextureView作不到的(给SurfaceView和TextureView添加OpenGL ES支持的不要来杠,这里是说直接使用)。
复制代码

这部份内容须要有必定的OpenGL ES入门知识才能看懂,不了解的同窗,若是感兴趣的话能够去移动端滤镜开发(二)初识OpenGl里补一下课

OpenGL在使用时,是须要一条专门绑定了OpenGL上下文环境的线程。 Android系统为咱们提供了一个集成好OpenGL ES环境的View,它就是GLSurfaceView,它继承自SurfaceView,咱们能够直接在GLSurfaceView提供的OpenGL环境中直接作OpenGL ES API调用。固然咱们也可使用EGL接口来建立本身的OpenGL环境(GLSurfaceView其实就是一个自带单独线程、由EGL建立好环境的这么一个View)

GLSurfaceView也暴露了接口,让咱们能够本身制定渲染载体:

setEGLWindowSurfaceFactory(new EGLWindowSurfaceFactory() {
    @Override
            public EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display,
                                                  EGLConfig config, Object nativeWindow) {
                return egl.eglCreateWindowSurface(display, config, mSurface, null);
            }

            @Override
            public void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface) {
                egl.eglDestroySurface(display, surface);
            }
});
复制代码

因此咱们能够利用GLSurfaceView的环境,让Camera数据渲染到咱们想要的载体上(SurfaceView/TextureView)。

建立好环境后,接下来就是渲染了,设置给Camera的SurfaceTexture咱们能够本身建立Android系统的一个特殊的OES纹理来构建SurfaceTexture,固然建立纹理的动做须要在OpenGL环境中

public int createOESTexture() {
    int[] textures = new int[1];
    GLES20.glGenTextures(1, textures, 0);
    GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textures[0]);
    // 放大和缩小都使用双线性过滤
    GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER,
            GLES20.GL_LINEAR);
    GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER,
            GLES20.GL_LINEAR);
    // GL_CLAMP_TO_EDGE 表示OpenGL只画图片一次,剩下的部分将使用图片最后一行像素重复
    GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S,
            GLES20.GL_CLAMP_TO_EDGE);
    GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T,
            GLES20.GL_CLAMP_TO_EDGE);
    return textures[0];
}
复制代码

在拿到OES纹理ID后,咱们就能够做为构造函数参数直接构建SurfaceTexture了

SurfaceTexture surfaceTexture = new SurfaceTexture(textureId);
复制代码

咱们能够直接使用此纹理ID构建的SurfaceTexture经过Camera的setPreviewTexture方法来指定渲染载体。

有了数据源以后还不够,咱们须要将纹理贴图绘制到屏幕上,这个时候咱们就须要借助OpenGL ES的API以及glsl语言来作画面的渲染。

顶点着色器:

attribute vec4 aPosition;
attribute vec4 aTexCoord;
varying vec2 vTexCoord;
uniform mat4 aMvpMatrix;
uniform mat4 aStMatrix;

void main() {
    gl_Position = aMvpMatrix * aPosition;
    vTexCoord = (aStMatrix * aTexCoord).xy;
}
复制代码

片元着色器:

#extension GL_OES_EGL_image_external : require
precision mediump float;
varying vec2 vTexCoord;
uniform samplerExternalOES sTexture;
void main() {
    gl_FragColor = texture2D(sTexture, vTexCoord);
}
复制代码

编译、链接shader程序

public int createProgram(String vertexSrc, String fragmentSrc) {
    int vertex = loadShader(GLES20.GL_VERTEX_SHADER, vertexSrc);
    int fragment = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSrc);
    int program = GLES20.glCreateProgram();
    GLES20.glAttachShader(program, vertex);
    GLES20.glAttachShader(program, fragment);
    GLES20.glLinkProgram(program);
    int[] linkStatus = new int[1];
    GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
    if (linkStatus[0] != GLES20.GL_TRUE) {
        Log.e(TAG, "Could not link program: ");
        Log.e(TAG, GLES20.glGetProgramInfoLog(program));
        GLES20.glDeleteProgram(program);
        program = 0;
    }
    return program;
}

private int loadShader(int type, String src) {
    int shader = GLES20.glCreateShader(type);
    GLES20.glShaderSource(shader, src);
    GLES20.glCompileShader(shader);
    int[] compileStatus = new int[1];
    GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0);
    if (compileStatus[0] == 0) {
        Log.e(TAG, "load shader failed, type: " + type);
        Log.e(TAG, " " + GLES20.glGetShaderInfoLog(shader));
        GLES20.glDeleteShader(shader);
        shader = 0;
    }
    return shader;
}
复制代码

剩下的就是指定视口和绘制了,须要注意的是当对纹理使用samplerExternalOES采样器采样时,应该首先使用getTransformMatrix(float[]) 查询获得的矩阵来变换纹理坐标,每次调用updateTexImage的时候,可能会致使变换矩阵发生变化,所以在纹理图像更新时须要从新查询,改矩阵将传统2D OpenGL ES纹理坐标列向量(s,t,0,1),其中s,t∈[0, 1],变换为纹理中对应的采样位置。该变换补偿了图像流中任何可能致使与传统OpenGL ES纹理有差别的属性。例如,从图像的左下角开始采样,能够经过使用查询获得的矩阵来变换列向量(0, 0, 0, 1),而从右上角采样能够经过变换(1, 1, 0, 1)来获得。

项目代码已经上传到github,喜欢的同窗喜欢能够贡献一个start

结语

今天就先写到这里,在本篇文章中,介绍了如何把ffmpeg集成到咱们的Android项目中,还介绍了如何在Android App中使用ffmpeg的命令行。最后向你们介绍了如何使用OpenGL ES渲染摄像头预览数据。在下篇文章中,咱们将会介绍如何使用EGL API搭建咱们本身的OpenGL环境,还会向你们介绍如何给摄像头预览数据添加简单的以及高级一些的实时滤镜渲染效果,敬请期待!

相关文章
相关标签/搜索