有趣生活

当前位置:首页>科技>android如何集成ffmpeg一文读懂AndroidFFmpeg

android如何集成ffmpeg一文读懂AndroidFFmpeg

发布时间:2026-07-25阅读(0)

导读概述本文首先以FFmpeg视频解码为主题,主要介绍了ffmpeg进行解码视频时的主要流程、基本原理;其次,文章还讲述了与FFmpeg视频解码有关的简单应用,....概述

本文首先以 FFmpeg 视频解码为主题,主要介绍了 ffmpeg 进行解码视频时的主要流程、基本原理;其次,文章还讲述了与 FFmpeg 视频解码有关的简单应用,包括如何在原有的 FFmpeg 视频解码的基础上按照一定时间轴顺序播放视频、如何在播放视频时加入 seek 的逻辑;除此之外,文章重点介绍了解码视频时可能容易遗漏的细节,最后是简单地阐述了下如何封装一个具有基本的视频解码功能的 VideoDecoder。

前言FFmpeg

FFmpeg 是一套可以用来录制、转换数字音频、视频,并能将其转化为流的开源计算机程序,它可生成用于处理和操作多媒体数据的库,其中包含了先进的音视频解码库 libavcodec 和音视频格式转换库 libavformat。

FFmpeg 六大常用功能模块
  • libavformat:多媒体文件或协议的封装和解封装库,如 mp4、flv 等文件封装格式,rtmp、rtsp 等网络协议封装格式;
  • libavcodec:音视频解码核心库;
  • libavfilter:音视频、字幕滤镜库;
  • libswscale:图像格式转换库;
  • libswresample:音频重采样库;
  • libavutil:工具库
视频解码基础入门
  • 解复用(Demux):解复用也可叫解封装。这里有一个概念叫封装格式,封装格式指的是音视频的组合格式,常见的有 mp4、flv、mkv 等。通俗来讲,封装是将音频流、视频流、字幕流以及其他附件按一定规则组合成一个封装的产物。而解封装起着与封装相反的作用,将一个流媒体文件拆解成音频数据和视频数据等。此时拆分后数据是经过压缩编码的,常见的视频压缩数据格式有 h264。

  • 解码(Decode):简单来说,就是对压缩的编码数据解压成原始的视频像素数据,常用的原始视频像素数据格式有 YUV。

  • 色彩空间转换(Color Space Convert):通常对于图像显示器来说,它是通过 RGB 模型来显示图像的,但在传输图像数据时使用 yuv 模型可以节省带宽。因此在显示图像时就需要将 yuv 像素格式的数据转换成 rgb 的像素格式后再进行渲染。
  • 渲染(Render):将前面已经解码和进行色彩空间转换的每一个视频帧的数据发送给显卡以绘制在屏幕画面上。
一、 引入 FFmpeg 前的准备工作1.1 FFmpeg so 库编译
  • 在 FFmpeg 官网下载源码库并解压;
  • 下载 NDK 库并解压;
  • 配置解压后的 FFmpeg 源码库目录中的 configure,修改高亮部分几个参数为以下的内容,主要目的是生成 Android 可使用的 名称-版本.so 文件的格式;

# ······# build settingsSHFLAGS=-shared -Wl,-soname,$$(@F)LIBPREF="lib"LIBSUF=".a"FULLNAME=$(NAME)$(BUILDSUF)LIBNAME=$(LIBPREF)$(FULLNAME)$(LIBSUF)SLIBPREF="lib"SLIBSUF=".so"SLIBNAME=$(SLIBPREF)$(FULLNAME)$(SLIBSUF)SLIBNAME_WITH_VERSION=$(SLIBNAME).$(LIBVERSION)# 已修改配置SLIBNAME_WITH_MAJOR=$(SLIBNAME)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)LIB_INSTALL_EXTRA_CMD=$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"SLIB_INSTALL_NAME=$(SLIBNAME_WITH_MAJOR)SLIB_INSTALL_LINKS=$(SLIBNAME)# ······

  • 在 FFmpeg 源码库目录下新建脚本文件 build_android_arm_v8a.sh,在文件中配置 NDK 的路径,并输入下面其他的内容;

