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 为例):
export ANDROID_NDK_HOME=/home/user/android-ndk-r25c
export API=24
export HOST_TAG=linux-x86_64
|
FFmpeg 使用 configure 脚本配置编译选项。以下是一个面向 Android 的典型配置:
#!/bin/bash
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 }
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
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
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")
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>
void init_ffmpeg() { av_log_set_level(AV_LOG_INFO); }
|
解封装将容器的字节流拆解为独立的 AVPacket(压缩数据包)。核心流程:
int demux(const char* input_path) { AVFormatContext* fmt_ctx = NULL; int ret; int video_stream_index = -1; int audio_stream_index = -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; }
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; }
av_dump_format(fmt_ctx, 0, input_path, 0);
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); } }
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); }
av_packet_free(&pkt); avformat_close_input(&fmt_ctx); return 0; }
|
关键点:
av_read_frame 返回的 AVPacket 可能包含完整帧或不完整帧,由解码器处理。
av_packet_unref 释放 AVPacket 内部引用的数据缓冲区,必须在每次迭代后调用。
avformat_find_stream_info 会读取一小部分数据来探测编码参数(可能耗时,可以设置 probesize 和 max_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; const AVCodec* codec = avcodec_find_decoder(codecpar->codec_id); if (!codec) { av_log(NULL, AV_LOG_ERROR, "找不到解码器: %d\n", codecpar->codec_id); return NULL; }
AVCodecContext* codec_ctx = avcodec_alloc_context3(codec); if (!codec_ctx) return NULL;
if (avcodec_parameters_to_context(codec_ctx, codecpar) < 0) { avcodec_free_context(&codec_ctx); return NULL; }
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; }
int decode_packet_to_frames(AVCodecContext* codec_ctx, AVPacket* pkt, AVFrame* frame, void (*on_frame)(AVFrame*)) { int ret = avcodec_send_packet(codec_ctx, pkt); if (ret < 0) { return ret; }
while (ret >= 0) { ret = avcodec_receive_frame(codec_ctx, frame); if (ret == AVERROR(EAGAIN)) { break; } else if (ret == AVERROR_EOF) { break; } else if (ret < 0) { return ret; }
on_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); 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, NULL, NULL, NULL ); return sws_ctx; }
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); }
|
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) { int ret = avcodec_send_frame(enc_ctx, frame); if (ret < 0) return ret;
while (ret >= 0) { ret = avcodec_receive_packet(enc_ctx, pkt); if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { break; } 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); 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) { avformat_alloc_output_context2(out_ctx, NULL, NULL, output_path); if (!out_ctx) return -1;
AVFormatContext* oc = *out_ctx;
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;
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;
if (!(oc->oformat->flags & AVFMT_NOFILE)) { avio_open(&oc->pb, output_path, AVIO_FLAG_WRITE); }
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"); 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);
avfilter_graph_create_filter(buffersink_ctx, buffersink, "out", NULL, NULL, graph);
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;
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; }
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 常用滤镜描述字符串示例
const char* desc1 = "scale=1280:720,crop=640:360:100:50";
const char* desc2 = "movie=watermark.png[wm];" "[in][wm]overlay=W-w-10:H-h-10[out]";
const char* desc3 = "fps=30,transpose=1";
const char* desc4 = "[in]scale=iw/2:ih/2[pip];" "[in][pip]overlay=main_w-overlay_w-10:main_h-overlay_h-10";
const char* desc5 = "drawtext=text='Android FFmpeg':fontsize=24:fontcolor=white:" "x=10:y=10:box=1:boxcolor=black@0.5";
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) { }
process_filtered_frame(filtered_frame); av_frame_unref(filtered_frame); }
|
五、Seek 操作
精确的跳转是播放器的基础功能。FFmpeg 使用 av_seek_frame 或 avformat_seek_file:
5.1 按时间戳跳转
int seek_to_position(AVFormatContext* fmt_ctx, int64_t target_ts_ms) { 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}, video_stream->time_base );
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; }
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 跳到目标之前的关键帧,然后继续解码(丢弃不需要的帧)直到到达目标时间。这是播放器实现「拖动进度条」时的标准做法。
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); } av_frame_unref(frame); } av_packet_unref(pkt); }
av_frame_free(&frame); av_packet_free(&pkt); return 0; }
|
FFmpeg 从 3.1 版本开始原生支持 Android MediaCodec。通过编译时开启 --enable-mediacodec 和 --enable-jni,可以在不写 Java 代码的情况下直接使用硬件编解码器。
const AVCodec* find_best_decoder(enum AVCodecID codec_id) { 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); }
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 函数示例:
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; } 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) { 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;
avformat_open_input(&in_fmt_ctx, input_path, NULL, NULL); avformat_find_stream_info(in_fmt_ctx, NULL);
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); } }
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);
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; 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;
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);
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); }
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); }
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;
|
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;
|
九、常见问题与解决方案
9.1 时间戳不连续
网络流(如 RTMP/HLS)可能出现时间戳跳跃。需要在解封装后进行时间戳修复:
AVFormatContext* fmt_ctx = ...; fmt_ctx->max_ts_probe = 0;
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)。如果需要在两者之间转换:
const AVBitStreamFilter* bsf = av_bsf_get_by_name("h264_mp4toannexb");
|
9.3 内存管理陷阱
AVFrame* frame = av_frame_alloc(); avcodec_receive_frame(dec_ctx, frame);
av_frame_unref(frame);
AVPacket* pkt = av_packet_alloc(); av_read_frame(fmt_ctx, pkt);
av_packet_unref(pkt);
|
十、总结
FFmpeg 在 Android 上的集成分为三大块:交叉编译、C API 管线、JNI 桥接。
- 交叉编译:通过 NDK + configure 脚本生成各 ABI 的 .so 库,集成 MediaCodec 硬件加速支持。
- 核心管线:avformat(解封装)→ avcodec(解码)→ avfilter/swscale(处理)→ avcodec(编码)→ avformat(封装),每一步遵循 send/receive 模式。
- 滤镜图:通过 avfilter_graph_create_filter + avfilter_graph_parse2 构建,支持缩放、裁剪、水印、去噪等复杂处理。
- Seek:使用 av_seek_frame + 解码器 flush + 丢弃中间帧实现精确跳转。
- 硬件加速:通过 MediaCodec 的解码器名(h264_mediacodec 等)由 FFmpeg 自动调用 Android 硬件解码,API 与软件解码完全一致。
掌握这些知识后,你就可以在 Android 上构建完整的视频播放器、转码工具、视频编辑应用等。
参考资料: