目录
  1. 1. 一、FFmpeg 架构总览
  2. 2. 二、Android NDK 交叉编译 FFmpeg
    1. 2.1. 2.1 编译环境准备
    2. 2.2. 2.2 configure 脚本的关键选项
    3. 2.3. 2.3 产物组织与 CMake 集成
  3. 3. 三、核心 API 管线:Demux → Decode → Filter → Encode → Mux
    1. 3.1. 3.1 注册与初始化
    2. 3.2. 3.2 解封装(Demux)—— avformat_open_input / av_read_frame
    3. 3.3. 3.3 解码(Decode)—— avcodec_send_packet / avcodec_receive_frame
    4. 3.4. 3.4 视频帧转换 —— sws_scale
    5. 3.5. 3.5 音频重采样 —— swr_convert
    6. 3.6. 3.6 编码(Encode)—— 反向 send/receive
    7. 3.7. 3.7 封装(Mux)—— 初始化输出
  4. 4. 四、滤镜图(Filter Graph)
    1. 4.1. 4.1 构建滤镜图的基本 API
    2. 4.2. 4.2 常用滤镜描述字符串示例
    3. 4.3. 4.3 在解码循环中使用滤镜图
  5. 5. 五、Seek 操作
    1. 5.1. 5.1 按时间戳跳转
    2. 5.2. 5.2 seek 的几种模式
  6. 6. 六、硬件加速:MediaCodec 集成
    1. 6.1. 6.1 FFmpeg 的 MediaCodec 支持
    2. 6.2. 6.2 使用 MediaCodec 硬件解码
    3. 6.3. 6.3 JNI 层包装
  7. 7. 七、完整转码示例
  8. 8. 八、性能优化要点
    1. 8.1. 8.1 多线程解码
    2. 8.2. 8.2 零拷贝策略
    3. 8.3. 8.3 解码线程与渲染线程分离
  9. 9. 九、常见问题与解决方案
    1. 9.1. 9.1 时间戳不连续
    2. 9.2. 9.2 H.264 annex B 与 avcC
    3. 9.3. 9.3 内存管理陷阱
  10. 10. 十、总结
【音视频、图像处理技术】开源库FFMPEG学习

FFmpeg 是音视频处理领域的事实标准开源库。它几乎支持所有已知的音视频封装格式、编码格式、流协议和滤镜。在 Android 平台上,FFmpeg 通常通过 NDK 交叉编译为动态库(libavcodec、libavformat 等),再由 JNI 桥接到 Java/Kotlin 层调用。本文从 FFmpeg 架构概览、Android NDK 交叉编译、核心 API 管线(解封装-解码-编码-封装)、滤镜图、Seek 操作、MediaCodec 硬件加速等角度,系统讲解 FFmpeg 在 Android 上的工程实践。

一、FFmpeg 架构总览

FFmpeg 是一个模块化的库集合,共包含 8 个主要库:

库名 职责
libavutil 数学工具、字符串处理、日志、内存管理、字典、可选参数解析等基础组件
libavcodec 编解码器框架,包含所有内置的编码器和解码器(H.264、H.265、AAC、MP3、VP8/VP9 等)
libavformat 封装/解封装(demuxer / muxer),支持 mp4、mkv、flv、ts、hls、rtmp 等容器格式和流协议
libavfilter 音视频滤镜框架,包括缩放、裁剪、叠加、去噪、水印、转场等
libswscale 图像缩放、像素格式转换(如 yuv420p → rgb24)
libswresample 音频重采样、声道布局转换、采样格式转换
libavdevice 采集设备(摄像头、录屏、声卡)的输入输出
libpostproc 后处理(从 FFmpeg 派生,现已基本被 libavfilter 替代)

这些库之间的关系和数据流向:

[输入文件/流]


┌─────────────┐
│ libavformat │ ← 解封装(demuxer):读取容器,提取编码数据包(AVPacket)
└──────┬──────┘


┌─────────────┐
│ libavcodec │ ← 解码(decoder):AVPacket → AVFrame(原始 PCM/像素数据)
└──────┬──────┘


┌─────────────┐
│ libavfilter │ ← 滤镜处理:缩放、裁剪、去噪、加水印等
│ libswscale │
│libswresample│
└──────┬──────┘


┌─────────────┐
│ libavcodec │ ← 编码(encoder):AVFrame → AVPacket
└──────┬──────┘


┌─────────────┐
│ libavformat │ ← 封装(muxer):AVPacket → 输出文件/流
└─────────────┘


[输出文件/流]

二、Android NDK 交叉编译 FFmpeg

2.1 编译环境准备

在 Linux / macOS 上交叉编译 FFmpeg for Android 需要以下工具:

  • Android NDK(推荐 r25+):提供交叉编译工具链
  • FFmpeg 源码:从 https://ffmpeg.org/download.html 获取(建议下载 release 版本)
  • 可选依赖:x264 / x265(H.264/H.265 软件编码器)、fdk-aac(AAC 编码器)、openssl(加密协议)

设置环境变量(以 NDK r25 为例):

# 设置 NDK 路径
export ANDROID_NDK_HOME=/home/user/android-ndk-r25c

# 设置目标 API 级别
export API=24 # Android 7.0

# 设置主机平台
export HOST_TAG=linux-x86_64

2.2 configure 脚本的关键选项

