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環境,還會向你們介紹如何給攝像頭預覽數據添加簡單的以及高級一些的實時濾鏡渲染效果,敬請期待!

相關文章
相關標籤/搜索