随着 FFmpeg、NDK 与 Android Studio 的不断迭代,本文可能也会像我参考过的过期文章一样失效(很遗憾),但希望本文中提到的问题排查以及步骤说明能够帮到你,如果发现了文章中的谬误以及不足之处也欢迎你提供建议与指正,十分感谢?。
初步目标是使用 FFmpeg 实现 Android 内简单的视频剪辑、添加背景音乐、添加字幕等功能,由于本人初学 Android 开发,能力有限,基础薄弱,无法较为全面地深入学习过程中遇到的问题,文章中可能掺杂有一些知其然而不知其所以然的部分或一些不恰当不精确的个人理解,还请见谅?。
设备:macOS Big Sur 11.6 (Apple Silicon M1)
FFmpeg 版本:4.4
开发环境:
1.82123.1.7779620
3.22.0-rc2
3.18.13.19.3 版本后提供对苹果芯片的支持7.0.3在实际上手前,阅读了 Android 与 Java 的官方开发文档与几篇优秀的相关文章,按照自己的理解和知识水平,整理了一些概念的基本且浅显的解释,方便理解下一步要进行的操作。
Native 层
JNI
NDK
交叉编译、构建系统与 CMake
ABI 与动态链接库
FFmpeg

虽然 Android 系统的许多 API 使用 Java 开发,但许多核心 Android 系统组件和服务(如 ART 和 HAL 等)由 C/C++ 写成,需要以 C/C++ 编写的 Native 库。因此 Android 除了提供开发 Java 代码所需的 JDK (Java Development Kit) 之外,还提供了供开发者进行 Native 层开发的 NDK (Native Development Kit)。
Java 运行于 Java 虚拟机之上,因而实现了易移植、可跨平台运行等特性,但这也使得 Android 需要依赖一些「Native」的代码来访问系统底层,去完成一些 Java 实现不了的任务。也正因如此,C/C++ 这类「原生」的语言也使 Android 程序丧失了跨平台这一特性,在为 Android 编译 C/C++ 程序时需考虑目标机器所使用的 CPU 架构、操作系统版本等。
JNI 即 Java Native Interface,是 Java 提供用来与其他语言编写的程序通信的接口,之中定义了 Java 字节码与 Native 代码的交互方式。这里我们通过 NDK 来使用 JNI,从而实现 Android 程序中 Java 代码与 C/C++ 代码的相互调用。
这里记录一些遇到的问题和自己认为可以暂时过掉的一些 quick answer:
NDK 即 Native Development Kit,在这里可以让我们在 Android 开发中使用 C/C++ 语言编写而成的库。
在 Android 开发中,我们应当先在 Java 文件中编写 Native 方法,然后在 C/C++ 文件中实现 Native 方法,接着使用 NDK 的工具链将 C/C++ 代码编译成动态链接库,然后使用 Android Studio 的 Gradle 将我们编译好的库打包到 APK 中。随后在运行程序时,Java 代码就可以通过 Java 原生接口 (JNI) 框架调用库中的 Native 方法。
交叉编译 (Cross Compile),指在与目标机器不同处理器架构的编译机器上,编译出适合目标机器架构运行的程序,我们如果要在 x86_64 平台的 PC 中编译出运行于 arm 架构的 Android 设备中的 C/C++ 程序,就需要用到交叉编译工具链 (Toolchain),即用于交叉编译的一系列工具。这里我们使用 NDK 提供的默认工具链(从 r19 版本之后开始,NDK 不再支持独立工具链)。
当我们编译一个 .c 文件的时候,我们可以直接将其丢进 gcc 中编译;但当我们需要编译一个项目的一系列 .c 文件或整合已有的库时,一股脑丢进去编译显然就会大乱套了,于是我们需要一个构建系统来管理这个项目的编译。例如在 Windows 下有 Visual Studio 的 .sln 文件,macOS 下有 Xcode 的 .xcodeproj 文件,Unix 下可以使用 Make 的 Makefile 文件或 Ninja 的 .ninja 文件等等。
这些构建系统的构建文件可以指导编译器或编译工具链来编译整个项目。像 Makefile 或者 .ninja 这样的较为简单的构建系统文件,我们可以尝试手写一份进行构建,但当我们的构建以及编译要涉及跨平台交叉编译时,我们便要针对不同的目标平台编写不同的文件,因此目前更通用的做法是使用像 CMake 这样更高等级的构建系统来生成这些构建文件。
CMake 是 Cross platform Make 的简写。CMake 是一个开源的跨平台编译工具(又被称为「元构建系统」),其可以根据 CMakeLists.txt 配置文件来生成一个指导工具链进行编译的标准构建文件(不同平台下可选择生成不同构建系统的构建文件),随后工具链便可根据该构建文件将源代码编译成动态链接库。
Android Studio 推荐使用 CMake + Ninja + NDK 内置工具链来进行 Native 库开发。
ABI 即应用二进制接口 (Application Binary Interface)。ABI 中包含以下信息
可使用的 CPU 指令集(和扩展指令集)。
运行时内存存储和加载的字节顺序。Android 始终是 little-endian(小端法)。
在应用和系统之间传递数据的规范(包括对齐限制),以及系统调用函数时如何使用堆栈和寄存器。
可执行二进制文件(例如程序和共享库)的格式,以及它们支持的内容类型。
如何重整 C++ 名称。
当我们编写 Java 代码时,由于 Java 运行在 Java 虚拟机上,我们无需关心设备具体的硬件条件、架构或 CPU,但当我们需要在 Android 程序中使用 Native 代码时,由于不同的 Android 设备使用不同的 CPU,而不同的 CPU 支持不同的指令集,CPU 与指令集的每种组合都有专属的 ABI。因此我们需要针对不同的 Android ABI,构建并编译出适应于不同 ABI 的 .so 动态链接库。
当我们将这些为不同 ABI 所编译的库打包成 APK 时,这些 APK 自然也是只有特定 ABI 的 Android 设备才能安装使用的。例如:苹果芯片支持的 arm64-v8a 镜像无法安装专门为 armeabi-v7a 编译的 APK 包,我们在编译的时候可以在 Gradle 的 ndk.abiFilters 参数中控制要编译打包何种 ABI 的库。
FFmpeg 是一套 C 语言下开发的开源、跨平台的音视频录制、转码及流处理的完整解决方案,被不少开源项目所使用。
经过前述文字的梳理,想必已经对 Android 下使用 Native 库的的基本逻辑与行为有了一定的理解,我们再进行梳理:
先下载一份 FFmpeg 源码 进行编译,你可以选择别人编译好的 FFmpeg build 或者使用别人写好的编译脚本,省去不少麻烦的同时跳过这一步,这里推荐 FFmpegKit。
Android 工程中只支持导入 .so 结尾的动态库,形如:libavcodec-57.so 。但是 FFmpeg 编译生成的动态库默认格式为 xx.so.版本号 ,形如:libavcodec.so.57 , 所以需要修改 FFmpeg 根目录下的 configure 文件,使其生成以 .so 结尾格式的动态库:
# 将 configure 文件中 build settings 下的:
SLIBNAME_WITH_MAJOR='$(SLIBNAME).$(LIBMAJOR)'
LIB_INSTALL_EXTRA_CMD='$$(RANLIB) "$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_VERSION)'
SLIB_INSTALL_LINKS='$(SLIBNAME_WITH_MAJOR) $(SLIBNAME)'
#替换为:
SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)'
LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)'
SLIB_INSTALL_LINKS='$(SLIBNAME)'
FFmpeg 已经为我们准备好了 Makefile 可以直接用于构建,还为我们提供了 configure 程序可以调节编译的设置,configure 提供许多参数可供选择,如编译模块,目标平台、编译工具链等等,通常的做法是编写一份脚本进行设置与构建,我们在目录下新建一个 build.sh 脚本文件。
在编译的过程中,由于自己技术水平过低,照抄别人的攻略脚本的过程中走了不少弯路。
这里是本人用于在 macOS 下编译 arm64-v8a 的 FFmpeg 使用的脚本。请务必根据说明与自己的工具链情况进行修改。如果在编译过程中遇到问题,一定要先查 log 以及翻阅官方文档,此处参照的 文档。
NDK_ROOT= #NDK 根目录
TOOLCHAIN=$NDK_ROOT/toolchains/llvm/prebuilt/darwin-x86_64
#工具链目录 目前 NDK 在 M1 还只能在 Rosseta 转译下使用 x64 的工具链
export PATH=$PATH:$TOOLCHAIN/bin
target_arch=aarch64
target_host=aarch64-linux
#编译目标平台
toolchain_prefix=$target_host-android21
#在 configure 中定义了新变量
#target_arch=arm
#target_host=armv7a-linux
#toolchain_prefix=$target_host-androideabi21
#这里是编译armv7的选项
#这里的变量设置以及接下来对 configure 的编辑非常重要
#如果照抄之前的过期博文(或此文)设置脚本会导致编译失败
#详见下面的分析
PREFIX= #编译输出路径
ANDROID_API=21 #最小API
./configure \
--prefix=$PREFIX \
#设定输出路径
--enable-shared \
#生成动态链接库
--disable-static \
#不生成静态库
--enable-postproc \
--enable-jni \
--enable-cross-compile \
#启用交叉编译
--extra-cflags="-D__ANDROID__API__=21 -U_FILE_OFFSET_BITS" \
--cross-prefix=$target_host- \
#设定交叉编译目标前缀
--cross_prefix_clang=$toolchain_prefix- \
--arch=$target_arch \
#设定目标框架
--target-os=android \
#设定目标平台系统 iOS = darwin
--sysroot=$TOOLCHAIN/sysroot
#设定sysroot目录
make clean
make -j4
make install
说明:
在编写脚本前,请先 cd 到工具链 bin 目录下,ls 查看工具链程序的文件名格式,在本人使用的 NDK 23.1.7779620 darwin 工具链中情况如下:
……
aarch64-linux-android-as
aarch64-linux-android21-clang
aarch64-linux-android21-clang++
aarch64-linux-android22-clang
aarch64-linux-android22-clang++
aarch64-linux-android23-clang
aarch64-linux-android23-clang++
aarch64-linux-android24-clang
aarch64-linux-android24-clang++
aarch64-linux-android26-clang
aarch64-linux-android26-clang++
aarch64-linux-android27-clang
aarch64-linux-android27-clang++
aarch64-linux-android28-clang
………
llvm-ar
llvm-as
llvm-cfi-verify
llvm-config
llvm-cov
llvm-cxxfilt
llvm-dis
llvm-dwarfdump
llvm-dwp
llvm-lib
llvm-link
llvm-lipo
llvm-modextract
llvm-nm
……
可以看到,NDK 提供的 clang 都是带有 Android 版本号前缀的,此时打开 configure 文件的源码,搜索到 if test "$target_os" = android 这一行,查看 Android 编译设置,可以发现许多问题:
这里的文件名全部设置成以我们输入的 cross_prefix 为前缀,但经过我们的查看,我们的文件名前缀实际上是形如 aarch64-linux-android21 这样 ${cross_prefix}-android+版本号 的格式。
这里将 cc_default 重写为了 clang,但没有重写 cxx_default。
这里的 strip 、ar、pkg-config 与 nm 工具也设置成了以 cross_prefix 为前缀,但实际上,可以看到我们的几个文件名前缀实际上是 llvm- ,在 Android 官方文档中也可以得知 binutils 工具(例如 ar 和 strip)不需要前缀,因为它们不受 minSdkVersion 影响。而 pkg-config 并没有内置在工具链中,需要我们通过包管理器手动获取。(本人没有安装的情况下编译也没有失败)
brew install pkg-config
请务必注意,这里的实际设置情况请以你自己的 NDK 工具链为参照。
为了保证正确编译,configure 的相关代码修改如下:
set_default target_os
if test "$target_os" = android; then
cc_default="clang"
cxx_default="clang++" #将cxx_default重写
fi #注:ndk r17版本后已弃用gcc
ar_default="llvm-${ar_default}" #将前缀修改为llvm-
cc_default="${cross_prefix_clang}${cc_default}" #在CMDLINE_SET中定义一个新变量cross_prefix_clang并在脚本中输入
cxx_default="${cross_prefix_clang}${cxx_default}" #也可以直接修改成${cross_prefix}-android21-${cxx_default}
nm_default="llvm-${nm_default}"
pkg_config_default="${pkg_config_default}" #使用我们安装的pkg-config
ranlib_default="llvm-${ranlib_default} -D"
strip_default="llvm-${strip_default}"
windres_default="${cross_prefix}${windres_default}"
运行脚本,如果编译成功可以看到我们设置的输出目录下已经出现了include、bin、share和 lib 这几个文件夹,lib 文件夹内就是我们需要的编译好的 FFmpeg 动态链接库。
得到了 FFmpeg 的动态链接库之后,我们还不能直接在 Android 应用中使用。因为我们还没有实现 Java 代码与 C 代码的互相通信:JNI。不少教程使用的是 NDK 提供的 ndk-build,但 Android 官方现在更加推荐使用 CMake,我们可以在 Gradle 插件的帮助下直接调用 CMake 而免去命令行操作之劳,请先检查是否安装 Ninja。
可以简单地按以下步骤操作:
新建一个项目,在 app 目录下右键,选择 Add C++ to Module ,Android Studio 会在 main 目录下自动生成 ProjectName.cpp 与 CMakeLists.txt,打开 CMakeLists.txt 观察格式,生成的注释已经很好读了,这里不再赘述。
……
add_library(
ffmpegtest #库名
SHARED
ffmpegtest.cpp) #实现JNI方法的cpp代码 自动生成文件名为项目名
……
在 cpp 目录下新建 lib/arm64-v8a 文件夹,将我们上一步骤编译好的 FFmpeg 的 lib 目录下 .so 格式的动态链接库粘贴进去,并将 include 文件夹复制粘贴到 cpp 目录下。(如果编译了其他 ABI 的库,在 lib 目录下新建以 ABI 为名的子目录存放)
打开 CMakeLists.txt 进行编辑,添加以下内容:
add_library(avcodec
#库名 注意无lib前缀
SHARED
#SHARED 表示动态链接库
IMPORTED)
#IMPORTED 表示外部导入库
set_target_properties(avcodec
#设置avcodec库的导入路径
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/lib/${CMAKE_ANDROID_ARCH_ABI}/libavcodec.so)
#CMAKE_SOURCE_DIR是CMakeLists.txt所在目录 请勿省略
#CMAKE_ANDROID_ARCH_ABI是下文在Gradle中设置的abifilter参数
#此处是arm64-v8a 请勿省略
…… #如上格式添加所有FFmpeg动态链接库
include_directories(${CMAKE_SOURCE_DIR}/include)
#添加FFmpeg头文件
target_link_libraries(
ffmpegtest
avfilter
avformat
#将所有动态链接库与ffmpegtest库(实现了JNI)链接
#至此 可以在java代码内通过JNI调用FFmpeg库中函数
#同时在ffmpegtest.cpp中的JNI函数中调用Java中的方法
#从而实现在Android中实现与FFmpeg库交互
avdevice
avcodec
avutil
swresample
swscale
postproc
${log-lib})
在类或应用中初始化库,在类中编写一个没有函数体的 native 方法并调用,这个时候方法会报错,⌥(Alt) + Enter 让 Android Studio 帮我们在 ffmpegtest.cpp 内生成 JNI 函数。
private native void run();
static{
System.loadLibrary("ffmpegtest");
}
……
#include <jni.h>
#include <android log.h=""> //导入Android log头文件
extern "C"{ //FFmpeg由C写成 注意使用C关键字括起来
#include "libavcodec/avcodec.h" //导入FFmpeg依赖库头文件
JNIEXPORT void JNICALL
Java_com_example_ffmpegtest_MainActivity_run(JNIEnv *env, jobject thiz) {
__android_log_print(ANDROID_LOG_INFO,"FFmpegTag",
"avcodec_configuration():\n%s",avcodec_configuration());
//输出avcodec配置到logcat
}
}
打开 module 等级的 build.gradle
ndkVersion 与 cmake.versiondefaultConfig.externalNativeBuild 中添加 ndk{abiFilters "arm64-v8a"} ,如果不指定这个参数,Gradle 会构建所有 ABI 的应用,如果你还编译了其他 ABI 的 FFmpeg 并且想要为其他 ABI 构建应用,在这里添加。sourceSets.main.jniLibs.srcDirs,这个参数已经过时而且会导致构建失败build 并运行,可以在 logcat 中看到 cpp 代码中从 FFmpeg 函数中发出的信息。至此,我们初步实现了编译并在 Android 中集成了 FFmpeg。
问题排查:
build.gradle 中定义,是否正确配置了 CMake 和 NinjaabiFilters【联创の炼金工坊】Android NDK 之 Hello World
Android 集成 FFmpeg (一) 基础知识及简单调用_yhao的博客-CSDN博客
将 NDK 与其他构建系统配合使用 | Android NDK | Android Developers
Android ABI | Android NDK | Android Developers
我正在用Ruby编写一个简单的程序来检查域列表是否被占用。基本上它循环遍历列表,并使用以下函数进行检查。require'rubygems'require'whois'defcheck_domain(domain)c=Whois::Client.newc.query("google.com").available?end程序不断出错(即使我在google.com中进行硬编码),并打印以下消息。鉴于该程序非常简单,我已经没有什么想法了-有什么建议吗?/Library/Ruby/Gems/1.8/gems/whois-2.0.2/lib/whois/server/adapters/base.
我是Google云的新手,我正在尝试对其进行首次部署。我的第一个部署是RubyonRails项目。我基本上是在关注thisguideinthegoogleclouddocumentation.唯一的区别是我使用的是我自己的项目,而不是他们提供的“helloworld”项目。这是我的app.yaml文件runtime:customvm:trueentrypoint:bundleexecrackup-p8080-Eproductionconfig.ruresources:cpu:0.5memory_gb:1.3disk_size_gb:10当我转到我的项目目录并运行gcloudprevie
我不知道为什么,但是当我设置这个设置时它无法编译设置:static_cache_control,[:public,:max_age=>300]这是我得到的syntaxerror,unexpectedtASSOC,expecting']'(SyntaxError)set:static_cache_control,[:public,:max_age=>300]^我只想将“过期”header设置为css、javaascript和图像文件。谢谢。 最佳答案 我猜您使用的是Ruby1.8.7。Sinatra文档中显示的语法似乎是在Ruby1.
2022/8/4更新支持加入水印水印必须包含透明图像,并且水印图像大小要等于原图像的大小pythonconvert_image_to_video.py-f30-mwatermark.pngim_dirout.mkv2022/6/21更新让命令行参数更加易用新的命令行使用方法pythonconvert_image_to_video.py-f30im_dirout.mkvFFMPEG命令行转换一组JPG图像到视频时,是将这组图像视为MJPG流。我需要转换一组PNG图像到视频,FFMPEG就不认了。pyav内置了ffmpeg库,不需要系统带有ffmpeg工具因此我使用ffmpeg的python包装p
最近因为项目需要,需要将Android手机系统自带的某个系统软件反编译并更改里面某个资源,并重新打包,签名生成新的自定义的apk,下面我来介绍一下我的实现过程。APK修改,分为以下几步:反编译解包,修改,重打包,修改签名等步骤。安卓apk修改准备工作1.系统配置好JavaJDK环境变量2.需要root权限的手机(针对系统自带apk,其他软件免root)3.Auto-Sign签名工具4.apktool工具安卓apk修改开始反编译本文拿Android系统里面的Settings.apk做demo,具体如何将apk获取出来在此就不过多介绍了,直接进入主题:按键win+R输入cmd,打开命令窗口,并将路
我正在尝试使用以下代码通过将ffmpeg实用程序作为子进程运行并获取其输出并解析它来确定视频分辨率:IO.popen'ffmpeg-i'+path_to_filedo|ffmpegIO|#myparsegoeshereend...但是ffmpeg输出仍然连接到标准输出并且ffmepgIO.readlines是空的。ffmpeg实用程序是否需要一些特殊处理?或者还有其他方法可以获得ffmpeg输出吗?我在WinXP和FedoraLinux下测试了这段代码-结果是一样的。 最佳答案 要跟进mouviciel的评论,您需要使用类似pope
我正在关注Hartl的railstutorial.org并已到达11.4.4:Imageuploadinproduction.我做了什么:注册亚马逊网络服务在AmazonIdentityandAccessManagement中,我创建了一个用户。用户创建成功。在AmazonS3中,我创建了一个新存储桶。设置新存储桶的权限:权限:本教程指示“授予上一步创建的用户读写权限”。但是,在存储桶的“权限”下,未提及新用户名。我只能在每个人、经过身份验证的用户、日志传送、我和亚马逊似乎根据我的名字+数字创建的用户名之间进行选择。我已经通过选择经过身份验证的用户并选中了上传/删除和查看权限的框(而不
我正在尝试循环哈希数组。当我到达获取枚举器开始循环的位置时,出现以下错误:undefinedmethod`[]'fornil:NilClass我的代码如下所示:defextraireAttributs(attributsParam)classeTrouvee=falsescanTrouve=falseownerOSTrouve=falseownerAppTrouve=falseresultat=Hash.new(0)attributs=Array(attributsParam)attributs.eachdo|attribut|#CRASHESHERE!!!typeAttribut=a
我正在尝试用Ruby(Rails)编写一个正则表达式,以便用户名的字符仅包含数字和字母(也没有空格)。我有这个正则表达式,/^[a-zA-Z0-9]+$/,但它似乎没有用,我在Rails中收到一个错误,说“The如果正则表达式使用多行anchor(^或$),这可能会带来安全风险。您是要使用\A和\z,还是忘记添加:multiline=>true选项?"我的user.rb模型中此实现的完整代码是:classUser我做错了什么以及如何修复此正则表达式,使其仅对数字和字母有效而不对空格有效?谢谢。 最佳答案 简短回答:使用/\A[a-z
是否有适用于Ruby语言的.NETFramework编译器?我听说过DLR(动态语言运行时),这是否将使Ruby能够用于.NET开发? 最佳答案 IronRuby是Microsoft支持的项目,建立在动态语言运行时之上。 关于.net-是否有Ruby.NET编译器?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/199638/