項目地址
通過閱讀本文,你將學到以下內容:
- 如何編譯多種CPU指令集的FFmpeg動態(tài)庫
- 如何配置CMakeLists.txt和build.gradle構建Android項目
編譯環(huán)境
- ndk-r16
- ffmpeg-4.0.2
一、交叉編譯FFmpeg生成動態(tài)庫
FFmpeg是一個強大的音視頻處理庫,我們有時候只需要用到這個庫的部分功能,因此我們需要通過configure的一些選項對它進行裁剪骤星。此外,我們還需要配置生成哪種CPU指令集的動態(tài)庫、生成的動態(tài)庫在什么操作系統(tǒng)上使用以及一些編譯選項等败明。
一個支持armeabi-v7a、arm64-v8a太防、x86和x86_64四種CPU指令集的FFmpeg編譯腳本妻顶,內容如下:
#!/bin/sh
# build.sh
# Builds all supported architectures of FFmpeg for Android.
# Versions: NDK - r16b, FFmpeg - 4.0.2
NDK=/Users/chenzhichang/Downloads/android-ndk-r16b
# MacOS:darwin-86_64
HOST=darwin-x86_64
# Takes three arguments:
# First: ARCH, supported values: armeabi-v7a, arm64-v8a, x86, x86_64
# Second: platform level. Range: 14-19, 21-24, 26-28
# Third: additinal configuration flags. Already present flags: --enable-cross-compile --disable-static --disable-programs --disable-doc --enable-shared --enable-protocol=file --enable-pic --enable-small
build () {
ARCH=$1
LEVEL=$2
if [ ! $ARCH ]; then
ARCH=armeabi-v7a
fi
if [ ! $LEVEL ]; then
LEVEL=21
fi
ISYSROOT=$NDK/sysroot
PLATFORM_ARCH=
CFLAGS=
TARGET=
TOOLCHAIN_FOLDER=
CONFIGURATION="--disable-asm \
--enable-cross-compile \
--disable-static \
--disable-programs \
--disable-doc \
--enable-shared \
--enable-protocol=file \
--enable-pic \
--enable-small \
--disable-devices \
$3"
case $ARCH in
"armeabi-v7a")
TARGET="arm-linux-androideabi"
CFLAGS="-march=armv7-a -mfloat-abi=softfp -mfpu=neon"
PLATFORM_ARCH="arm"
TOOLCHAIN_FOLDER="arm-linux-androideabi"
;;
"arm64-v8a")
TARGET="aarch64-linux-android"
CFLAGS="-march=armv8-a"
PLATFORM_ARCH="arm64"
CONFIGURATION="$CONFIGURATION --disable-pthreads"
TOOLCHAIN_FOLDER="aarch64-linux-android"
;;
"x86")
TARGET="i686-linux-android"
CFLAGS="-march=i686 -mtune=intel -mssse3 -mfpmath=sse -m32"
PLATFORM_ARCH="x86"
TOOLCHAIN_FOLDER="x86"
;;
"x86_64")
TARGET="x86_64-linux-android"
CFLAGS="-march=x86-64 -msse4.2 -mpopcnt -m64 -mtune=intel"
PLATFORM_ARCH="x86_64"
TOOLCHAIN_FOLDER="x86_64"
;;
esac
CROSS_PREFIX=$NDK/toolchains/$TOOLCHAIN_FOLDER-4.9/prebuilt/$HOST/bin/$TARGET-
ASM=$ISYSROOT/usr/include/$TARGET
SYSROOT=$NDK/platforms/android-$LEVEL/arch-$PLATFORM_ARCH/
PREFIX="android/$ARCH"
./configure --prefix=$PREFIX \
$CONFIGURATION \
--cross-prefix=$CROSS_PREFIX \
--arch=$PLATFORM_ARCH \
--target-os=android \
--sysroot=$SYSROOT \
--extra-cflags="$CFLAGS -I$ASM -isysroot $ISYSROOT -D__ANDROID_API__=$LEVEL -Wfatal-errors -U_FILE_OFFSET_BITS -Os -fPIC -DANDROID -D__thumb__ -Wno-deprecated"
make clean
make -j4
make install
}
#build "armeabi-v7a" "21"
#build "arm64-v8a" "21"
#build "x86_64" "21"
#build "x86" "21"
這個腳本的用法很簡單酸员,只需要將該腳本文件拷貝到ffmpeg源碼的根目錄下,將腳本中的NDK變量值改為你的電腦上NDK所在的路徑讳嘱。如果你的電腦不是Mac幔嗦,則還需要修改HOST變量值。最后執(zhí)行下面這條命令即可沥潭。其中armeabi-v7a表示CPU指令集邀泉,21表示Android版本。
./build.sh armeabi-v7a 21
在這里簡單介紹一下關于此腳本的一些配置項钝鸽,從而方便你能夠根據你的需求來修改這個腳本:
- --prefix:指定編譯輸出的文件路徑
- --target-os:指定目標操作系統(tǒng)
- --disable-static:禁止生成靜態(tài)庫
- --disable-programs:禁止生成ffplay汇恤、ffmpeg等可執(zhí)行文件
- --disable-doc:禁止生成文檔
- --enable-shared:生成動態(tài)動態(tài)鏈接庫
- --enable-cross-compile:開啟交叉編譯(跨平臺編譯)
執(zhí)行此腳本,最終生成文件如下:
二拔恰、創(chuàng)建NDK項目并引入FFmpeg動態(tài)庫
創(chuàng)建一個NDK項目因谎,然后將生成的ffmpeg動態(tài)庫文件和頭文件拷貝到libs文件夾,最終項目結構如下圖颜懊。
三财岔、配置build.gradle
需要配置的內容如下:
- 配置so庫的路徑
- 配置cmake編譯選項和支持的CPU指令集,其中-Wno-deprecated-declarations選項用于忽略使用廢棄API的編譯警告饭冬。
- 配置CMakeLists.txt文件的路徑
最終配置如下:
apply plugin: 'com.android.application'
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.chenzhichang.testffmpeg"
minSdkVersion 15
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
//配置so庫的路徑
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
//配置cmake編譯選項和支持的CPU指令集
externalNativeBuild {
cmake {
cppFlags "-frtti -fexceptions -Wno-deprecated-declarations"
}
ndk{
abiFilters "armeabi-v7a", "x86_64", "x86"
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
//配置CMakeLists.txt文件的路徑
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
四使鹅、配置CMakeLists.txt
配置如下:
cmake_minimum_required(VERSION 3.4.1)
find_library( log-lib
log )
# 定義變量
set(distribution_DIR ../../../../libs)
# 添加庫——自己編寫的庫
# 庫名稱:native-lib
# 庫類型:SHARED,表示動態(tài)庫昌抠,后綴為.so(如果是STATIC患朱,則表示靜態(tài)庫,后綴為.a)
# 庫源碼文件:src/main/cpp/native-lib.cpp
add_library( native-lib
SHARED
src/main/cpp/native-lib.cpp )
# 添加庫——外部引入的庫
# 庫名稱:avcodec(不需要包含前綴lib)
# 庫類型:SHARED炊苫,表示動態(tài)庫裁厅,后綴為.so(如果是STATIC,則表示靜態(tài)庫侨艾,后綴為.a)
# IMPORTED表明是外部引入的庫
add_library( avcodec
SHARED
IMPORTED)
# 設置目標屬性
# 設置avcodec目標庫的IMPORTED_LOCATION屬性执虹,用于說明引入庫的位置
# 還可以設置其他屬性,格式:PROPERTIES key value
set_target_properties( avcodec
PROPERTIES IMPORTED_LOCATION
${distribution_DIR}/${ANDROID_ABI}/libavcodec.so)
add_library( avfilter
SHARED
IMPORTED)
set_target_properties( avfilter
PROPERTIES IMPORTED_LOCATION
${distribution_DIR}/${ANDROID_ABI}/libavfilter.so)
add_library( avformat
SHARED
IMPORTED)
set_target_properties( avformat
PROPERTIES IMPORTED_LOCATION
${distribution_DIR}/${ANDROID_ABI}/libavformat.so)
add_library( avutil
SHARED
IMPORTED)
set_target_properties( avutil
PROPERTIES IMPORTED_LOCATION
${distribution_DIR}/${ANDROID_ABI}/libavutil.so)
add_library( swresample
SHARED
IMPORTED)
set_target_properties( swresample
PROPERTIES IMPORTED_LOCATION
${distribution_DIR}/${ANDROID_ABI}/libswresample.so)
add_library( swscale
SHARED
IMPORTED)
set_target_properties( swscale
PROPERTIES IMPORTED_LOCATION
${distribution_DIR}/${ANDROID_ABI}/libswscale.so)
# 引入頭文件
include_directories(libs/include)
# 告訴編譯器生成native-lib庫需要鏈接的庫
# native-lib庫需要依賴avcodec唠梨、avfilter等庫
target_link_libraries( native-lib
avcodec
avfilter
avformat
avutil
swresample
swscale
${log-lib} )
關于CMake的詳細內容可以閱讀官方文檔
五袋励、編寫代碼測試FFmpeg是否能夠正常使用
- activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btn_protocol"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="2dp"
android:text="Protocol"
android:textAllCaps="false" />
<Button
android:id="@+id/btn_format"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="2dp"
android:text="Format"
android:textAllCaps="false" />
<Button
android:id="@+id/btn_codec"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="2dp"
android:text="Codec"
android:textAllCaps="false" />
<Button
android:id="@+id/btn_filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="2dp"
android:text="Filter"
android:textAllCaps="false" />
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Hello World!" />
</ScrollView>
</LinearLayout>
- native-lib.cpp
#include <jni.h>
#include <string>
extern "C"
{
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavfilter/avfilter.h>
JNIEXPORT jstring JNICALL
Java_com_chenzhichang_testffmpeg_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
JNIEXPORT jstring JNICALL
Java_com_chenzhichang_testffmpeg_MainActivity_urlprotocolinfo(JNIEnv *env, jobject instance) {
char info[40000] = {0};
av_register_all();
struct URLProtocol *pup = NULL;
struct URLProtocol **p_temp = &pup;
avio_enum_protocols((void **) p_temp, 0);
while ((*p_temp) != NULL) {
sprintf(info, "%sInput: %s\n", info, avio_enum_protocols((void **) p_temp, 0));
}
pup = NULL;
avio_enum_protocols((void **) p_temp, 1);
while ((*p_temp) != NULL) {
sprintf(info, "%sInput: %s\n", info, avio_enum_protocols((void **) p_temp, 1));
}
return env->NewStringUTF(info);
}
JNIEXPORT jstring JNICALL
Java_com_chenzhichang_testffmpeg_MainActivity_avformatinfo(JNIEnv *env, jobject instance) {
char info[40000] = {0};
av_register_all();
AVInputFormat *if_temp = av_iformat_next(NULL);
AVOutputFormat *of_temp = av_oformat_next(NULL);
while (if_temp != NULL) {
sprintf(info, "%sInput: %s\n", info, if_temp->name);
if_temp = if_temp->next;
}
while (of_temp != NULL) {
sprintf(info, "%sOutput: %s\n", info, of_temp->name);
of_temp = of_temp->next;
}
return env->NewStringUTF(info);
}
JNIEXPORT jstring JNICALL
Java_com_chenzhichang_testffmpeg_MainActivity_avcodecinfo(JNIEnv *env, jobject instance) {
char info[40000] = {0};
av_register_all();
AVCodec *c_temp = av_codec_next(NULL);
while (c_temp != NULL) {
if (c_temp->decode != NULL) {
sprintf(info, "%sdecode:", info);
} else {
sprintf(info, "%sencode:", info);
}
switch (c_temp->type) {
case AVMEDIA_TYPE_VIDEO:
sprintf(info, "%s(video):", info);
break;
case AVMEDIA_TYPE_AUDIO:
sprintf(info, "%s(audio):", info);
break;
default:
sprintf(info, "%s(other):", info);
break;
}
sprintf(info, "%s[%10s]\n", info, c_temp->name);
c_temp = c_temp->next;
}
return env->NewStringUTF(info);
}
JNIEXPORT jstring JNICALL
Java_com_chenzhichang_testffmpeg_MainActivity_avfilterinfo(JNIEnv *env, jobject instance) {
char info[40000] = {0};
avfilter_register_all();
AVFilter *f_temp = (AVFilter *) avfilter_next(NULL);
while (f_temp != NULL) {
sprintf(info, "%s%s\n", info, f_temp->name);
f_temp = f_temp->next;
}
return env->NewStringUTF(info);
}
}
- MainActivity.java
package com.chenzhichang.testffmpeg;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
/**
* @author chenzhichang
*/
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
TextView tvInfo;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
((Button)findViewById(R.id.btn_protocol)).setOnClickListener(this);
((Button)findViewById(R.id.btn_codec)).setOnClickListener(this);
findViewById(R.id.btn_filter).setOnClickListener(this);
findViewById(R.id.btn_format).setOnClickListener(this);
tvInfo = (TextView) findViewById(R.id.tv_info);
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.btn_protocol:
tvInfo.setText(urlprotocolinfo());
break;
case R.id.btn_format:
tvInfo.setText(avformatinfo());
break;
case R.id.btn_codec:
tvInfo.setText(avcodecinfo());
break;
case R.id.btn_filter:
tvInfo.setText(avfilterinfo());
break;
default:
break;
}
}
public native String stringFromJNI();
public native String urlprotocolinfo();
public native String avformatinfo();
public native String avcodecinfo();
public native String avfilterinfo();
}
運行結果如下圖:
參考資料
IljaKosynkin/FFmpeg-Development-Kit
ffmpeg使用NDK編譯時遇到的一些坑 - luo0xue的博客 - CSDN博客
ffmpeg ./configure參數說明 - azraelly - 博客園
Android開發(fā)學習之路--Android Studio cmake編譯ffmpeg - 東月之神 - CSDN博客