# 清空上次的编译make clean# 这里先配置你的 NDK 路径export NDK=/Users/bytedance/Library/Android/sdk/ndk/21.4.7075529TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/darwin-x86_64function build_android{./configure \--prefix=$PREFIX \--disable-postproc \--disable-debug \--disable-doc \--enable-FFmpeg \--disable-doc \--disable-symver \--disable-static \--enable-shared \--cross-prefix=$CROSS_PREFIX \--target-os=android \--arch=$ARCH \--cpu=$CPU \--cc=$CC \--cxx=$CXX \--enable-cross-compile \--sysroot=$SYSROOT \--extra-cflags="-Os -fpic $OPTIMIZE_CFLAGS" \--extra-ldflags="$ADDI_LDFLAGS"make cleanmake -j16make installecho "============================ build android arm64-v8a success =========================="}# arm64-v8aARCH=arm64CPU=armv8-aAPI=21CC=$TOOLCHAIN/bin/aarch64-linux-android$API-clangCXX=$TOOLCHAIN/bin/aarch64-linux-android$API-clang SYSROOT=$NDK/toolchains/llvm/prebuilt/darwin-x86_64/sysrootCROSS_PREFIX=$TOOLCHAIN/bin/aarch64-linux-android-PREFIX=$(pwd)/android/$CPUOPTIMIZE_CFLAGS="-march=$CPU"echo $CCbuild_android

  • 设置 NDK 文件夹中所有文件的权限 chmod 777 -R NDK;
  • 终端执行脚本 ./build_android_arm_v8a.sh,开始编译 FFmpeg。编译成功后的文件会在 FFmpeg 下的 android 目录中,会出现多个 .so 文件;

  • 若要编译 arm-v7a,只需要拷贝修改以上的脚本为以下 build_android_arm_v7a.sh 的内容。

#armv7-aARCH=armCPU=armv7-aAPI=21CC=$TOOLCHAIN/bin/armv7a-linux-androideabi$API-clangCXX=$TOOLCHAIN/bin/armv7a-linux-androideabi$API-clang SYSROOT=$NDK/toolchains/llvm/prebuilt/darwin-x86_64/sysrootCROSS_PREFIX=$TOOLCHAIN/bin/arm-linux-androideabi-PREFIX=$(pwd)/android/$CPUOPTIMIZE_CFLAGS="-mfloat-abi=softfp -mfpu=vfp -marm -march=$CPU "

1.2 在 Android 中引入 FFmpeg 的 so 库
  • NDK 环境、cmake 构建工具、LLDB(C/C 代码调试工具);
  • 新建 C module,一般会生成以下几个重要的文件:CMakeLists.txt、native-lib.cpp、MainActivity;
  • 在 app/src/main/ 目录下,新建目录,并命名 JNILibs,这是 Android Studio 默认放置 so 动态库的目录;接着在 jniLibs 目录下,新建 arm64-v8a 目录,然后将编译好的 .so 文件粘贴至此目录下;然后再将编译时生成的 .h 头文件(FFmpeg 对外暴露的接口)粘贴至 cpp 目录下的 include 中。以上的 .so 动态库目录和 .h 头文件目录都会在 CMakeLists.txt 中显式声明和链接进来;
  • 最上层的 MainActivity,在这里面加载 C/C 代码编译的库:native-lib。native-lib 在 CMakeLists.txt 中被添加到名为 "ffmpeg" 的 library 中,所以在 System.loadLibrary()中输入的是 "ffmpeg";

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() } // 声明一个外部引用的方法,此方法和 C/C 层的代码是对应的。 external fun stringFromJNI(): String companion object { // 在 init{} 中加载 C/C 编译成的 library:ffmpeg // library 名称的定义和添加在 CMakeLists.txt 中完成 init { System.loadLibrary("ffmpeg") } }}

  • native-lib.cpp 是一个 C 接口文件,Java 层中声明的 external 方法在这里得到实现;

#include <jni.h>#include <string>extern "C" JNIEXPORT jstring JNICALLJava_com_bytedance_example_MainActivity_stringFromJNI( JNIEnv *env, jobject /* this */) { std::string hello = "Hello from C "; return env->NewStringUTF(hello.c_str());}

  • CMakeLists.txt 是一个构建脚本,目的是配置可以编译出 native-lib 此 so 库的构建信息;

