本文你可以了解到
本文将介绍如何将上一篇文章编译出来的?FFmpeg so
?库,引入到?Android
?工程中,并验证?so
?是否可以正常使用。
在过去,通常使用?makefile
?的方式在项目中引入?C/C++
?代码支持,随着?Android Studio
?的普及,makefile
?的方式已经基本被?CMake
?替代。
有了?Android
?官方的支持,NDK
?层代码的开发变得更加容易。以前一谈到?Android NDK
?,许多人就会大惊失色,感觉是深不可测的东西,一方面是?makefile
?的编写很难,一方面是?C/C++
?相比?Java
?来说,比较晦涩。
但是不必担心,一是有了?CMake
?,二是对于?C/C++
?的基本使用其实和?Java
?差不多,本系列涉及到的,也都是对?C/C++
?的基础使用,毕竟,高级的我也不会不是吗?哈哈哈~~
首先,需要下载?CMake
?相关工具,在?Android Studio
?中依次点击?Tools->SDK Manager->SDK Tools
,然后勾选
CMake
?: CMake 构建工具
LLDB
?: C/C++ 代码调试工具
NDK
?: NDK 环境
最后依次点击?OK->OK->Finish
?,开始下载(文件比较大,可能会比较慢,请耐心等待)。
下载CMake工具
有两种方式:
一是,新建一个新的工程,并勾选?C/C++
?支持选项,系统将自动创建一个支持?C/C++
?编码的工程。
二是,在已有的项目上,手动添加所有的添加项来支持?C/C++
?编码,其实就是自己手动添加「第一种方式」
中?Android Studio
?为我们自动创建的那些东西。
首先,通过新建一个新工程的方式,看看?IDE
?为我们生成了那些东西。
依次点击?File -> New -> New Project
,进入新建工程页面,拉到最后,选择?Native C++
?然后按照默认配置,一路?Next -> Next -> Finish
?即可。
新建C++工程
2)Android Studio 自动生成了什么
生成的工程目录如下:
工程目录
重点关注上图标注的3个地方:
MainActivity
class MainActivity : AppCompatActivity() {
?
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
?
// Example of a call to a native method
sample_text.text = stringFromJNI()
}
?
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
external fun stringFromJNI(): String
?
companion object {
?
// Used to load the 'native-lib' library on application startup.
init {
System.loadLibrary("native-lib")
}
}
}
很简单,使用过?so
?库的应该都看得懂,这里简单说一下。
代码的最下面,companion object
?在?Kotlin
?中表示静态代码块,类似?Java
?中的?static { }
,其中的代码有且只会被执行一次。
接着在?init{}
?方法中,加载了?C/C++
?代码编译成的?so
?库:?native-lib
。
往上一句代码,用?external
?声明了一个外部引用的方法?stringFromJNI()
?,这个方法和?C/C++
?层的代码是对应的。
最终在最上面的?onCreate
?中,将从?C/C++
?层返回的?String
?显示出来。
【免费分享】音视频学习资料包、大厂面试题、技术视频和学习路线图,资料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以点击788280672加群免费领取~
cpp
?文件包其中有两个文件非常重要,分别是?native-lib.cpp
?、?CMakeLists.txt
。
i.?native-lib.cpp
?:是一个 C++ 接口文件,在?MainActivity
?中声明的外部方法将在这里得到实现。
自动生成?native-lib.cpp
?的内容如下:
#include <jni.h>
#include <string>
?
extern "C" JNIEXPORT jstring JNICALL
Java_com_chenlittleping_mynativeapp_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
可以看到,这个 cpp 文件中的方法命名非常的长,不过其实非常简单。
首先是头部固定写法?extern "C" JNIEXPORT jstring JNICALL
:
extern "C"
?表示以?C语言
?的方式来编译;
jstring
?表示该方法返回类型是?Java
?层的?String
?类型,类似的还是有:?void
?jint
等;
然后是 Java 层对应方法的映射,即整个方法命名其实是?Java
?层对应方法的绝对路径。
其中,最前面的?Java_
?是固定写法;
com_chenlittleping_mynativeapp_MainActivity_
: 对应的是?com.chenlittleping.mynativeapp.MainActivity.
,其实就是?.
?换为?_
;
stringFromJNI
?和 Java 层的方法一致。
最后是两个参数,?JNIEnv *env
?和?jobject
,分别代表?JNI
?的上下文环境和调用这个接口的?Java
?的类的实例。
调用这个方法,将会在?C++
?层创建一个字符串,并以?Java#String
?的类型返回。
ii.?CMakeLists.txt
?: 也就是构建脚本。内容如下:
# cmake 最低版本
cmake_minimum_required(VERSION 3.4.1)
?
# 配置so库编译信息
add_library(
# 输出so库的名称
native-lib
?
# 设置生成库的方式,默认为SHARE动态库
SHARED
?
# 列出参与编译的所有源文件
native-lib.cpp)
?
# 查找代码中使用到的系统库
find_library( # Sets the name of the path variable.
log-lib
?
# Specifies the name of the NDK library that
# you want CMake to locate.
log)
?
# 指定编译目标库时,cmake要链接的库
target_link_libraries(
# 指定目标库,native-lib 是在上面 add_library 中配置的目标库
native-lib
# 列出所有需要链接的库
${log-lib})
这是最简单的编译配置,具体见上面的注释。
CMakeLists.txt
?的目的就是配置可以编译出?native-lib
?so 库的构建信息。
说白了,就是告诉编译器:
- 编译的目标是谁
- 依赖的源文件在哪里找
- 依赖的 `系统或第三方` 的 `动态或静态` 库在哪里找。
在?第二步
?中,已经把构建?so
?库的信息配置好了,接下来要把这些信息注册到?Gradle
?中,编译器才会去编译它。
app 的?build.gradle
?内容如下:
apply plugin: 'com.android.application'
?
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
?
android {
compileSdkVersion 28
buildToolsVersion "29.0.1"
defaultConfig {
applicationId "com.chenlittleping.mynativeapp"
minSdkVersion 19
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// 1) CMake 编译配置
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
// 2) 配置 CMakeLists 路径
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}
}
?
dependencies {
// 省略无关代码
//......
}
最主要的两个地方是两个?externalNativeBuild
?。
第 1 个?externalNativeBuild
?中,可以做一些优化配置,比如只打包包含?armeabi
?架构的?so
?:
externalNativeBuild {
cmake {
cppFlags ""
}
ndk {
abiFilters "armeabi" //, "armeabi-v7a"
}
}
第 2 个?externalNativeBuild
,主要是配置?CMakeLists.txt
?的路径和版本。
Android Studio
?为我们生成的关于?C/C++
?支持的主要就是以上三个地方,有了以上配置,就可以在?MainActivity
?页面中正常的显示出?Hello from C++
?。
前面就说过,在已有项目上添加?C/C++
?支持,就是由我们自己手动添加整个配置。那么根据签名介绍的三个步骤,依葫芦画瓢,就可以添加了。
这里刚好就用添加?FFMpeg so
?到本系列文章现有 Demo 工程中来演示一遍。
首先,在?app/src/main/
?目录下,新建文件夹,并命名为?cpp
?。
接着,在?cpp
?目录下,右键?New -> C/C++ Source File
?,新建?native-lib.cpp
?文件。
接着,在?cpp
?目录下,右键?New -> File
?,新建?CMakeLists.txt
?,先将上面?IDE
?生成的那份代码粘贴进来, FFmpeg的配置在后面详细讲解。
# CMakeLists.txt
?
# cmake 最低版本
cmake_minimum_required(VERSION 3.4.1)
?
# 配置so库编译信息
add_library(
# 输出so库的名称
native-lib
?
# 设置生成库的方式,默认为SHARE动态库
SHARED
?
# 列出参与编译的所有源文件
native-lib.cpp)
?
# 查找代码中使用到的系统库
find_library( # Sets the name of the path variable.
log-lib
?
# Specifies the name of the NDK library that
# you want CMake to locate.
log)
?
# 指定编译目标库时,cmake要链接的库
target_link_libraries(
# 指定目标库,native-lib 是在上面 add_library 中配置的目标库
native-lib
?
# 列出所有需要链接的库
${log-lib})
android {
// ...
defaultConfig {
// ...
// 1) CMake 编译配置
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
// ...
// 2) 配置 CMakeLists 路径
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}
}
?
// ...
如果只是简单的编写?C/C++
?代码,以上基础配置就可以了。
接着来看看本文的重点,如何使用?CMakeLists.txt
?引入?FFmpeg
?的动态库。
在?上一篇文章中,我们编译的?FFmpeg so
?库的?CPU
?架构为?armv7-a
,所以,我们需要把所有的?so
?库放置到?armeabi-v7a
?目录下。
首先,在?app/src/main/
?目录下,新建文件夹,并命名为?jniLibs
?。
app/src/main/jniLibs
?是 Android Studio 默认的放置 so 动态库的目录。
接着,在?jniLibs
?目录下,新建?armeabi-v7a
?目录。
最后把?FFmpeg
?编译得到的所有?so
?库粘贴到?armeabi-v7a
?目录。如下:
so目录
在编译?FFmpeg
?的时候,除了生成?so
?外,还会生成对应的?.h
?头文件,也就是?FFmpeg
?对外暴露的所有接口。
FFmpeg编译输出
在?cpp
?目录下,新建?ffmpeg
?目录,然后把编译时生成的?include
?文件粘贴进来。
头文件目录
上面已经把?so
?和?头文件
?放置到对应的目录中了,但是编译器是不会把它们编译、链接、并打包到?Apk
?中的,我们还需要在?CMakeLists.txt
?中显性的把相关的?so
?添加和链接起来。完整的?CMakeLists.txt
?如下:
cmake_minimum_required(VERSION 3.4.1)
?
# 支持gnu++11
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11")
?
# 1. 定义so库和头文件所在目录,方面后面使用
set(ffmpeg_lib_dir ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
set(ffmpeg_head_dir ${CMAKE_SOURCE_DIR}/ffmpeg)
?
# 2. 添加头文件目录
include_directories(${ffmpeg_head_dir}/include)
?
# 3. 添加ffmpeg相关的so库
add_library( avutil
SHARED
IMPORTED )
set_target_properties( avutil
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavutil.so )
?
add_library( swresample
SHARED
IMPORTED )
set_target_properties( swresample
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libswresample.so )
add_library( avcodec
SHARED
IMPORTED )
set_target_properties( avcodec
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavcodec.so )
add_library( avfilter
SHARED
IMPORTED)
set_target_properties( avfilter
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavfilter.so )
add_library( swscale
SHARED
IMPORTED)
set_target_properties( swscale
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libswscale.so )
?
add_library( avformat
SHARED
IMPORTED)
set_target_properties( avformat
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavformat.so )
?
add_library( avdevice
SHARED
IMPORTED)
set_target_properties( avdevice
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavdevice.so )
?
# 查找代码中使用到的系统库
find_library( # Sets the name of the path variable.
log-lib
?
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
?
# 配置目标so库编译信息
add_library( # Sets the name of the library.
native-lib
?
# Sets the library as a shared library.
SHARED
?
# Provides a relative path to your source file(s).
native-lib.cpp
)
?
# 指定编译目标库时,cmake要链接的库
target_link_libraries(
?
# 指定目标库,native-lib 是在上面 add_library 中配置的目标库
native-lib
?
# 4. 连接 FFmpeg 相关的库
avutil
swresample
avcodec
avfilter
swscale
avformat
avdevice
?
# Links the target library to the log library
# included in the NDK.
${log-lib} )
主要看看注释中新加入的?1~4
?点。
1)通过?set
?方法定义了?so
?和?头文件
?所在目录,方便后面使用。
其中?CMAKE_SOURCE_DIR
?为系统变量,指向?CMakeLists.txt
?所在目录。?ANDROID_ABI
?也是系统变量,指向 so 对应的?CPU
?框架目录:armeabi、armeabi-v7a、x86 ...
set(ffmpeg_lib_dir ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
set(ffmpeg_head_dir ${CMAKE_SOURCE_DIR}/ffmpeg)
2)通过?include_directories
?设置头文件查找目录
include_directories(${ffmpeg_head_dir}/include)
3)通过?add_library
?添加 FFmpeg 相关的?so
?库,以及?set_target_properties
?设置?so
?对应的目录。
其中,add_library 第一个参数为 so 名字,
SHARED
?表示引入方式为动态库引入。
add_library( avcodec
SHARED
IMPORTED )
set_target_properties( avcodec
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavcodec.so )
4)最后,通过?target_link_libraries
?把前面添加进来的?FFMpeg so
?库都链接到目标库?native-lib
?上。
这样,我们就将?FFMpeg
?相关的?so
?库都引入到当前工程中了。下面就要来测试一下,是否可以正常调用到?FFmpeg
?相关的方法了。
要检查?FFmpeg
?是否可以使用,可以通过获取?FFmpeg
?基础信息来验证。
把获取到的?FFmpeg
?信息显示出来。
class FFmpegActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_ffmpeg_info)
?
tv.text = ffmpegInfo()
}
?
private external fun ffmpegInfo(): String
?
companion object {
init {
System.loadLibrary("native-lib")
}
}
}
#include <jni.h>
#include <string>
#include <unistd.h>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavfilter/avfilter.h>
#include <libavcodec/jni.h>
?
JNIEXPORT jstring JNICALL
Java_com_cxp_learningvideo_FFmpegActivity_ffmpegInfo(JNIEnv *env, jobject /* this */) {
?
char info[40000] = {0};
AVCodec *c_temp = av_codec_next(NULL);
while (c_temp != NULL) {
if (c_temp->decode != NULL) {
sprintf(info, "%sdecode:", 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);
} else {
sprintf(info, "%sencode:", info);
}
c_temp = c_temp->next;
}
return env->NewStringUTF(info);
}
}
首先,我们看到代码被包裹在?extern "C" { }
?当中,和前面的系统创建的稍微有些不同,通过这个大括号包裹,我们就不需要每个方法都添加单独的?extern "C"
?开头了。
另外,由于?FFmpeg
?是使用?C
?语言编写的,所在?C++
?文件中引用?#include
?的时候,也需要包裹在?extern "C" { }
,才能正确的编译。
方法的新建就不用说了,和前面介绍的命名方法一致。
在方法中,使用?FFmpeg
?提供的方法?av_codec_next
,获取到 FFmpeg 的编解码器,然后通过循环遍历,将所有的音视频编解码器信息拼接起来,最后返回给?Java
?层。
至此,FFmpeg
?加入到工程中,并被调用。
如果一切正常,App运行后,就会显示出?FFmpeg
?音视频编解码器的信息。