1. 程式人生 > >Android studio 3.0 整合 FFmpeg

Android studio 3.0 整合 FFmpeg

這篇文章的重點在於編譯FFmpeg庫和Android studio 3.0中配置FFmpeg相關檔案,所以如果是需要了解FFmpeg的實際應用的可以不用繼續往下看了。因為我自己在整個過程中遇到很多的坑,所以我在整個過程中都用雲筆記記錄了下來,希望幫助到後來需要的同學,文章涉及的所有步驟我都是親自嘗試過的,如果有不正確的地方,煩請指正。

環境準備

1. ubuntu
阿里雲伺服器 Ubuntu 16.04.3 LTS
2. ndk
android-ndk-r15c

國內可能無法訪問,可以通過如下命令下載

wget https://dl.google.com/android/repository/android-ndk-r15c-linux-x86_64.zip

如果需要下載其他版本的可以查閱這篇部落格

下載完成後,使用如下命令解壓

sudo unzip android-ndk-r15c-linux-x86_64.zip

然後把ndk的路徑加入環境變數,使用如下命令:

vim /etc/profile

然後在檔案末尾加入ndk的路徑,內容如下:

export ANDROID_NDK=/home/download/android-ndk-r15c
export PATH=$ANDROID_NDK:$PATH

然後使用如下命令,是環境變數生效:

source /etc/profile

NOTE:上面這個命令只是臨時生效,重新開啟一個終端就失效了,優點是不用重啟系統就能馬上生效

3. ffmpeg

到官網下載ffmpeg,或者使用命令下載,我這裡下載的是4.0.2的版本:

wget https://ffmpeg.org/releases/ffmpeg-4.0.2.tar.bz2

然後解壓:

tar -jxvf ffmpeg-4.0.2.tar.bz2

開始編譯

進入ffmpeg解壓完成後的根目錄,為了方便多次編譯,我們可以將編譯的命令寫入一個shell指令碼中,以後每次更改編譯引數重新執行指令碼就可以了。

編譯指令碼:

#!/bin/bash
export NDK=/home/download/android-ndk-r15c
export SYSROOT=$NDK/platforms/android-9/arch-arm/
export TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64
export PREFIX=/usr/local/ffmpeg/
export ADDI_CFLAGS="-marm"

./configure --target-os=android \
--prefix=$PREFIX --arch=arm \
--disable-doc \
--enable-shared \
--disable-static \
--disable-x86asm \
--disable-symver \
--enable-gpl \
--disable-ffmpeg \
--disable-ffplay \
--disable-ffprobe \
--disable-doc \
--disable-symver \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
--enable-cross-compile \
--sysroot=$SYSROOT \
--extra-cflags="-Os -fpic $ADDI_CFLAGS" \
--extra-ldflags="$ADDI_LDFLAGS" \
$ADDITIONAL_CONFIGURE_FLAG
make
make install
注意:
  1. shell指令碼的格式問題,windows上建立的shell指令碼在linux上可能因為格式問題識別不了;
  2. ./configure \和下面的引數之間不能有空格;
  3. 編譯平臺對應的android版本最好使用android-9,不然到低於這個版本的android平臺上可以會報錯;
  4. make到一半,卡住不動,可能是linux記憶體不夠了,通過命令free -m檢視記憶體,-m表示以M為單位顯示記憶體。

android專案中使用FFmpeg

上面編譯完成之後會在編譯指令碼指定的PREFIX的路徑下生成include、lib、share三個資料夾,include中是FFmpeg的方法的標頭檔案,lib是生成的so動態連結庫,share裡面有一些FFmpeg的示例程式。

這裡我們使用Android Studio 3.0來建立android工程,從as 2.2之後,我們開始用cmake編譯jni,所以我們用CMakeLists.txt來代替Android.mk。略過使用as建立android ndk專案,建立完成後,我們開始配置。

1. 匯入ffmpeg相關檔案

在src/main下新建ffmpeg資料夾,然後將編譯生成的包含所有ffmpeg標頭檔案的整個include資料夾拷貝到該目錄下,因為我們是編譯的armeabi-v7a平臺下的so庫,所以在ffmpeg目錄下新建armeabi-v7a資料夾,然後將所有so庫拷貝到該資料夾下,最終目錄結構如下:

在這裡插入圖片描述

2. 配置CMakeList.txt
# CMake的最低版本
cmake_minimum_required(VERSION 3.4.1)

add_library( native-lib
             SHARED
             src/main/cpp/native-lib.cpp )

find_library( log-lib
              log )

set(JNI_LIBS_DIR ${CMAKE_SOURCE_DIR}/src/main/ffmpeg)

add_library(avutil
            SHARED
            IMPORTED )
set_target_properties(avutil
                      PROPERTIES IMPORTED_LOCATION
                      ${JNI_LIBS_DIR}/${ANDROID_ABI}/libavutil.so )

add_library(swresample
            SHARED
            IMPORTED )
set_target_properties(swresample
                      PROPERTIES IMPORTED_LOCATION
                      ${JNI_LIBS_DIR}/${ANDROID_ABI}/libswresample.so )

add_library(swscale
            SHARED
            IMPORTED )
set_target_properties(swscale
                      PROPERTIES IMPORTED_LOCATION
                      ${JNI_LIBS_DIR}/${ANDROID_ABI}/libswscale.so )

add_library(avcodec
            SHARED
            IMPORTED )
set_target_properties(avcodec
                      PROPERTIES IMPORTED_LOCATION
                      ${JNI_LIBS_DIR}/${ANDROID_ABI}/libavcodec.so )