FFmpeg 使用 configure 脚本配置编译选项。以下是一个面向 Android 的典型配置:

#!/bin/bash
# build_ffmpeg_android.sh

NDK=$ANDROID_NDK_HOME
TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/$HOST_TAG
SYSROOT=$TOOLCHAIN/sysroot

function build_one {
./configure \
--prefix=$PREFIX \
--enable-cross-compile \
--target-os=android \
--cross-prefix=$CROSS_PREFIX \
--cc=$CC \
--cxx=$CXX \
--arch=$ARCH \
--cpu=$CPU \
--sysroot=$SYSROOT \
--extra-cflags="$EXTRA_CFLAGS" \
--extra-ldflags="$EXTRA_LDFLAGS" \
\
--disable-doc \
--disable-ffmpeg \
--disable-ffplay \
--disable-ffprobe \
--disable-avdevice \
--disable-postproc \
--disable-symver \
\
--enable-shared \
--disable-static \
--enable-small \
--enable-gpl \
--enable-nonfree \
\
--enable-jni \
--enable-mediacodec \
--enable-hwaccel=h264_mediacodec \
--enable-hwaccel=hevc_mediacodec \
--enable-hwaccel=mpeg4_mediacodec \
--enable-decoder=h264_mediacodec \
--enable-decoder=hevc_mediacodec \
--enable-decoder=mpeg4_mediacodec
}

# 编译 arm64-v8a
ARCH=arm64
CPU=armv8-a
CROSS_PREFIX=$TOOLCHAIN/bin/llvm-
CC=$TOOLCHAIN/bin/aarch64-linux-android$API-clang
CXX=$TOOLCHAIN/bin/aarch64-linux-android$API-clang++
PREFIX=$(pwd)/android/$ARCH
EXTRA_CFLAGS="-march=$CPU -O3 -fPIC"
EXTRA_LDFLAGS=""
build_one
make -j$(nproc)
make install

# 编译 armeabi-v7a(带 NEON)
ARCH=arm
CPU=armv7-a
CROSS_PREFIX=$TOOLCHAIN/bin/arm-linux-androideabi-
CC=$TOOLCHAIN/bin/armv7a-linux-androideabi$API-clang
CXX=$TOOLCHAIN/bin/armv7a-linux-androideabi$API-clang++
PREFIX=$(pwd)/android/$ARCH
EXTRA_CFLAGS="-march=$CPU -mfloat-abi=softfp -mfpu=neon -O3 -fPIC"
EXTRA_LDFLAGS=""
build_one
make -j$(nproc)
make install

# 编译 x86_64(用于模拟器)
ARCH=x86_64
CPU=x86-64
CROSS_PREFIX=$TOOLCHAIN/bin/llvm-
CC=$TOOLCHAIN/bin/x86_64-linux-android$API-clang
CXX=$TOOLCHAIN/bin/x86_64-linux-android$API-clang++
PREFIX=$(pwd)/android/$ARCH
EXTRA_CFLAGS="-O3 -fPIC"
EXTRA_LDFLAGS=""
build_one
make -j$(nproc)
make install

关键配置选项解释:

  • --enable-mediacodec:启用 Android MediaCodec 硬件加速
  • --enable-jni:启用 JNI 支持(Android 特有)
  • --enable-shared:生成 .so 文件(动态库),减小 APK 体积
  • --disable-static:不生成 .a 文件
  • --enable-small:优化体积而非速度(移动端优先缩小尺寸)
  • --enable-gpl --enable-nonfree:启用 GPL 和非自由许可的编解码器(注意许可证合规)

2.3 产物组织与 CMake 集成

编译完成后,产物目录结构:

android/
├── arm64-v8a/
│ ├── include/ # 头文件
│ │ ├── libavcodec/
│ │ ├── libavformat/
│ │ ├── libavutil/
│ │ ├── libavfilter/
│ │ ├── libswscale/
│ │ └── libswresample/
│ └── lib/ # .so 文件
│ ├── libavcodec.so
│ ├── libavformat.so
│ ├── libavutil.so
│ ├── libavfilter.so
│ ├── libswscale.so
│ └── libswresample.so
├── armeabi-v7a/
│ └── ...
└── x86_64/
└── ...

在 Android 项目中的 CMakeLists.txt

cmake_minimum_required(VERSION 3.18)
project("ffmpeg_decoder")

# 设置 FFmpeg 路径
set(FFMPEG_DIR ${CMAKE_SOURCE_DIR}/ffmpeg/${ANDROID_ABI})

# 添加头文件
include_directories(${FFMPEG_DIR}/include)

# 添加动态库
add_library(avcodec SHARED IMPORTED)
set_target_properties(avcodec PROPERTIES
IMPORTED_LOCATION ${FFMPEG_DIR}/lib/libavcodec.so)

add_library(avformat SHARED IMPORTED)
set_target_properties(avformat PROPERTIES
IMPORTED_LOCATION ${FFMPEG_DIR}/lib/libavformat.so)

add_library(avutil SHARED IMPORTED)
set_target_properties(avutil PROPERTIES
IMPORTED_LOCATION ${FFMPEG_DIR}/lib/libavutil.so)

add_library(swscale SHARED IMPORTED)
set_target_properties(swscale PROPERTIES
IMPORTED_LOCATION ${FFMPEG_DIR}/lib/libswscale.so)