# For more information about using CMake with Android Studio, read the# documentation: https://d.android.com/studio/projects/add-native-code.html# Sets the minimum version of CMake required to build the native library.cmake_minimum_required(VERSION 3.10.2)# Declares and names the project.project("ffmpeg")# Creates and names a library, sets it as either STATIC# or SHARED, and provides the relative paths to its source code.# You can define multiple libraries, and CMake builds them for you.# Gradle automatically packages shared libraries with your APK.# 定义 so 库和头文件所在目录,方便后面使用set(FFmpeg_lib_dir ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})set(FFmpeg_head_dir ${CMAKE_SOURCE_DIR}/FFmpeg)# 添加头文件目录include_directories( FFmpeg/include)add_library( # Sets the name of the library. ffmmpeg # Sets the library as a shared library. SHARED # Provides a relative path to your source file(s). native-lib.cpp )# Searches for a specified prebuilt library and stores the path as a# variable. Because CMake includes system libraries in the search path by# default, you only need to specify the name of the public NDK library# you want to add. CMake verifies that the library exists before# completing its build.# 添加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 )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)# Specifies libraries CMake should link to your target library. You# can link multiple libraries, such as libraries you define in this# build script, prebuilt third-party libraries, or system libraries.target_link_libraries( # Specifies the target library. audioffmmpeg # 把前面添加进来的 FFmpeg.so 库都链接到目标库 native-lib 上 avutil swresample avcodec -landroid # Links the target library to the log library # included in the NDK. ${log-lib})

  • 以上的操作就将 FFmpeg 引入 Android 项目。
二、FFmpeg 解码视频的原理和细节2.1 主要流程

2.2 基本原理2.2.1 常用的 ffmpeg 接口

// 1 分配 AVFormatContextavformat_alloc_context();// 2 打开文件输入流avformat_open_input(AVFormatContext **ps, const char *url, const AVInputFormat *fmt, AVDictionary **options);// 3 提取输入文件中的数据流信息avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);// 4 分配编解码上下文avcodec_alloc_context3(const AVCodec *codec);// 5 基于与数据流相关的编解码参数来填充编解码器上下文avcodec_parameters_to_context(AVCodecContext *codec, const AVCodecParameters *par);// 6 查找对应已注册的编解码器avcodec_find_decoder(enum AVCodecID id);// 7 打开编解码器avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);// 8 不停地从码流中提取压缩帧数据,获取的是一帧视频的压缩数据av_read_frame(AVFormatContext *s, AVPacket *pkt);// 9 发送原生的压缩数据输入到解码器(compressed data)avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);// 10 接收解码器输出的解码数据avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);

2.2.2 视频解码的整体思路
  • 首先要注册 libavformat 并且注册所有的编解码器、复用/解复用组、协议等。它是所有基于 FFmpeg 的应用程序中第一个被调用的函数, 只有调用了该函数,才能正常使用 FFmpeg 的各项功能。另外,在最新版本的 FFmpeg 中目前已经可以不用加入这行代码;

av_register_all();

  • 打开视频文件,提取文件中的数据流信息;

auto av_format_context = avformat_alloc_context();avformat_open_input(&av_format_context, path_.c_str(), nullptr, nullptr);avformat_find_stream_info(av_format_context, nullptr);

  • 然后获取视频媒体流的下标,才能找到文件中的视频媒体流;