add_library(avformat
            SHARED
            IMPORTED )
set_target_properties(avformat
                      PROPERTIES IMPORTED_LOCATION
                      ${JNI_LIBS_DIR}/${ANDROID_ABI}/libavformat.so )

add_library(avfilter
            SHARED
            IMPORTED )
set_target_properties(avfilter
                      PROPERTIES IMPORTED_LOCATION
                      ${JNI_LIBS_DIR}/${ANDROID_ABI}/libavfilter.so )

add_library(avdevice
            SHARED
            IMPORTED )
set_target_properties(avdevice
                      PROPERTIES IMPORTED_LOCATION
                      ${JNI_LIBS_DIR}/${ANDROID_ABI}/libavdevice.so )

include_directories(src/main/ffmpeg/include)

target_link_libraries(  native-lib
                        avutil
                        swresample
                        swscale
                        avcodec
                        avformat
                        avfilter
                        avdevice
                        android
                        ${log-lib})

說明:

  • cmake_minimum_required:指定cmake的最低版本
  • set():這裡相當於定義一個變數,後面通過${var}來引用這個變數,我們這裡是指定了ffmpeg庫檔案根目錄
  • add_library和set_target_properties:新增庫檔案,ffmpeg的8個so庫我們都需要通過這兩個方法引入
  • include_directories:這裡指定ffmpeg的標頭檔案的路徑,如果這裡不指定的話,我們在編寫程式碼時include標頭檔案需要寫很長的路徑,當然還有個更重要的原因是,ffmpeg本身的標頭檔案之間互相引用就是預設這個路徑作為根目錄的,如果不指定這個目錄,編譯的時候會在ffmpeg的標頭檔案裡面報錯,說找不到其他標頭檔案,ffmpeg的自己的標頭檔案引用是下面的格式:
#include "libavcodec/avcodec.h"
#include "libavutil/dict.h"
#include "libavutil/log.h"

看到這裡我們就能明白為什麼一定要指定這個標頭檔案的根目錄了

  • target_link_libraries:連結so庫。這裡有個要注意的地方就是:如果我們在編寫程式碼時使用了ANativeWindow相關的方法的話,需要在連結庫檔案的時候加入android這個庫檔案,不然會報undefined reference to的錯
3. 修改build.gradle檔案
android {
    ......
    defaultConfig {
        externalNativeBuild {
            cmake {
                cppFlags "-fexceptions"
                abiFilters "armeabi-v7a"
            }
        }
    }
	// 加上這個,打包apk的時候才會將ffmpeg的so庫打包進libs目錄裡面
	sourceSets {
		main {
			jni.srcDirs = []
			jniLibs.srcDirs = ['src/main/ffmpeg']
		}
	}
    ......
}
4. 編寫cpp檔案使用ffmpeg

上面配置完成之後,我們就可以編寫程式碼,然後在程式碼中使用ffmpeg的方法了。

在src/main/java的包下建立我們的java類,然後在類中編寫native方法,然後通過System.loadLibrary載入我們需要的so庫,如下所示:

static {
        System.loadLibrary("avutil");
        System.loadLibrary("swresample");
        System.loadLibrary("swscale");
        System.loadLibrary("avcodec");
        System.loadLibrary("avformat");
        System.loadLibrary("avfilter");
        System.loadLibrary("avdevice");
    }

public native void decode(String input, String output);

然後在命令列下,進入專案根目錄app/src/main/java/目錄下,使用命令生成標頭檔案:

javah 包名.類名

然後我們在建立ndk專案時自動生成的cpp檔案裡面加入一個方法,方法名和引數就是我們剛生成的標頭檔案裡面宣告的方法,示例如下:

#include <jni.h>
#include <string>
#include <android/log.h>
#include <android/native_window_jni.h>
#include "libyuv.h"
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavutil/imgutils.h"
}


// __android_log_print()需要<android/log.h>標頭檔案
#define LOGI(FORMAT,...) __android_log_print(ANDROID_LOG_INFO, "sisyphus", FORMAT, ##__VA_ARGS__)
#define LOGE(FORMAT,...) __android_log_print(ANDROID_LOG_ERROR, "sisyphus", FORMAT, ##__VA_ARGS__)

extern "C"
JNIEXPORT jstring

JNICALL
Java_com_sisyphus_ffmpegdemo_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

extern "C"
JNIEXPORT void JNICALL
Java_com_sisyphus_ffmpegdemo_FFmpegPlayer_decode(JNIEnv *env, jobject instance, jstring input_,
                                                 jstring output_, jobject surface) {
    const char *input = env->GetStringUTFChars(input_, 0);
    const char *output = env->GetStringUTFChars(output_, 0);

    av_register_all();

    env->ReleaseStringUTFChars(input_, input);
    env->ReleaseStringUTFChars(output_, output);
}

這裡還有最後一個需要注意的地方就是:因為ffmpeg是用c語言編寫的,我們是在cpp檔案裡面使用的,所以我們在include他的標頭檔案時需要將標頭檔案包含在extern “C” {}裡面,如下:

extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavutil/imgutils.h"
}

不然的話,雖然我們在編寫程式碼的時候不會報錯,在最後編譯的時候就會報錯說我們使用的方法都沒有定義。

5. 編譯

點選選單Build->Make Project編譯專案,最後會在專案根目錄的\app\build\intermediates\cmake\debug\obj\armeabi-v7a下生成一個so庫。

打完收工