add_library(swresample SHARED IMPORTED)
set_target_properties(swresample PROPERTIES
IMPORTED_LOCATION ${FFMPEG_DIR}/lib/libswresample.so)

# 本地库
add_library(native-decoder SHARED
native_decoder.cpp
decoder_core.cpp
)

# 链接
target_link_libraries(native-decoder
avformat
avcodec
avutil
swscale
swresample
log
z
)

三、核心 API 管线:Demux → Decode → Filter → Encode → Mux

FFmpeg 处理媒体文件的完整流程可分为五个阶段。以下用 C 代码(通过 JNI 调用)逐步讲解。

3.1 注册与初始化

FFmpeg 4.0 之后,注册不再是必须的,但出于兼容性考虑,在旧代码中常可见:

#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/log.h>

// FFmpeg 4.0+ 不需要主动注册,各组件在首次使用时自动注册
// 但建议在程序入口设置日志级别
void init_ffmpeg() {
av_log_set_level(AV_LOG_INFO);
// 旧版本:av_register_all(); // FFmpeg < 4.0
}

3.2 解封装(Demux)—— avformat_open_input / av_read_frame

解封装将容器的字节流拆解为独立的 AVPacket(压缩数据包)。核心流程:

int demux(const char* input_path) {
AVFormatContext* fmt_ctx = NULL;
int ret;
int video_stream_index = -1;
int audio_stream_index = -1;

// 1. 打开输入文件/流
ret = avformat_open_input(&fmt_ctx, input_path, NULL, NULL);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "无法打开输入: %s\n", input_path);
return ret;
}

// 2. 读取流信息(探测编码参数)
ret = avformat_find_stream_info(fmt_ctx, NULL);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "无法获取流信息\n");
avformat_close_input(&fmt_ctx);
return ret;
}

// 3. 打印媒体信息
av_dump_format(fmt_ctx, 0, input_path, 0);

// 4. 找到视频流和音频流的索引
for (int i = 0; i < fmt_ctx->nb_streams; i++) {
AVCodecParameters *codecpar = fmt_ctx->streams[i]->codecpar;
if (codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream_index = i;
av_log(NULL, AV_LOG_INFO, "视频流 #%d: %s, %dx%d\n",
i, avcodec_get_name(codecpar->codec_id),
codecpar->width, codecpar->height);
} else if (codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
audio_stream_index = i;
av_log(NULL, AV_LOG_INFO, "音频流 #%d: %s, %d Hz, %d channels\n",
i, avcodec_get_name(codecpar->codec_id),
codecpar->sample_rate, codecpar->ch_layout.nb_channels);
}
}

// 5. 循环读取数据包
AVPacket* pkt = av_packet_alloc();
while (av_read_frame(fmt_ctx, pkt) >= 0) {
if (pkt->stream_index == video_stream_index) {
// 处理视频包
decode_video_packet(pkt, fmt_ctx->streams[video_stream_index]);
} else if (pkt->stream_index == audio_stream_index) {
// 处理音频包
decode_audio_packet(pkt, fmt_ctx->streams[audio_stream_index]);
}
av_packet_unref(pkt);
}

// 6. 清理
av_packet_free(&pkt);
avformat_close_input(&fmt_ctx);
return 0;
}

关键点

  • av_read_frame 返回的 AVPacket 可能包含完整帧或不完整帧,由解码器处理。
  • av_packet_unref 释放 AVPacket 内部引用的数据缓冲区,必须在每次迭代后调用。
  • avformat_find_stream_info 会读取一小部分数据来探测编码参数(可能耗时,可以设置 probesizemax_analyze_duration 来限制)。

3.3 解码(Decode)—— avcodec_send_packet / avcodec_receive_frame

从 FFmpeg 3.1 起,解码 API 改为「send/receive」模式:

// 初始化解码器
AVCodecContext* init_decoder(AVStream* stream) {
AVCodecParameters* codecpar = stream->codecpar;

// 1. 查找解码器
const AVCodec* codec = avcodec_find_decoder(codecpar->codec_id);
if (!codec) {
av_log(NULL, AV_LOG_ERROR, "找不到解码器: %d\n", codecpar->codec_id);
return NULL;
}

// 2. 分配解码器上下文
AVCodecContext* codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) return NULL;

// 3. 将流参数复制到解码器上下文
if (avcodec_parameters_to_context(codec_ctx, codecpar) < 0) {
avcodec_free_context(&codec_ctx);
return NULL;
}

// 4. 打开解码器
if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
av_log(NULL, AV_LOG_ERROR, "无法打开解码器\n");
avcodec_free_context(&codec_ctx);
return NULL;
}

return codec_ctx;
}

// 解码循环(在 av_read_frame 循环中调用)
int decode_packet_to_frames(AVCodecContext* codec_ctx,
AVPacket* pkt, AVFrame* frame,
void (*on_frame)(AVFrame*)) {
// 1. 发送压缩数据包到解码器
int ret = avcodec_send_packet(codec_ctx, pkt);
if (ret < 0) {
// AVERROR(EAGAIN):解码器内部缓冲区满,需要先 receive frame
// AVERROR_EOF:已发送 flush packet,解码器结束
return ret;
}

// 2. 循环接收解码后的帧
while (ret >= 0) {
ret = avcodec_receive_frame(codec_ctx, frame);
if (ret == AVERROR(EAGAIN)) {
// 还需要更多输入数据,继续 send_packet
break;
} else if (ret == AVERROR_EOF) {
// 解码器已耗尽
break;
} else if (ret < 0) {
return ret; // 真正的错误
}

// 处理解码后的帧
on_frame(frame);

// 释放 frame 内部引用,以便下一次接收
av_frame_unref(frame);
}

return 0;
}

