该项目是基于FFmpeg6.0环境编写。文中涉及代码在不同版本可能会有变动
从以下两个地址任选其一下载源码:
执行根目录的configure文件,
./configure
该方式会生成相关文件,否则项目不可运行,
参考如下:
https://ffmpeg.org/doxygen/6.0/md_INSTALL.html
编译过程中会出现错误,根据错误进行修改,通常会缺少pkg-config
安装包,这个会在编译时候提示缺少这个,不提示就是不缺少。安装如下:
brew install pkg-config
整体错误大概分为缺少依赖的安装包和环境。这个根据提示进行安装即可。没办法绕过
编译成.so参考如下
Android 集成 FFmpeg (一) 基础知识及简单调用
https://blog.csdn.net/yhaolpz/article/details/76408829
如果使用sh文件进行编译的话会出现权限拒绝的情况,可以使用以下命令:
chmod +x build_android.sh
然后再执行sh文件。
以下为sh配置的文件,不需要在项目中配置环境变量,但是所有文件中用到的具体路径需要进行修改。
需要注意的是每次添加新的命令需要使用\
进行分割。在进行配置的时候对于高级选项的命令,比如优化部分--disable-neon
最好不要使用,否则容易出现错误。相关命令可以通过configure --help
进行查看
#!/bin/bash
NDK=/Users/c/Documents/sdk/android/ndk/25.1.8937393
TOOLCHAIN_ROOT_DIR=darwin-x86_64
TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/$TOOLCHAIN_ROOT_DIR/
API=21
#要编译的ffmpeg内容方法
function build_android {
echo "Compiling FFmpeg for $CPU"
./configure \
--prefix=$PREFIX \
--disable-hwaccels \
--disable-gpl \
--disable-postproc \
--disable-programs \
--disable-mediacodec \
--disable-decoder=h264_mediacodec \
--disable-static \
--disable-vulkan \
--disable-doc \
--disable-ffmpeg \
--disable-ffplay \
--disable-ffprobe \
--disable-avdevice \
--disable-doc \
--disable-symver \
--disable-x86asm \
--disable-filters \
--enable-cross-compile \
--enable-jni \
--enable-shared \
--cross-prefix=$CROSS_PREFIX \
--nm=$NM \
--strip=$STRIP \
--pkgconfigdir=$PKG_CONFIG_DIR \
--pkg-config=$PKG_CONFIG \
--target-os=android \
--arch=$ARCH \
--cpu=$CPU \
--cc=$CC \
--cxx=$CXX \
--sysroot=$SYSROOT \
--extra-cflags="-Os -fpic $OPTIMIZE_CFLAGS" \
--extra-ldflags="$ADDI_LDFLAGS" \
$ADDITIONAL_CONFIGURE_FLAG
make clean
make
make install
echo "The Compilation of FFmpeg for $CPU is completed"
}
#接下来是根据需要来决定
#armv8-a
ARCH=arm64
CPU=armv8-a
CC=$TOOLCHAIN/bin/aarch64-linux-android$API-clang
CXX=$TOOLCHAIN/bin/aarch64-linux-android$API-clang++
LLVM_TOOLCHAIN=$TOOLCHAIN/bin
PKG_CONFIG_DIR=/opt/homebrew/Cellar/pkg-config/0.29.2_3
PKG_CONFIG=$PKG_CONFIG_DIR/bin/pkg-config
NM=$LLVM_TOOLCHAIN/llvm-nm
STRIP=$LLVM_TOOLCHAIN/llvm-strip
SYSROOT=$TOOLCHAIN/sysroot
CROSS_PREFIX=$TOOLCHAIN/bin/aarch64-linux-android-
PREFIX=$(pwd)/android/$CPU
OPTIMIZE_CFLAGS="-march=$CPU"
build_android
其实最初文件可以写成以下方式:
export ANDROID_NDK=<NDK路径>
export TOOLCHAIN=$ANDROID_NDK/toolchains/llvm/prebuilt/darwin-x86_64
export SYSROOT=$ANDROID_NDK/toolchains/llvm/prebuilt/darwin-x86_64/sysroot
./configure \
--prefix=$PWD/build \
--enable-shared \
--disable-static \
--disable-doc \
--disable-programs \
--disable-symver \
--arch=arm64 \
--target-os=android \
--cc=$TOOLCHAIN/bin/aarch64-linux-android21-clang \
--cxx=$TOOLCHAIN/bin/aarch64-linux-android21-clang++ \
--cross-prefix=$TOOLCHAIN/bin/aarch64-linux-android- \
--sysroot=$SYSROOT
但是编译中会出现各种错误,最终要么重新指定路径,要么禁止使用,就改成了如此。
关于命令的含义参考如下:
FFmpeg中configure的参数配置解释
对于pkg-config
的配置如果编译时候找不到,要么禁用--disable-pkgconfig
,要么使用以下方式进行配置
./configure --pkg-config=/path/to/pkg-config --pkgconfigdir=/path/to/pkgconfigdir
最终会在ffmpeg的根目录生成android/
目录(这里是android/
),里面共有三个文件夹include
、lib
、share
。.so位于lib文件夹下:
shell脚本参考如下:
shell脚本语言(超全超详细)
在FFmpeg编译成.so完成后,需要添加JNI编码,否则的话无法在Android项目中直接使用。该操作需要配置ndk-build环境
参考链接:
Android 集成 FFmpeg (一) 基础知识及简单调用
这里使用文本工具进行代码编写和编译,不使用Android Studio进行编译。
首先在上述的FFmpeg根目录下后面生成的文件夹android
中创建文件ndkBuild\com\jni\FFmpeg.java
和ndkBuild\jni
文件夹
FFmpeg.java
代码如下:
package com.jni;
public class FFmpeg {
public static native void run();
}
然后在ndkBuild
目录下执行命令生成com_jni_FFmpeg.h
文件
该命令的中间的.
表示当前路径,可以更改为其余路径,后面是具体的类的名字
javah -classpath . com.jni.FFmpeg
然后在ndkBuild\jni
文件夹下创建com_jni_FFmpeg.c
、Android.mk
、Application.mk
代码如下:
com_jni_FFmpeg.c
#include
#include "com_jni_FFmpeg.h"
#include
#include
#include
#include
#include
#include
#include
# 这里是把AV_CODEC_ID_MP2的编码信息打印一下
JNIEXPORT void JNICALL Java_com_jni_FFmpeg_run(JNIEnv *env, jclass obj) {
char info[40000] = {0};
const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_MP2);
AVCodecContext *avcodecContext = avcodec_alloc_context3(codec);
const char *license = avcodec_license();
if(avcodecContext->codec_type == AVMEDIA_TYPE_VIDEO){//音频
sprintf(info,"%s[Video]",info);
}else{//视频
sprintf(info,"%s[Audio]",info);
}
sprintf(info,"%s[%10s]\n",info,codec->name);
avcodec_free_context(&avcodecContext); //释放内存
__android_log_print(ANDROID_LOG_INFO,"myTag","info:\n%s",info);
}
Android.mk
LOCAL_PATH:= $(call my-dir)
FFMPEG_PATH:=/Users/c/Documents/CTest/ffmpeg-6.0
FFMPEG_ANDROID:=$(FFMPEG_PATH)/android/armv8-a
INCLUDE_PATH:=$(FFMPEG_ANDROID)/include
FFMPEG_LIB_PATH:=$(FFMPEG_ANDROID)/lib
include $(CLEAR_VARS)
LOCAL_MODULE:= libavcodec
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavcodec.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE:= libavformat
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavformat.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE:= libswscale
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libswscale.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE:= libavutil
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavutil.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE:= libavfilter
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavfilter.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE:= libswresample
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libswresample.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := ffmpeg
LOCAL_SRC_FILES := com_jni_FFmpeg.c
LOCAL_C_INCLUDES := $(FFMPEG_PATH)
LOCAL_LDLIBS := -lm -llog
LOCAL_SHARED_LIBRARIES := libavcodec libavfilter libavformat libavutil libswresample libswscale
include $(BUILD_SHARED_LIBRARY)
Application.mk
APP_ABI := arm64-v8a
APP_PLATFORM=android-21
然后在当前目录下执行命令:
ndk-build
生成Android.mk
中定义的.so
文件。最终会在ndkBuild
下面生成两个文件夹libs
、obj
。在libs\arm64-v8a
下面会有生成的.so库。共七个libavcodec.so
libavfilter.so
libavformat.so
libavutil.so
libswresample.so
libswscale.so
libffmpeg.so
。其中libffmpeg.so
为编写的JNI生成的so库,其余为依赖库。
将上述的七个.so库复制到项目的libs文件夹下,可以将整个libs文件夹对Android项目下的libs进行覆盖。app目录下的build.gradle
修改如下:
android{
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
}
将刚才编写的java文件连同com.jni的文件夹一起复制到src目录下。整体结构如下:
如果将.so连同arm64-v8a文件夹复制到main下面的jniLibs文件夹下面则不需要对build.gradle进行修改。如下
然后在项目中调用代码
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val btn: Button = findViewById(R.id.button)
btn.setOnClickListener {
FFmpeg.run()
}
FFmpeg.test()
}
}
可以在logcat下看到结果
2023-06-08 11:36:01.783 5346-5346 myTag com.test.ffmpeg I info:
[Audio][ mp2]
在学习一个技巧的时候最好根据官方来进行学习,因为其余人的教程可能存在无法实时更新导致的api变动问题,这样使用旧版代码可能无法运行,也就无从下手。在学习ffmpeg的开发过程中主要有以下几个步骤
configure --help
进行查看FFmpeg本身是一个工具系列的库,是一个开源性质的库,所以教程相对于那些商业化的框架会比较难以上手,比如OkHttp
框架。最初通过浏览器搜索的结果,代码基本上都难以运行,大部分都是api过时,这样其实不利于入门,毕竟通过别人的代码学会了,但是api过时了,又不知道怎么更新api。通过仔细查阅官方网站整理了一条路径(不一定是最优学习方式,有更好的可以留言)。
首先官网提供了必要的api文档和一些基本的示例(虽然示例可能不是我们想象中的示例程序)。通过官方文档我们知道整体文档分为: 命令行工具文档、组件文档、图书馆文件、一般文件、API文档、社区贡献文档。
通过每一个都点进去后可以简单知道,内容很多,但是作为初学者来说,实在是难以接受,茫然无措,不知道从何开始。如果是用命令行就可以完成需求的话,当然命令行工具文档是很好的选择,但是如果需要自己通过api开发,就只能查看api文档了。其余的文档可以等后面熟悉了再进行阅读。
api文档位于:https://ffmpeg.org/doxygen/trunk/index.html。每天都会更新。整体页面如下
通过此我们知道的有用信息如下:
整个库分为八个部分
在上面的Modules选项下面可以看到都有哪些模块:
通过Examples选项下面可以看到有哪些例子
如果恰巧自己想实现的需求是例子中所有的,那就非常完美,可是点进去例子后发现,例子对初学者来说写的很复杂。我们最初只想写一个简单的测试例子,测试程序是否编译通过,比如打印下ffmpeg的基础信息,打印下基本的信息。如果不这样做,直接完成一个音视频播放的例子,无疑会显得特别困难。然后我们进行需求分析,什么样的场景满足这样的需求?通过上述信息搜集,可以知道我们打印下ffmpeg支持的格式会比较简单,也不需要进行文件读写。这里的话就锁定使用libavformat。点击进去后发现内容如下:
模块如下,文件有三个头文件,一个avformat.h,剩下两个是版本信息,其余的库也是如此,我们的主要参考api就位于第一个文件中,点击进去后发现,里面包含了定义的常量和函数。
被标记的就是会用到的遍历函数。我们点击第一个函数。
这里是函数的定义,解释了需要从传参数为NULL开始便利,下面是引用该函数的位置(有的api下面还要示例程序的引用地址,不过这个api没有)。选择第一个引用的位置点击进去查看:
这里是一个简单的使用方式。将该代码拷贝到实际项目中去,然后别忘了添加相应的头文件,格式如下:<库名/头文件名.h> #include
。相应的库名和头文件名在上述步骤已经给出。
所以整体代码如下:
#include
#include "com_jni_FFmpeg.h"
#include
#include
#include
#include
JNIEXPORT void JNICALL Java_com_jni_FFmpeg_run(JNIEnv *env, jclass obj) {
const AVOutputFormat *fmt = NULL;
const AVOutputFormat *fmt_found = NULL;
void *i = 0;
int score_max, score;
while ((fmt = av_muxer_iterate(&i))) {
score = 0;
const char *fmtName = fmt->name;
__android_log_print(ANDROID_LOG_INFO,"myTag","fmtName:\n%s",fmtName);
}
}
运行程序后可再logcat控制台看到如下内容:
2023-06-08 14:56:30.886 8824-8824 myTag com.test.ffmpeg I fmtName:
a64
2023-06-08 14:56:30.886 8824-8824 myTag com.test.ffmpeg I fmtName:
ac3
2023-06-08 14:56:30.886 8824-8824 myTag com.test.ffmpeg I fmtName:
adts
2023-06-08 14:56:30.886 8824-8824 myTag com.test.ffmpeg I fmtName:
adx
2023-06-08 14:56:30.886 8824-8824 myTag com.test.ffmpeg I fmtName:
aiff
2023-06-08 14:56:30.886 8824-8824 myTag com.test.ffmpeg I fmtName:
alp
2023-06-08 14:56:30.886 8824-8824 myTag com.test.ffmpeg I fmtName:
amr
2023-06-08 14:56:30.886 8824-8824 myTag com.test.ffmpeg I fmtName:
amv
...
...
不过这种代码编写方式不太好,没办法发挥ide的特性,想了解使用ide编写代码的方式可以查看Mac环境下使用Clion编译测试运行Ffmpeg。
下面是根据网上其余人的需求写的代码,打印出支持的编解码类型
#include
#include "com_jni_FFmpeg.h"
#include
#include
#include
#include
#include "libavcodec/avcodec.h" //编码器格式
#include
#include
void iterateAvOutFormat(){
const AVOutputFormat *fmt = NULL;
const AVOutputFormat *fmt_found = NULL;
void *i = 0;
int score_max, score;
while ((fmt = av_muxer_iterate(&i))) {
score = 0;
const char *fmtName = fmt->name;
__android_log_print(ANDROID_LOG_INFO,"myTag","fmtName:\n%s",fmtName);
}
}
void printMp2Decoder(){
char info[40000] = {0};
const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_MP2);
AVCodecContext *avcodecContext = avcodec_alloc_context3(codec);
const char *license = avcodec_license();
if(avcodecContext->codec_type == AVMEDIA_TYPE_VIDEO){//音频
sprintf(info,"%s[Video]",info);
}else{//视频
sprintf(info,"%s[Audio]",info);
}
sprintf(info,"%s[%10s]\n",info,codec->name);
avcodec_free_context(&avcodecContext); //释放内存
__android_log_print(ANDROID_LOG_INFO,"myTag","info:\n%s",info);
}
void iterateMimeType(AVCodecDescriptor *desc){
if (desc == NULL){
return;
}
const char *const *const mime_types = desc->mime_types; //这是个数组类型
if (mime_types == NULL){
return;
}
char** current_str = mime_types;
while (*current_str != NULL) {
// printf("类型:%s\n", *current_str);
__android_log_print(ANDROID_LOG_INFO,"myTag","mimeType:\n%1c",*current_str);
current_str++;
}
}
//打印编码信息
//代码源自: https://ffmpeg.org/doxygen/trunk/opt__common_8c_source.html#l00635
void iterateCodec(){
const AVCodecDescriptor *desc = NULL;
const AVCodecDescriptor **codecs;
unsigned nb_codecs = 0, i = 0;
while ((desc = avcodec_descriptor_next(desc))){
nb_codecs++; //计算有多少个编码器
enum AVCodecID avCodecId = desc->id;
enum AVMediaType avCodecType = desc->type;
const char *avCodecName = desc->name;
const char *avCodecLongName = desc->long_name;
const char *const *const mime_types = desc->mime_types; //这是个数组类型
__android_log_print(ANDROID_LOG_INFO,"myTag","avCodecName:\n%1s, avCodecLongName: %2s",avCodecName, avCodecLongName);
iterateMimeType(desc);
}
}
JNIEXPORT void JNICALL Java_com_jni_FFmpeg_run(JNIEnv *env, jclass obj) {
// printMp2Decoder();
// iterateAvOutFormat();
iterateCodec();
}