Android NDK ffmpeg 音视频开发实战
文章目录
- 接入FFmpeg
- 1.下载FFmpeg 源码
- 2.编译FFmpeg.so库
- 异常处理
- 3.自定义FFmpeg交互so库创建
- 4.配置CMakeLists.txt
- 5.CMakeLists.txt 环境配置
- 6.Native与Java层调用
- 解码器准备
接入FFmpeg
1.下载FFmpeg 源码
FFmpeg官网地址
2.编译FFmpeg.so库
- 移动 FFmpeg 源码文件夹至 Android Studio 的 cpp 包下(也可以不移)
- 在 FFmpeg 文件夹内创建用来编译 .so 库 的 sh脚本
编译脚本是基于以下 Android 各较新版本:
库 | 版本 |
---|---|
Android SDK | 35 |
NDK | 26 |
CMake | 3.6 |
JDK | 11 |
Gradle | 8.11.0 |
Android Gradle Plugin | 8.6.0 |
#!/usr/bin/env bash
# 声明脚本解释器为 bash# -------------------- 配置路径 --------------------
# ‼️修改成自己的NDK版本和路径
NDK=/Users/xxx/Library/Android/sdk/ndk/26.1.10909125
# Android NDK 路径HOST_TAG=darwin-x86_64
# 主机系统标识,macOS 64位TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/$HOST_TAG
# LLVM 交叉编译工具链路径SYSROOT=$TOOLCHAIN/sysroot
# sysroot 路径,NDK 中包含标准库等文件的根目录API=21
# Android API 级别FFMPEG_SRC=$(pwd)
# 当前工作目录,即 ffmpeg 源码根目录OUTPUT_ROOT=$FFMPEG_SRC/android-build
# 编译输出目录ARCHS=("armeabi-v7a" "arm64-v8a" "x86_64")
# 需要编译的架构列表# -------------------- 函数:编译单个架构 --------------------
build_one() {ARCH=$1# 接收参数,架构名称echo "==================== 编译架构: $ARCH ===================="# 打印当前编译的架构,方便查看进度case $ARCH inarmeabi-v7a)TARGET=armv7a-linux-androideabi# armeabi-v7a 架构对应的目标三元组;;arm64-v8a)TARGET=aarch64-linux-android# arm64-v8a 架构对应的目标三元组;;x86_64)TARGET=x86_64-linux-android# x86_64 架构对应的目标三元组;;*)echo "❌ 未知架构: $ARCH"exit 1# 如果传入架构不在已知列表,退出脚本;;esacCC="$TOOLCHAIN/bin/clang --target=${TARGET}${API} --sysroot=$SYSROOT"# 定义 C 编译器,带上目标三元组和 API 版本,指定 sysrootCXX="$TOOLCHAIN/bin/clang++ --target=${TARGET}${API} --sysroot=$SYSROOT"# 定义 C++ 编译器,参数同上PREFIX=$OUTPUT_ROOT/$ARCH# 该架构编译后文件的安装目录mkdir -p $PREFIX# 创建安装目录,若不存在则新建cd $FFMPEG_SRC# 进入 ffmpeg 源码目录,准备开始编译make clean# 清理之前的编译结果,避免干扰./configure \--prefix=$PREFIX \--target-os=android \--arch=$ARCH \--enable-cross-compile \--cc="$CC" \--cxx="$CXX" \--sysroot=$SYSROOT \--enable-shared \--disable-static \--disable-doc \--disable-programs \--disable-symver \--disable-debug \--disable-asm \--extra-cflags="-Os -fPIC" \--extra-ldflags=""# 调用 ffmpeg 的 configure 脚本,配置编译选项:# --prefix:安装路径# --target-os=android:目标操作系统为 Android# --arch:目标架构# --enable-cross-compile:启用交叉编译# --cc 和 --cxx:指定 C 和 C++ 编译器# --sysroot:指定 sysroot 路径# --enable-shared:生成动态库(so)# --disable-static:不生成静态库# --disable-doc:不生成文档# --disable-programs:不编译 ffmpeg 命令行工具# --disable-symver:关闭符号版本控制# --disable-debug:关闭调试# --disable-asm:禁用汇编优化(可根据需求开启)# --extra-cflags:额外的编译参数,此处优化大小且使用 PIC# --extra-ldflags:额外的链接参数,当前为空if [ $? -ne 0 ]; thenecho "❌ 配置失败: $ARCH"exit 1fi# 如果 configure 出错,则打印失败信息并退出make -j$(sysctl -n hw.ncpu)# 多线程编译,线程数为当前 CPU 核数make install# 安装编译结果到指定目录
}# -------------------- 循环编译所有架构 --------------------
for ARCH in "${ARCHS[@]}"; dobuild_one $ARCH# 按顺序调用函数,编译每个架构
doneecho "==================== 全部架构编译完成 ===================="
# 全部架构编译结束,打印提示
- 编译成功后在路径下可以看到生成的 .so 库和 .h 头文件
cpp/ffmpeg-7.1.1/android-build/arm64-v8a/lib .so库位置
cpp/ffmpeg-7.1.1/android-build/arm64-v8a/include .h 头文件
一共输出7个 .so库:
so库 | 功能 |
---|---|
libavcodec.so | 编解码器核心库 |
libavformat.so | 负责封装/解封装(容器格式,如 mp4、mkv) |
libavutil.so | 工具库(数学、字节序、日志等) |
libswscale.so | 视频像素格式转换(YUV ↔ RGB 等) |
libswresample.so | 音频重采样、通道布局转换 |
libavfilter.so | 滤镜处理(裁剪、特效等) |
libavdevice.so | 设备输入输出(通常可以不用) |
编译.so库完成。
异常处理
- 报错:nasm/yasm not found or too old. Use --disable-x86asm for a crippled build.
-
安装 nasm
brew install nasm
-
- 报错:C compiler test failed.
- 在系统NDK路径确认 clang 编译路径的有效性
$NDK/toolchains/llvm/prebuilt/darwin-arm64/bin/clang
- 在系统NDK路径确认 clang 编译路径的有效性
3.自定义FFmpeg交互so库创建
-
创建一个Java类,自定义调用 native 层的方法
package com.example.video;public class FFmpegLoader {//自定义方法,返回FFmpeg的版本,判断接入成功public native String getFFmpegVersion();static {//自定义so库名System.loadLibrary("native-lib-myFFmpeg");} }
-
通过命令行在当前路径中生成 .h 文件,再放入cpp文件夹中
javac -h ./jni FFmpegLoader.java
.h文件内容:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_example_video_FFmpegLoader */#ifndef _Included_com_example_video_FFmpegLoader #define _Included_com_example_video_FFmpegLoader #ifdef __cplusplus extern "C" { #endif /** Class: com_example_video_FFmpegLoader* Method: getFFmpegVersion* Signature: ()V*/ JNIEXPORT void JNICALL Java_com_example_video_FFmpegLoader_getFFmpegVersion(JNIEnv *, jobject);#ifdef __cplusplus } #endif #endif
移入路径:cpp/com_example_video_FFmpegLoader.h
-
在同一cpp路径下,创建 cpp 文件,会有 #include <libavcodec/avcodec.h> 等文件报红,是因为没有配置CMakeLists.txt 链接,可以先不管,或者先注释掉 FFmpge 库的引用,编译完之后再调用。
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_example_video_FFmpegLoader */ #include <string> extern "C" { #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libavutil/avutil.h> }#ifndef _Included_com_example_video_FFmpegLoader #define _Included_com_example_video_FFmpegLoader #ifdef __cplusplus extern "C" { #endif /** Class: com_example_video_FFmpegLoader* Method: getFFmpegVersion* Signature: ()V*/ JNIEXPORT jstring JNICALL Java_com_example_video_FFmpegLoader_getFFmpegVersion(JNIEnv *env, jobject){const char *version = av_version_info();return env->NewStringUTF(version); }#ifdef __cplusplus } #endif #endif
4.配置CMakeLists.txt
-
外部so库调用 :将编译成功的 so 库(路径:cpp/ffmpeg-7.1.1/android-build/arm64-v8a/lib),集中存放到 jni 文件夹下(/src/main/jni)。并且根据 不同架构分别存放,用于之后在CMakeList.txt 中可以动态链接到这些so库。
根据 jni 文件夹相对 CMakeLists.txt 文件的位置,找到各 so 库,通过外部引入。# 引入 FFmpeg 的 so 库 # 1.编解码器核心库 add_library(avcodec SHARED IMPORTED) set_target_properties(avcodec PROPERTIES IMPORTED_LOCATION${CMAKE_SOURCE_DIR}/../jni/${ANDROID_ABI}/libavcodec.so)
-
添加头文件目录:让自定义 cpp 文件,可以通过头文件链接到其他 so 库
# 添加头文件目录include_directories(${CMAKE_SOURCE_DIR}/include)
-
将自定义 cpp 文件(native-lib-myFFmpeg 库)与FFmpeg 库 链接到一起,使得我的 native 方法可以调用到 FFmpage 的内容。
完整代码:
cmake_minimum_required(VERSION 3.6)
project(ffmpeg_test)# 添加头文件目录
include_directories(${CMAKE_SOURCE_DIR}/include)# 引入 FFmpeg 的 so 库
# 1.编解码器核心库
add_library(avcodec SHARED IMPORTED)
set_target_properties(avcodec PROPERTIES IMPORTED_LOCATION${CMAKE_SOURCE_DIR}/../jni/${ANDROID_ABI}/libavcodec.so)#2.负责封装/解封装(容器格式,如 mp4、mkv)
add_library(avformat SHARED IMPORTED)
set_target_properties(avformat PROPERTIES IMPORTED_LOCATION${CMAKE_SOURCE_DIR}/../jni/${ANDROID_ABI}/libavformat.so)#3.工具库(数学、字节序、日志等)
add_library(avutil SHARED IMPORTED)
set_target_properties(avutil PROPERTIES IMPORTED_LOCATION${CMAKE_SOURCE_DIR}/../jni/${ANDROID_ABI}/libavutil.so)#4.视频像素格式转换(YUV ↔ RGB 等)
add_library(swscale SHARED IMPORTED)
set_target_properties(swscale PROPERTIES IMPORTED_LOCATION${CMAKE_SOURCE_DIR}/../jni/${ANDROID_ABI}/libswscale.so)#5.音频重采样、通道布局转换
add_library(swresample SHARED IMPORTED)
set_target_properties(swresample PROPERTIES IMPORTED_LOCATION${CMAKE_SOURCE_DIR}/../jni/${ANDROID_ABI}/libswresample.so)#6.滤镜处理(裁剪、特效等)
add_library(avfilter SHARED IMPORTED)
set_target_properties(avfilter PROPERTIES IMPORTED_LOCATION${CMAKE_SOURCE_DIR}/../jni/${ANDROID_ABI}/libavfilter.so)# 添加你的本地库
add_library(native-lib-myFFmpeg SHARED native-lib-myFFmpeg.cpp)# 链接 FFmpeg 库
target_link_libraries(native-lib-myFFmpegavcodecavformatavutilswscaleswresampleavfilterandroid${log-lib}
)
5.CMakeLists.txt 环境配置
配置如下三点:
- CMakeLists.txt 路径
- ndk 编译架构
- C++编译语言
android {defaultConfig {🧠 externalNativeBuild{cmake{cppFlags("")}}🧠 ndk{abiFilters += listOf("armeabi-v7a","x86_64","arm64-v8a")}}🧠 externalNativeBuild{cmake {path = file("src/main/cpp/CMakeLists.txt")}}
}
build 项目,生成自定义的 native-lib-myFFmpeg.so 文件,路径:
build/intermediates/cxx/Debug/48u475k3/obj/armeabi-v7a/libnative-lib-myFFmpeg.so
6.Native与Java层调用
TextView tv = findViewById(R.id.video_text);FFmpegLoader fFmpegLoader = new FFmpegLoader();tv.setText(fFmpegLoader.getFFmpegVersion());
显示FFmpeg的版本,完成FFmpeg 接入。
解码器准备
native-lib-myFFmpeg.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_video_FFmpegLoader */#ifndef _Included_com_example_video_FFmpegLoader
#define _Included_com_example_video_FFmpegLoader
#ifdef __cplusplus
extern "C" {
#endif
/** Class: com_example_video_FFmpegLoader* Method: getFFmpegVersion* Signature: ()V*/
JNIEXPORT void JNICALL Java_com_example_video_SimplePlayer_getFFmpegVersion(JNIEnv *, jobject);#ifdef __cplusplus
}
#endif
#endif
native-lib-myFFmpeg.cpp
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_video_FFmpegLoader */
#include <string>
#include "NativeSimplePlayer.h"
#include "JNICallbackHelper.h"extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
}#ifndef _Included_com_example_video_FFmpegLoader
#define _Included_com_example_video_FFmpegLoader
#ifdef __cplusplus
extern "C" {
#endif
/** Class: com_example_video_FFmpegLoader* Method: getFFmpegVersion* Signature: ()V*/
JNIEXPORT jstring JNICALL Java_com_example_video_SimplePlayer_getFFmpegVersion(JNIEnv *env, jobject){const char *version = av_version_info();return env->NewStringUTF(version);
}#ifdef __cplusplus
}
#endif
#endif//虚拟机可以跨线程
JavaVM *vm = 0;
jint JNI_OnLoad(JavaVM *vm,void *args){::vm = vm;return JNI_VERSION_1_6;
}extern "C"
JNIEXPORT void JNICALL
Java_com_example_video_SimplePlayer_prepareNative(JNIEnv *env, jobject job, jstring data_source) {const char* data_source_ = env->GetStringUTFChars(data_source,0);//可能是主线程,也可能是子线程auto *helper = new JNICallbackHelper(vm,env,job);auto *player = new NativeSimplePlayer(data_source_,helper);player->prepare();env->ReleaseStringUTFChars(data_source,data_source_);
}extern "C"
JNIEXPORT void JNICALL
Java_com_example_video_SimplePlayer_startNative(JNIEnv *env, jobject thiz) {// TODO: implement startNative()
}extern "C"
JNIEXPORT void JNICALL
Java_com_example_video_SimplePlayer_stopNative(JNIEnv *env, jobject thiz) {// TODO: implement stopNative()
}extern "C"
JNIEXPORT void JNICALL
Java_com_example_video_SimplePlayer_releaseNative(JNIEnv *env, jobject thiz) {// TODO: implement releaseNative()
}
NativeSimplePlayer.cpp
//
// Created on 2025/7/22.
//#include <string.h>
#include "NativeSimplePlayer.h"NativeSimplePlayer::NativeSimplePlayer(const char *data_source_,JNICallbackHelper *helper) {//如果被释放,会造成悬空指针//this->data_source = data_source_;//深拷贝//C层:demo.mp4\0 C层会自动 + \0 ,strlen 不计算 \0 的长度this->data_source = new char[strlen(data_source_) + 1];strcpy(this->data_source,data_source_);this->helper = helper;
}NativeSimplePlayer::~NativeSimplePlayer() {if( data_source ){delete data_source;}if( helper){delete helper;}
}void* task_prepare(void * args){//读取文件auto *player = static_cast<NativeSimplePlayer *>(args);player->prepare_();return 0;//必须返回,错误很难找
}void NativeSimplePlayer::prepare_() {//为什么FFmpeg源码,大量使用上下文 Context//因为FFmpge源码是纯C,他没有对象,只能上下文贯穿环境,操作成员变量/*** 第一步,打开媒体文件地址* @parm AVFormatContext -> formatContext 上下文* @parm filename -> data_source 路径* @parm AVInputFormat -> *fmt Mac、Windows 摄像头、麦克风* @param AVDictionary -> 设置Http连接超时,打开rtmp超时*/formatContext = avformat_alloc_context();AVDictionary *dictionary = 0;av_dict_set(&dictionary,"timeout","5000000",0);int result = avformat_open_input(&formatContext,data_source,0,&dictionary);//释放字典av_dict_free(&dictionary);if(result){//回调错误信息给Java,通过JNI反射return;}/*** 第二步,查找媒体中的音视频流信息*/result = avformat_find_stream_info(formatContext,0);if( result < 0 ){return;}/*** 第三步,根据流信息,流的个数,用循环来找*/// for(int i = 0;i < formatContext->nb_streams;++i){for(int i = 0;i < 2;++i){/*** 第四步,获取媒体流(视频,音频)*/AVStream *stream = formatContext->streams[i];/*** 第五步,从上面的流中 获取 编码解码的参数* 由于后面的解码器 编码器 都需要参数(记录的宽高)*/AVCodecParameters *parameters = stream->codecpar;/*** 第六步,获取编/解码器,根据上面的参数👆*/const AVCodec *codec = avcodec_find_decoder(parameters->codec_id);/*** 第七步,编解码器 上下文【真正干活】*/AVCodecContext *codecContext = avcodec_alloc_context3(codec);if( !codecContext ){return;}/*** 第八步,他目前是一张白纸* 把 parameter 拷贝给=> codecContext*/result = avcodec_parameters_to_context(codecContext,parameters);if( result < 0){return;}/*** 第九步,打开解码器*/result = avcodec_open2(codecContext,codec,0);if( result ){return;}/*** 第十步,从编解码器从,获取流的类型 codec_type 决定是音频还是视频*/if( parameters->codec_type == AVMediaType::AVMEDIA_TYPE_AUDIO){//分开音频audio_channel = new AudioChannel();} else if(parameters->codec_type == AVMediaType::AVMEDIA_TYPE_VIDEO){//分开视频video_channel = new VideoChannel();}}/*** 第十一步,如果流中没有音频,也没有视频 【健壮性校验】*/if( !audio_channel && !video_channel){return;}/*** 第十二步,恭喜你,准备成功,媒体准备完成,通知上层*/if(helper){helper->onPrepared(THREAD_CHILD);}
}void NativeSimplePlayer::prepare() {//此时为Activity调用到的,所以为主线程//解封装 FFmpeg 来解析,要使用子线程pthread_create(&pid_prepare,0,task_prepare,this);}
NativeSimplePlayer.h
//
// Created on 2025/7/22.
//#ifndef COROUTINE_NATIVESIMPLEPLAYER_H
#define COROUTINE_NATIVESIMPLEPLAYER_H#include <cstring>
#include <pthread.h>
#include "AudioChannel.h"
#include "VideoChannel.h"
#include "JNICallbackHelper.h"
#include "AudioChannel.h"
#include "VideoChannel.h"
#include "util.h"extern "C"{ //ffmpeg 是纯C写的,必须采用C的编译方式,否则崩溃
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"};class NativeSimplePlayer {
private:char *data_source = 0;//指针需要初始值pthread_t pid_prepare;AVFormatContext *formatContext = 0;AudioChannel *audio_channel = 0;VideoChannel *video_channel = 0;JNICallbackHelper *helper =0 ;
public:NativeSimplePlayer(const char *data_source,JNICallbackHelper *helper);~NativeSimplePlayer();void prepare();void prepare_();
};#endif //COROUTINE_NATIVESIMPLEPLAYER_H
JNICallbackHelper.h Native层调用Java层
//
// Created on 2025/7/22.
//#ifndef COROUTINE_JNICALLBACKHELPER_H
#define COROUTINE_JNICALLBACKHELPER_H#include <jni.h>
#include "util.h"class JNICallbackHelper {
private:JavaVM *vm = 0;JNIEnv *env = 0;jobject job;jmethodID jmd_prepared;public:JNICallbackHelper(_JavaVM *vm, _JNIEnv *env, jobject jobject);virtual ~JNICallbackHelper();void onPrepared(int thread_mode);
};#endif //COROUTINE_JNICALLBACKHELPER_H
JNICallbackHelper.cpp
//
// Created on 2025/7/22.
//#include "JNICallbackHelper.h"JNICallbackHelper::JNICallbackHelper(_JavaVM *vm, _JNIEnv *env, jobject jobject) {this->vm = vm;this->env = env;//全局引用this->job = env->NewGlobalRef(jobject);jclass claz = env->GetObjectClass(jobject);//🌟这里没有赋值jmd_prepared = env->GetMethodID(claz,"onPrepared","()V");
}JNICallbackHelper::~JNICallbackHelper() {vm = 0;env->DeleteGlobalRef(job);job = 0;env = 0;
}void JNICallbackHelper::onPrepared(int thread_mode) {if( thread_mode == THREAD_MAIN){env->CallVoidMethod(job,jmd_prepared);} else if (thread_mode == THREAD_CHILD){//子线程不可以跨线程,要用全新的envJNIEnv *env_child;vm->AttachCurrentThread(&env_child,0);env_child->CallVoidMethod(job,jmd_prepared);vm->DetachCurrentThread();}
}
util.h
//
// Created on 2025/7/22.
//#ifndef COROUTINE_UTIL_H
#define COROUTINE_UTIL_H#define THREAD_MAIN 1
#define THREAD_CHILD 2#endif //COROUTINE_UTIL_H
CMakeLists.txt 添加调用的 cpp
# 添加你的本地库
add_library(native-lib-myFFmpegSHAREDnative-lib-myFFmpeg.cppJNICallbackHelper.cppVideoChannel.cppAudioChannel.cppNativeSimplePlayer.cpp)
Jave层调用类
package com.example.video;public class SimplePlayer {public native String getFFmpegVersion();static {System.loadLibrary("native-lib-myFFmpeg");}public SimplePlayer() {}private String dataSource;private OnPreparedListener onPreparedListener;public void setDataSource(String dataSource) {this.dataSource = dataSource;}/*** 播放器的准备工作*/public void prepare(){prepareNative(dataSource);}/*** 开始播放*/public void start(){startNative();}/*** 停止播放*/public void stop(){stopNative();}/*** 释放资源*/public void release(){releaseNative();}public void onPrepared(){if( onPreparedListener != null){onPreparedListener.onPrepared();}}public void setOnPreparedListener(OnPreparedListener listener){onPreparedListener = listener;}public interface OnPreparedListener{void onPrepared();}/*** ======================= native 函数区 ==========================*/private native void prepareNative(String dataSource);private native void startNative();private native void stopNative() ;private native void releaseNative() ;}
Activity 准备视频编码器完成
package com.example.video;import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.widget.TextView;
import android.widget.Toast;import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;import java.io.File;public class VideoActivity extends AppCompatActivity {private SimplePlayer mPlayer;public static final String TAG = VideoActivity.class.getName();@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);EdgeToEdge.enable(this);setContentView(R.layout.activity_video);ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);return insets;});TextView tv = findViewById(R.id.video_text);//tv.setText(mPlayer.getFFmpegVersion());initPlayer();}private void initPlayer(){mPlayer = new SimplePlayer();File videFile = new File(Environment.getExternalStorageDirectory()+ File.separator + "Download/Mcloud.mp4");mPlayer.setDataSource(videFile.getAbsolutePath());//准备成功的回调——C++子线程调用mPlayer.setOnPreparedListener(new SimplePlayer.OnPreparedListener() {@Overridepublic void onPrepared() {Log.d(TAG, "onPrepared: 准备完成");runOnUiThread(new Runnable() {@Overridepublic void run() {Toast.makeText(VideoActivity.this,"准备完成,即将播放",Toast.LENGTH_SHORT).show();}});mPlayer.start();}});}@Overrideprotected void onResume() {super.onResume();//触发mPlayer.prepare();}@Overrideprotected void onStop() {super.onStop();mPlayer.stop();}@Overrideprotected void onDestroy() {super.onDestroy();mPlayer.release();}
}