执行模型说明

  • Send 一个 packet 不一定 Receive 一个 frame。对于 B 帧编码的流,可能需要 send 多个 packet 才能 receive 到解码帧(解码顺序与显示顺序不同)。
  • Flush 解码器:在 av_read_frame 循环结束后,需要 send 一个 NULL packet 来冲刷解码器内部的缓冲帧:
    avcodec_send_packet(codec_ctx, NULL);  // flush
    while (avcodec_receive_frame(codec_ctx, frame) >= 0) {
    on_frame(frame);
    av_frame_unref(frame);
    }

3.4 视频帧转换 —— sws_scale

解码后的 AVFrame 通常是 YUV 像素格式(如 yuv420p),而 Android 上显示一般需要 RGB。使用 libswscale 进行转换:

struct SwsContext* init_sws(int src_width, int src_height, 
enum AVPixelFormat src_format,
int dst_width, int dst_height,
enum AVPixelFormat dst_format) {
struct SwsContext* sws_ctx = sws_getContext(
src_width, src_height, src_format,
dst_width, dst_height, dst_format,
SWS_BILINEAR, // 缩放算法:SWS_BILINEAR, SWS_BICUBIC, SWS_LANCZOS
NULL, NULL, NULL
);
return sws_ctx;
}

// 在 decode 的回调中转换
void convert_and_display(AVFrame* src_frame, struct SwsContext* sws_ctx,
uint8_t* rgb_buffer, int rgb_stride) {
// 准备目标数据指针数组
uint8_t* dst_data[4] = { rgb_buffer, NULL, NULL, NULL };
int dst_linesize[4] = { rgb_stride, 0, 0, 0 };

sws_scale(sws_ctx,
(const uint8_t* const*)src_frame->data,
src_frame->linesize,
0, // 起始行
src_frame->height, // 行数
dst_data,
dst_linesize);

// rgb_buffer 现在包含 RGB 数据,可以渲染到 Android Surface / Bitmap
}

3.5 音频重采样 —— swr_convert

音频解码后的格式(采样率、声道布局、采样格式)可能与播放设备输出不匹配。使用 libswresample

struct SwrContext* init_swr(int64_t in_ch_layout, enum AVSampleFormat in_fmt, 
int in_rate,
int64_t out_ch_layout, enum AVSampleFormat out_fmt,
int out_rate) {
SwrContext* swr_ctx = swr_alloc_set_opts(NULL,
out_ch_layout, out_fmt, out_rate,
in_ch_layout, in_fmt, in_rate,
0, NULL);

swr_init(swr_ctx);
return swr_ctx;
}

int resample_audio_frame(SwrContext* swr_ctx, AVFrame* in_frame,
uint8_t** out_buffer, int out_samples) {
int ret = swr_convert(swr_ctx,
out_buffer, out_samples,
(const uint8_t**)in_frame->data,
in_frame->nb_samples);
return ret; // 返回实际输出的采样数
}

3.6 编码(Encode)—— 反向 send/receive

编码与解码对称,但方向相反 — send frame, receive packet:

// 编码一帧
int encode_frame(AVCodecContext* enc_ctx, AVFrame* frame, AVPacket* pkt) {
// 1. 发送原始帧
int ret = avcodec_send_frame(enc_ctx, frame);
if (ret < 0) return ret;

// 2. 接收编码后的数据包
while (ret >= 0) {
ret = avcodec_receive_packet(enc_ctx, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
}
// 3. 重要:纠正时间基
pkt->stream_index = video_stream_index;
av_packet_rescale_ts(pkt, enc_ctx->time_base,
out_fmt_ctx->streams[video_stream_index]->time_base);
pkt->pts = av_rescale_q(pkt->pts, enc_ctx->time_base,
out_fmt_ctx->streams[video_stream_index]->time_base);
pkt->dts = av_rescale_q(pkt->dts, enc_ctx->time_base,
out_fmt_ctx->streams[video_stream_index]->time_base);

// 4. 写入输出容器
av_interleaved_write_frame(out_fmt_ctx, pkt);
av_packet_unref(pkt);
}
return 0;
}

3.7 封装(Mux)—— 初始化输出

int init_muxer(const char* output_path, AVFormatContext** out_ctx,
AVCodecContext* video_enc_ctx, AVCodecContext* audio_enc_ctx) {
// 1. 分配输出上下文
avformat_alloc_output_context2(out_ctx, NULL, NULL, output_path);
if (!out_ctx) return -1;

AVFormatContext* oc = *out_ctx;

// 2. 为视频创建新流
AVStream* video_stream = avformat_new_stream(oc, NULL);
avcodec_parameters_from_context(video_stream->codecpar, video_enc_ctx);
video_stream->time_base = video_enc_ctx->time_base;

// 3. 为音频创建新流
AVStream* audio_stream = avformat_new_stream(oc, NULL);
avcodec_parameters_from_context(audio_stream->codecpar, audio_enc_ctx);
audio_stream->time_base = audio_enc_ctx->time_base;

// 4. 打开输出文件
if (!(oc->oformat->flags & AVFMT_NOFILE)) {
avio_open(&oc->pb, output_path, AVIO_FLAG_WRITE);
}

// 5. 写文件头(包含全局元数据和编码器初始化信息)
avformat_write_header(oc, NULL);

return 0;
}

四、滤镜图(Filter Graph)

FFmpeg 的滤镜框架支持可视化编程式的音视频处理链。滤镜图由一个个滤镜节点和它们之间的连接(link)构成。

4.1 构建滤镜图的基本 API

int build_video_filter_graph(AVFilterGraph** filter_graph,
AVFilterContext** buffersrc_ctx,
AVFilterContext** buffersink_ctx,
int width, int height, enum AVPixelFormat pix_fmt,
AVRational time_base,
const char* filter_desc) {
AVFilterGraph* graph = avfilter_graph_alloc();

const AVFilter* buffersrc = avfilter_get_by_name("buffer");
const AVFilter* buffersink = avfilter_get_by_name("buffersink");

// 1. 创建 buffer source(输入端)
char args[512];
snprintf(args, sizeof(args),
"video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=1/1",
width, height, pix_fmt, time_base.num, time_base.den);

avfilter_graph_create_filter(buffersrc_ctx, buffersrc, "in",
args, NULL, graph);

// 2. 创建 buffer sink(输出端)
avfilter_graph_create_filter(buffersink_ctx, buffersink, "out",
NULL, NULL, graph);

// 3. 解析滤镜描述字符串,连接 buffer source 到 buffer sink
AVFilterInOut* outputs = avfilter_inout_alloc();
outputs->name = av_strdup("in");
outputs->filter_ctx = *buffersrc_ctx;
outputs->pad_idx = 0;
outputs->next = NULL;

AVFilterInOut* inputs = avfilter_inout_alloc();
inputs->name = av_strdup("out");
inputs->filter_ctx = *buffersink_ctx;
inputs->pad_idx = 0;
inputs->next = NULL;

// 使用 avfilter_graph_parse2 解析复杂的滤镜描述
int ret = avfilter_graph_parse2(graph, filter_desc, &inputs, &outputs);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "滤镜图解析失败: %s\n", av_err2str(ret));
return ret;
}

// 4. 配置滤镜图
ret = avfilter_graph_config(graph, NULL);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "滤镜图配置失败\n");
return ret;
}

*filter_graph = graph;
return 0;
}

4.2 常用滤镜描述字符串示例

// 例 1:缩放 + 裁剪
const char* desc1 = "scale=1280:720,crop=640:360:100:50";
// 先缩放到 1280x720,再从中裁剪 640x360(偏移 x=100, y=50)

// 例 2:水印叠加
const char* desc2 =
"movie=watermark.png[wm];"
"[in][wm]overlay=W-w-10:H-h-10[out]";
// 加载 watermark.png,叠加在右下角(距右/下 10px)

// 例 3:FPS 转换 + 旋转
const char* desc3 = "fps=30,transpose=1";
// 转为 30fps,顺时针旋转 90 度(transpose=1)

// 例 4:画中画
const char* desc4 =
"[in]scale=iw/2:ih/2[pip];"
"[in][pip]overlay=main_w-overlay_w-10:main_h-overlay_h-10";

// 例 5:添加文本
const char* desc5 =
"drawtext=text='Android FFmpeg':fontsize=24:fontcolor=white:"
"x=10:y=10:box=1:boxcolor=black@0.5";

// 例 6:视频去噪(hqdn3d 滤镜)
const char* desc6 = "hqdn3d=4:3:6:4.5";

4.3 在解码循环中使用滤镜图

// 将解码后的帧送入滤镜图
av_buffersrc_add_frame_flags(buffersrc_ctx, decoded_frame, AV_BUFFERSRC_FLAG_KEEP_REF);

// 从滤镜图取出处理后的帧
AVFrame* filtered_frame = av_frame_alloc();
while (1) {
int ret = av_buffersink_get_frame(buffersink_ctx, filtered_frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
if (ret < 0) { /* 错误处理 */ }

// 使用 filtered_frame(例如送入编码器或保存为图像)
process_filtered_frame(filtered_frame);
av_frame_unref(filtered_frame);
}

五、Seek 操作

精确的跳转是播放器的基础功能。FFmpeg 使用 av_seek_frameavformat_seek_file

5.1 按时间戳跳转

int seek_to_position(AVFormatContext* fmt_ctx, int64_t target_ts_ms) {
// 1. 将毫秒时间戳转换为流的 time_base 单位
int video_stream_index = /* 视频流索引 */;
AVStream* video_stream = fmt_ctx->streams[video_stream_index];

int64_t seek_target = av_rescale_q(
target_ts_ms,
(AVRational){1, 1000}, // 毫秒 time_base
video_stream->time_base // 视频流 time_base
);

// 2. 跳转
int ret = av_seek_frame(fmt_ctx,
video_stream_index,
seek_target,
AVSEEK_FLAG_BACKWARD); // 跳转到目标之前最近的关键帧

if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Seek 失败\n");
return ret;
}

// 3. 刷新解码器
avcodec_flush_buffers(video_codec_ctx);
avcodec_flush_buffers(audio_codec_ctx);

return 0;
}

5.2 seek 的几种模式

Flag 说明
AVSEEK_FLAG_BACKWARD 跳到目标时间戳之前最近的关键帧(默认且最常用)
AVSEEK_FLAG_BYTE 按字节偏移跳转(非按时间)
AVSEEK_FLAG_ANY 跳转到任意帧(可能从非关键帧开始解码,画质暂时不佳)
AVSEEK_FLAG_FRAME 按帧号跳转

精确 seek 的实现策略:先 AVSEEK_FLAG_BACKWARD 跳到目标之前的关键帧,然后继续解码(丢弃不需要的帧)直到到达目标时间。这是播放器实现「拖动进度条」时的标准做法。

// 精确跳转:seek + 舍弃中间帧
int precise_seek_and_decode(AVFormatContext* fmt_ctx,
AVCodecContext* video_ctx,
int64_t target_pts,
void (*on_frame)(AVFrame*)) {
av_seek_frame(fmt_ctx, video_stream_idx, target_pts, AVSEEK_FLAG_BACKWARD);
avcodec_flush_buffers(video_ctx);

AVPacket* pkt = av_packet_alloc();
AVFrame* frame = av_frame_alloc();

while (av_read_frame(fmt_ctx, pkt) >= 0) {
if (pkt->stream_index != video_stream_idx) {
av_packet_unref(pkt);
continue;
}

avcodec_send_packet(video_ctx, pkt);
while (avcodec_receive_frame(video_ctx, frame) >= 0) {
if (frame->pts >= target_pts) {
// 到达目标时间点,开始正式处理
on_frame(frame);
}
// 小于 target_pts 的帧:解码但丢弃(保证后续 B/P 帧能正确解码)
av_frame_unref(frame);
}
av_packet_unref(pkt);
}

av_frame_free(&frame);
av_packet_free(&pkt);
return 0;
}

六、硬件加速:MediaCodec 集成

6.1 FFmpeg 的 MediaCodec 支持

FFmpeg 从 3.1 版本开始原生支持 Android MediaCodec。通过编译时开启 --enable-mediacodec--enable-jni,可以在不写 Java 代码的情况下直接使用硬件编解码器。

6.2 使用 MediaCodec 硬件解码

// 查找硬件解码器(优先级高于软件解码器)
const AVCodec* find_best_decoder(enum AVCodecID codec_id) {
// 优先使用 MediaCodec 硬件解码器
if (codec_id == AV_CODEC_ID_H264) {
const AVCodec* hw_decoder = avcodec_find_decoder_by_name("h264_mediacodec");
if (hw_decoder) return hw_decoder;
} else if (codec_id == AV_CODEC_ID_HEVC) {
const AVCodec* hw_decoder = avcodec_find_decoder_by_name("hevc_mediacodec");
if (hw_decoder) return hw_decoder;
}

// 回退到软件解码器
return avcodec_find_decoder(codec_id);
}

// 初始化硬件解码器(与软件解码器相同的 API)
AVCodecContext* init_hw_decoder(const AVCodec* codec, AVCodecParameters* codecpar) {
AVCodecContext* ctx = avcodec_alloc_context3(codec);
avcodec_parameters_to_context(ctx, codecpar);

// 硬件加速不需要特殊设置,直接打开即可
avcodec_open2(ctx, codec, NULL);
return ctx;
}

6.3 JNI 层包装

在实际 Android 应用中,Native 层通过 JNI 与 Java/Kotlin 交互。典型架构:

Kotlin/Java 层
├── FFmpegPlayer.kt # 播放器接口
├── SurfaceView # 视频渲染
└── AudioTrack # 音频播放

│ JNI

Native C/C++ 层 (libnative-player.so)
├── player_jni.cpp # JNI 函数注册与回调
├── decoder_video.cpp # 视频解码线程
├── decoder_audio.cpp # 音频解码线程
├── renderer_video.cpp # ANativeWindow 渲染
└── renderer_audio.cpp # OpenSL ES / AAudio 输出


FFmpeg 库
├── libavformat.so
├── libavcodec.so
├── libavutil.so
├── libswscale.so
└── libswresample.so

关键 JNI 函数示例:

// 从 Java 层传递 Surface 到 Native 层
static ANativeWindow* native_window = NULL;

JNIEXPORT void JNICALL
Java_com_example_player_FFmpegPlayer_nativeSetSurface(
JNIEnv* env, jobject thiz, jobject surface) {

// 释放之前的窗口
if (native_window) {
ANativeWindow_release(native_window);
native_window = NULL;
}

// 从 Surface 对象创建 ANativeWindow
if (surface) {
native_window = ANativeWindow_fromSurface(env, surface);
}
}

// 渲染解码后的视频帧
void render_frame(AVFrame* frame) {
if (!native_window) return;

// 设置缓冲区大小
ANativeWindow_setBuffersGeometry(native_window,
frame->width, frame->height,
WINDOW_FORMAT_RGBA_8888);

ANativeWindow_Buffer window_buffer;
// 锁定窗口缓冲区
if (ANativeWindow_lock(native_window, &window_buffer, NULL) == 0) {
// 使用 sws_scale 将 YUV 转为 RGBA,直接写入 window_buffer.bits
// ...
ANativeWindow_unlockAndPost(native_window);
}
}

七、完整转码示例

以下是一个从输入文件转到输出文件的完整 C 程序骨架,展示整体管线:

