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
注意:
- shell指令碼的格式問題,windows上建立的shell指令碼在linux上可能因為格式問題識別不了;
- ./configure \和下面的引數之間不能有空格;
- 編譯平臺對應的android版本最好使用android-9,不然到低於這個版本的android平臺上可以會報錯;
- 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庫。
打完收工