int video_stream_index = -1;for (int i = 0; i < av_format_context->nb_streams; i ) { // 匹配找到视频媒体流的下标, if (av_format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { video_stream_index = i; LOGD(TAG, "find video stream index = %d", video_stream_index); break; }}

  • 获取视频媒体流、获取解码器上下文、获取解码器上下文、配置解码器上下文的参数值、打开解码器;

// 获取视频媒体流auto stream = av_format_context->streams[video_stream_index];// 找到已注册的解码器auto codec = avcodec_find_decoder(stream->codecpar->codec_id);// 获取解码器上下文AVCodecContext* codec_ctx = avcodec_alloc_context3(codec);// 将视频媒体流的参数配置到解码器上下文auto ret = avcodec_parameters_to_context(codec_ctx, stream->codecpar);if (ret >= 0) { // 打开解码器 avcodec_open2(codec_ctx, codec, nullptr); // ······}

  • 通过指定像素格式、图像宽、图像高来计算所需缓冲区需要的内存大小,分配设置缓冲区;并且由于是上屏绘制,因此我们需要用到 ANativeWindow,使用 ANativeWindow_setBuffersGeometry 设置此绘制窗口的属性;

video_width_ = codec_ctx->width;video_height_ = codec_ctx->height;int buffer_size = av_image_get_buffer_size(AV_PIX_FMT_RGBA, video_width_, video_height_, 1);// 输出 bufferout_buffer_ = (uint8_t*) av_malloc(buffer_size * sizeof(uint8_t));// 通过设置宽高来限制缓冲区中的像素数量,而非显示屏幕的尺寸。// 如果缓冲区与显示的屏幕尺寸不相符,则实际显示的可能会是拉伸,或者被压缩的图像int result = ANativeWindow_setBuffersGeometry(native_window_, video_width_, video_height_, WINDOW_FORMAT_RGBA_8888);

  • 分配内存空间给像素格式为 RGBA 的 AVFrame,用于存放转换成 RGBA 后的帧数据;设置 rgba_frame 缓冲区,使其与 out_buffer_ 相关联;

auto rgba_frame = av_frame_alloc();av_image_fill_arrays(rgba_frame->data, rgba_frame->linesize, out_buffer_, AV_PIX_FMT_RGBA, video_width_, video_height_, 1);

  • 获取 SwsContext,它在调用 sws_scale() 进行图像格式转换和图像缩放时会使用到。YUV420P 转换为 RGBA 时可能会在调用 sws_scale 时格式转换失败而无法返回正确的高度值,原因跟调用 sws_getContext 时 flags 有关,需要将 SWS_BICUBIC 换成 SWS_FULL_CHR_H_INT | SWS_ACCURATE_RND;

struct SwsContext* data_convert_context = sws_getContext( video_width_, video_height_, codec_ctx->pix_fmt, video_width_, video_height_, AV_PIX_FMT_RGBA, SWS_BICUBIC, nullptr, nullptr, nullptr);

  • 分配内存空间给用于存储原始数据的 AVFrame,指向原始帧数据;并且分配内存空间给用于存放视频解码前数据的 AVPacket;

auto frame = av_frame_alloc();auto packet = av_packet_alloc();

  • 从视频码流中循环读取压缩帧数据,然后开始解码;

ret = av_read_frame(av_format_context, packet);if (packet->size) { Decode(codec_ctx, packet, frame, stream, lock, data_convert_context, rgba_frame);}

  • 在 Decode() 函数中将装有原生压缩数据的 packet 作为输入发送给解码器;

/* send the packet with the compressed data to the decoder */ret = avcodec_send_packet(codec_ctx, pkt);

  • 解码器返回解码后的帧数据到指定的 frame 上,后续可对已解码 frame 的 pts 换算为时间戳,按时间轴的显示顺序逐帧绘制到播放的画面上;

while (ret >= 0 && !is_stop_) { // 返回解码后的数据到 frame ret = avcodec_receive_frame(codec_ctx, frame); if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { return; } else if (ret < 0) { return; } // 拿到当前解码后的 frame,对其 pts 换算成时间戳,以便于跟传入的指定时间戳进行比 auto decode_time_ms = frame->pts * 1000 / stream->time_base.den; if (decode_time_ms >= time_ms_) { last_decode_time_ms_ = decode_time_ms; is_seeking_ = false; // ······ // 图片数据格式转换 // ······ // 把转换后的数据绘制到屏幕上 } av_packet_unref(pkt);}

  • 绘制画面之前,要进行图片数据格式的转换,这里就要用到前面获取到的 SwsContext;

// 图片数据格式转换int result = sws_scale( sws_context, (const uint8_t* const*) frame->data, frame->linesize, 0, video_height_, rgba_frame->data, rgba_frame->linesize);if (result <= 0) { LOGE(TAG, "Player Error : data convert fail"); return;}

  • 因为是上屏绘制,所以用到了 ANativeWindow 和 ANativeWindow_Buffer。在绘制画面之前,需要使用锁定窗口的下一个绘图 surface 以进行绘制,然后将要显示的帧数据写入到缓冲区中,最后解锁窗口的绘图 surface,将缓冲区的数据发布到屏幕显示上;

// 播放result = ANativeWindow_lock(native_window_, &window_buffer_, nullptr);if (result < 0) { LOGE(TAG, "Player Error : Can not lock native window");} else { // 将图像绘制到界面上 // 注意 : 这里 rgba_frame 一行的像素和 window_buffer 一行的像素长度可能不一致 // 需要转换好 否则可能花屏 auto bits = (uint8_t*) window_buffer_.bits; for (int h = 0; h < video_height_; h ) { memcpy(bits h * window_buffer_.stride * 4, out_buffer_ h * rgba_frame->linesize[0], rgba_frame->linesize[0]); } ANativeWindow_unlockAndPost(native_window_);}

  • 以上就是主要的解码过程。除此之外,因为 C 使用资源和内存空间时需要自行释放,所以解码结束后还需要调用释放的接口释放资源,以免造成内存泄漏。

sws_freeContext(data_convert_context);av_free(out_buffer_);av_frame_free(&rgba_frame);av_frame_free(&frame);av_packet_free(&packet);avcodec_close(codec_ctx);avcodec_free_context(&codec_ctx);avformat_close_input(&av_format_context);avformat_free_context(av_format_context);ANativeWindow_release(native_window_);

2.3 简单应用

为了更好地理解视频解码的过程,这里封装一个视频解码器 VideoDecoder ,解码器初步会有以下几个函数:

VideoDecoder(const char* path, std::function<void(long timestamp)> on_decode_frame);void Prepare(ANativeWindow* window);bool DecodeFrame(long time_ms);void Release();

在这个视频解码器中,输入指定时间戳后会返回解码的这一帧数据。其中较为重要的是 DecodeFrame(long time_ms) 函数,它可以由使用者自行调用,传入指定帧的时间戳,进而解码对应的帧数据。此外,可以增加同步锁以实现解码线程和使用线程分离。

2.3.1 加入同步锁实现视频播放

若只要对视频进行解码,是不需要使用同步等待的;

但若是要实现视频的播放,那么每解码绘制完一帧就需使用锁进行同步等待,这是因为播放视频时需要让解码和绘制分离、且按照一定的时间轴顺序和速度进行解码和绘制。

condition_.wait(lock);

在上层调用 DecodeFrame 函数传入解码的时间戳时唤醒同步锁,让解码绘制的循环继续执行。

bool VideoDecoder::DecodeFrame(long time_ms) { // ······ time_ms_ = time_ms; condition_.notify_all(); return true;}

2.3.2 播放时加入 seek_frame

在正常播放情况下,视频是一帧一帧逐帧解码播放;但在拖动进度条到达指定的 seek 点的情况下,如果还是从头到尾逐帧解码到 seek 点的话,效率可能不太高。这时候就需要在一定规则内对 seek 点的时间戳做检查,符合条件的直接 seek 到指定的时间戳。

FFmpeg 中的 av_seek_frame
  • av_seek_frame 可以定位到关键帧和非关键帧,这取决于选择的 flag 值。因为视频的解码需要依赖关键帧,所以一般我们需要定位到关键帧;

int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp, int flags);

  • av_seek_frame 中的 flag 是用来指定寻找的 I 帧和传入的时间戳之间的位置关系。当要 seek 已过去的时间戳时,时间戳不一定会刚好处在 I 帧的位置,但因为解码需要依赖 I 帧,所以需要先找到此时间戳附近一个的 I 帧,此时 flag 就表明要 seek 到当前时间戳的前一个 I 帧还是后一个 I 帧;
  • flag 有四个选项:
TAGS标签:  android  如何  集成  FFmpeg  一文  android如何集

Copyright © 2024 有趣生活 All Rights Reserve吉ICP备19000289号-5 TXT地图HTML地图XML地图