int transcode(const char* input_path, const char* output_path) {
AVFormatContext *in_fmt_ctx = NULL, *out_fmt_ctx = NULL;
AVCodecContext *v_dec_ctx = NULL, *v_enc_ctx = NULL;
AVCodecContext *a_dec_ctx = NULL, *a_enc_ctx = NULL;
int v_stream_idx = -1, a_stream_idx = -1;
int ret;

// ---- 1. 打开输入 ----
avformat_open_input(&in_fmt_ctx, input_path, NULL, NULL);
avformat_find_stream_info(in_fmt_ctx, NULL);

// ---- 2. 初始化解码器 ----
for (int i = 0; i < in_fmt_ctx->nb_streams; i++) {
AVCodecParameters* cp = in_fmt_ctx->streams[i]->codecpar;
if (cp->codec_type == AVMEDIA_TYPE_VIDEO && v_stream_idx < 0) {
v_stream_idx = i;
const AVCodec* dec = avcodec_find_decoder(cp->codec_id);
v_dec_ctx = avcodec_alloc_context3(dec);
avcodec_parameters_to_context(v_dec_ctx, cp);
avcodec_open2(v_dec_ctx, dec, NULL);
} else if (cp->codec_type == AVMEDIA_TYPE_AUDIO && a_stream_idx < 0) {
a_stream_idx = i;
const AVCodec* dec = avcodec_find_decoder(cp->codec_id);
a_dec_ctx = avcodec_alloc_context3(dec);
avcodec_parameters_to_context(a_dec_ctx, cp);
avcodec_open2(a_dec_ctx, dec, NULL);
}
}

// ---- 3. 初始化输出 ----
avformat_alloc_output_context2(&out_fmt_ctx, NULL, NULL, output_path);

AVStream* v_out_stream = avformat_new_stream(out_fmt_ctx, NULL);
AVStream* a_out_stream = avformat_new_stream(out_fmt_ctx, NULL);

// 视频编码器(这里以 H.264 为例)
const AVCodec* v_enc = avcodec_find_encoder(AV_CODEC_ID_H264);
v_enc_ctx = avcodec_alloc_context3(v_enc);
v_enc_ctx->width = v_dec_ctx->width;
v_enc_ctx->height = v_dec_ctx->height;
v_enc_ctx->pix_fmt = v_enc->pix_fmts[0];
v_enc_ctx->time_base = (AVRational){1, 30};
v_enc_ctx->bit_rate = 2000000;
// x264 预设与调优(通过 av_opt_set)
av_opt_set(v_enc_ctx->priv_data, "preset", "fast", 0);
av_opt_set(v_enc_ctx->priv_data, "crf", "23", 0);
avcodec_open2(v_enc_ctx, v_enc, NULL);
avcodec_parameters_from_context(v_out_stream->codecpar, v_enc_ctx);
v_out_stream->time_base = v_enc_ctx->time_base;

// 音频编码器(AAC)
const AVCodec* a_enc = avcodec_find_encoder(AV_CODEC_ID_AAC);
a_enc_ctx = avcodec_alloc_context3(a_enc);
a_enc_ctx->sample_rate = a_dec_ctx->sample_rate;
a_enc_ctx->ch_layout = a_dec_ctx->ch_layout;
a_enc_ctx->sample_fmt = a_enc->sample_fmts[0];
a_enc_ctx->time_base = (AVRational){1, a_enc_ctx->sample_rate};
a_enc_ctx->bit_rate = 128000;
avcodec_open2(a_enc_ctx, a_enc, NULL);
avcodec_parameters_from_context(a_out_stream->codecpar, a_enc_ctx);
a_out_stream->time_base = a_enc_ctx->time_base;

// 打开输出文件和写头
avio_open(&out_fmt_ctx->pb, output_path, AVIO_FLAG_WRITE);
avformat_write_header(out_fmt_ctx, NULL);

// ---- 4. 主循环:读 → 解码 → 编码 → 写 ----
AVPacket *pkt = av_packet_alloc();
AVFrame *dec_frame = av_frame_alloc();
AVPacket *enc_pkt = av_packet_alloc();

while (av_read_frame(in_fmt_ctx, pkt) >= 0) {
if (pkt->stream_index == v_stream_idx) {
// 视频管线
avcodec_send_packet(v_dec_ctx, pkt);
while (avcodec_receive_frame(v_dec_ctx, dec_frame) >= 0) {
// 编码
avcodec_send_frame(v_enc_ctx, dec_frame);
while (avcodec_receive_packet(v_enc_ctx, enc_pkt) >= 0) {
av_packet_rescale_ts(enc_pkt, v_enc_ctx->time_base,
v_out_stream->time_base);
enc_pkt->stream_index = v_out_stream->index;
av_interleaved_write_frame(out_fmt_ctx, enc_pkt);
av_packet_unref(enc_pkt);
}
av_frame_unref(dec_frame);
}
} else if (pkt->stream_index == a_stream_idx) {
// 音频管线(类似)
avcodec_send_packet(a_dec_ctx, pkt);
while (avcodec_receive_frame(a_dec_ctx, dec_frame) >= 0) {
avcodec_send_frame(a_enc_ctx, dec_frame);
while (avcodec_receive_packet(a_enc_ctx, enc_pkt) >= 0) {
av_packet_rescale_ts(enc_pkt, a_enc_ctx->time_base,
a_out_stream->time_base);
enc_pkt->stream_index = a_out_stream->index;
av_interleaved_write_frame(out_fmt_ctx, enc_pkt);
av_packet_unref(enc_pkt);
}
av_frame_unref(dec_frame);
}
}
av_packet_unref(pkt);
}

// ---- 5. 冲刷编码器缓冲区 ----
avcodec_send_frame(v_enc_ctx, NULL);
while (avcodec_receive_packet(v_enc_ctx, enc_pkt) >= 0) {
av_packet_rescale_ts(enc_pkt, v_enc_ctx->time_base, v_out_stream->time_base);
enc_pkt->stream_index = v_out_stream->index;
av_interleaved_write_frame(out_fmt_ctx, enc_pkt);
av_packet_unref(enc_pkt);
}

// ---- 6. 写文件尾并清理 ----
av_write_trailer(out_fmt_ctx);

av_packet_free(&pkt);
av_packet_free(&enc_pkt);
av_frame_free(&dec_frame);
avcodec_free_context(&v_dec_ctx);
avcodec_free_context(&v_enc_ctx);
avcodec_free_context(&a_dec_ctx);
avcodec_free_context(&a_enc_ctx);
avformat_close_input(&in_fmt_ctx);
avio_closep(&out_fmt_ctx->pb);
avformat_free_context(out_fmt_ctx);

return 0;
}

八、性能优化要点

8.1 多线程解码

FFmpeg 支持帧级和片级多线程解码:

codec_ctx->thread_count = 0;  // 自动选择最优线程数
codec_ctx->thread_type = FF_THREAD_FRAME | FF_THREAD_SLICE;
// 对 H.264/H.265 等支持片级多线程的格式可启用 FF_THREAD_SLICE

8.2 零拷贝策略

在解码和渲染之间尽可能避免内存拷贝:

  • 使用 AV_PIX_FMT_MEDIACODEC 时,解码后的帧直接存储在 MediaCodec 的输出 buffer 中,不需要从 GPU 拷贝到 CPU。
  • 在 JNI 层使用 ANativeWindow 直接从 AVFrame 渲染,避免将 YUV 数据拷贝回 Java 堆。

8.3 解码线程与渲染线程分离

// 生产者-消费者模式
typedef struct {
AVFrame* frames[MAX_QUEUE_SIZE];
int read_idx, write_idx;
pthread_mutex_t mutex;
pthread_cond_t cond;
} FrameQueue;

// 解码线程:produce → enqueue
// 渲染线程:dequeue → render → av_frame_unref

九、常见问题与解决方案

9.1 时间戳不连续

网络流(如 RTMP/HLS)可能出现时间戳跳跃。需要在解封装后进行时间戳修复:

// 设置时基校正
AVFormatContext* fmt_ctx = ...;
fmt_ctx->max_ts_probe = 0; // 关闭时间戳探测
// 或者对解包后的 AVPacket 自行重建时间戳
static int64_t pts_offset = 0;
if (pts_offset == 0) pts_offset = -pkt->pts;
pkt->pts += pts_offset;
pkt->dts += pts_offset;

9.2 H.264 annex B 与 avcC

MP4 容器中的 H.264 使用 avcC 格式存储 extradata(含 SPS/PPS),而裸流使用 annex B 格式(起始码 00 00 00 01)。如果需要在两者之间转换:

// annex B → avcC(ffmpeg 自动处理,但手动操作时注意)
// 可以使用 h264_mp4toannexb 比特流滤镜
const AVBitStreamFilter* bsf = av_bsf_get_by_name("h264_mp4toannexb");

9.3 内存管理陷阱

// 常见错误:忘记 av_frame_unref 导致内存泄漏
// AVFrame 内部通过引用计数管理缓冲区,av_frame_unref 减引用计数
AVFrame* frame = av_frame_alloc();
avcodec_receive_frame(dec_ctx, frame);
// ... 使用 frame ...
av_frame_unref(frame); // 必须!否则缓冲区永不释放

// 同理,AVPacket 也需要 av_packet_unref
AVPacket* pkt = av_packet_alloc();
av_read_frame(fmt_ctx, pkt);
// ... 使用 pkt ...
av_packet_unref(pkt); // 必须!

十、总结

FFmpeg 在 Android 上的集成分为三大块:交叉编译、C API 管线、JNI 桥接。

  1. 交叉编译:通过 NDK + configure 脚本生成各 ABI 的 .so 库,集成 MediaCodec 硬件加速支持。
  2. 核心管线:avformat(解封装)→ avcodec(解码)→ avfilter/swscale(处理)→ avcodec(编码)→ avformat(封装),每一步遵循 send/receive 模式。
  3. 滤镜图:通过 avfilter_graph_create_filter + avfilter_graph_parse2 构建,支持缩放、裁剪、水印、去噪等复杂处理。
  4. Seek:使用 av_seek_frame + 解码器 flush + 丢弃中间帧实现精确跳转。
  5. 硬件加速:通过 MediaCodec 的解码器名(h264_mediacodec 等)由 FFmpeg 自动调用 Android 硬件解码,API 与软件解码完全一致。

掌握这些知识后,你就可以在 Android 上构建完整的视频播放器、转码工具、视频编辑应用等。

参考资料

打赏
  • 微信
  • 支付宝

评论