Logo
FFmpeg 7 Native C++ Pipeline

Android FFmpeg 7 C++ API Video Transcoding & Slicing

Implement frame-by-frame decoding, filtering, AV-sync, scaling, and precise progress calculations directly utilizing libavcodec and libavfilter.

Step 1: Native C++ Project Architecture

Standard layout for modern high-performance media SDKs

Native SDK Layout

app/
├── src/
│   └── main/
│       ├── java/com/media/ffmpeg/
│       │   ├── FFmpegVideoEditor.kt
│       │   └── OnProgressListener.kt
│       ├── cpp/
│       │   ├── include/ (FFmpeg 7 原生头文件)
│       │   ├── lib/ (arm64-v8a 动态库)
│       │   ├── VideoCropEngine.h (底层媒体处理头文件)
│       │   ├── VideoCropEngine.cpp (C++ API 核心逻辑)
│       │   ├── ffmpeg_jni.cpp (JNI 映射边界层)
│       │   └── CMakeLists.txt
└── build.gradle
CMakeLists.txt (链接原生 FFmpeg 7 核心库集群)
cmake_minimum_required(VERSION 3.22.1)
project("ffmpeg_native_editor")

set(FFMPEG_LIB_DIR ${CMAKE_SOURCE_DIR}/lib/${ANDROID_ABI})
include_directories(${CMAKE_SOURCE_DIR}/include)

# 动态载入 FFmpeg 7 各项底层子组件
foreach(MODULE avformat avcodec avfilter avutil swresample swscale)
    add_library(${MODULE} SHARED IMPORTED)
    set_target_properties(${MODULE} PROPERTIES IMPORTED_LOCATION ${FFMPEG_LIB_DIR}/lib${MODULE}.so)
endforeach()

# 编译业务层原生的 C++ 剪辑核心
add_library(ffmpeg_editor SHARED 
    VideoCropEngine.cpp 
    ffmpeg_jni.cpp
)

target_link_libraries(ffmpeg_editor
    avformat avcodec avfilter avutil swresample swscale
    log android
)

Step 2: C++ Media Processing Engine Blueprint

Encapsulate pipeline state variables cleanly in a native class

VideoCropEngine.h
C++ Header
#ifndef VIDEO_CROP_ENGINE_H
#define VIDEO_CROP_ENGINE_H

#include <string>
#include <functional>

extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavfilter/avfilter.h>
#include <libavutil/avutil.h>
}

class VideoCropEngine {
public:
    using ProgressCallback = std::function<void(int progress)>;

    VideoCropEngine();
    ~VideoCropEngine();

    bool cropAndScale(
        const std::string& inputPath,
        const std::string& outputPath,
        double startTime,
        double endTime,
        int targetWidth,
        int targetHeight,
        ProgressCallback callback
    );

private:
    AVFormatContext* ifmt_ctx = nullptr;
    AVFormatContext* ofmt_ctx = nullptr;
    
    // 针对现代 FFmpeg 7 的流管理器句柄
    int video_stream_idx = -1;
    int audio_stream_idx = -1;

    bool openInput(const std::string& path);
    bool initOutput(const std::string& path, int width, int height);
    bool initFilterGraph(int width, int height);
    void closePipeline();
};

#endif // VIDEO_CROP_ENGINE_H

Step 3: Implementing Pipeline, AV-Sync & Resizing

Realize frame level stream synchronization using precise duration tracking

VideoCropEngine.cpp
C++ Implementation
#include "VideoCropEngine.h"
#include <android/log.h>

#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "FFmpegEngine", __VA_ARGS__)

VideoCropEngine::VideoCropEngine() {}
VideoCropEngine::~VideoCropEngine() { closePipeline(); }

bool VideoCropEngine::openInput(const std::string& path) {
    if (avformat_open_input(&ifmt_ctx, path.c_str(), nullptr, nullptr) < 0) return false;
    if (avformat_find_stream_info(ifmt_ctx, nullptr) < 0) return false;
    
    for (unsigned int i = 0; i < ifmt_ctx->nb_streams; i++) {
        if (ifmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO && video_stream_idx < 0) {
            video_stream_idx = i;
        } else if (ifmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && audio_stream_idx < 0) {
            audio_stream_idx = i;
        }
    }
    return video_stream_idx >= 0;
}

bool VideoCropEngine::cropAndScale(
    const std::string& inputPath, const std::string& outputPath,
    double startTime, double endTime, int targetWidth, int targetHeight,
    ProgressCallback callback) {
    
    if (!openInput(inputPath)) return false;
    
    // 1. 精准控制起点 Seek 操作
    int64_t seek_target = startTime * AV_TIME_BASE;
    if (avformat_seek_file(ifmt_ctx, -1, INT64_MIN, seek_target, INT64_MAX, AV_SEEK_FLAG_BACKWARD) < 0) {
        LOGE("Seek failed to start time.");
        return false;
    }

    // 2. 初始化输出编码器与复用上下文 (假定已初始化 in_codec_ctx, out_codec_ctx, ofmt_ctx)
    // 3. 构建 FFmpeg 7 画面分辨率调整过滤器 (假定已初始化 buffersrc_ctx, buffersink_ctx)

    AVPacket* in_packet = av_packet_alloc();
    AVFrame* decoded_frame = av_frame_alloc();
    AVFrame* filtered_frame = av_frame_alloc();
    AVPacket* out_packet = av_packet_alloc();

    double total_duration = endTime - startTime;
    int64_t start_video_pts = -1; // 用于基准时间戳平移
    bool execution_success = true;

    // 4. 解复用 -> 解码 -> 像素过滤 -> 重新编码的主循环流
    while (av_read_frame(ifmt_ctx, in_packet) >= 0) {
        AVStream* in_stream = ifmt_ctx->streams[in_packet->stream_index];
        double current_pts_time = in_packet->pts * av_q2d(in_stream->time_base);
        
        // 超过终点时刻,立即打破循环安全截断
        if (current_pts_time > endTime) {
            av_packet_unref(in_packet);
            break;
        }

        if (in_packet->stream_index == video_stream_idx) {
            // 记录裁剪起点首帧 PTS,作为绝对平移坐标系原点
            if (start_video_pts == -1) start_video_pts = in_packet->pts;

            // [视频管道:送去解码器]
            if (avcodec_send_packet(in_codec_ctx, in_packet) == 0) {
                while (avcodec_receive_frame(in_codec_ctx, decoded_frame) == 0) {
                    
                    // 毫秒级高阶进度换算公式
                    if (current_pts_time >= startTime) {
                        double elapsed = current_pts_time - startTime;
                        int percent = (int)((elapsed / total_duration) * 100);
                        if (percent >= 0 && percent <= 100 && callback) {
                            callback(percent); // 触发上抛 lambda 闭包句柄
                        }
                        
                        // [送入 AVFilter 处理缩放]
                        if (av_buffersrc_add_frame_flags(buffersrc_ctx, decoded_frame, AV_BUFFERSRC_FLAG_KEEP_REF) == 0) {
                            while (av_buffersink_get_frame(buffersink_ctx, filtered_frame) == 0) {
                                
                                // [音视频同步铁律:时间戳根据裁剪起点进行绝对平移重校]
                                filtered_frame->pts = filtered_frame->best_effort_timestamp - start_video_pts;
                                
                                // [送入编码器进行重新编码,打碎并注入新 GOP 帧组合]
                                if (avcodec_send_frame(out_codec_ctx, filtered_frame) == 0) {
                                    while (avcodec_receive_packet(out_codec_ctx, out_packet) == 0) {
                                        // 转换 TimeBase 并写入容器
                                        av_packet_rescale_ts(out_packet, out_codec_ctx->time_base, out_stream->time_base);
                                        av_interleaved_write_frame(ofmt_ctx, out_packet);
                                        av_packet_unref(out_packet);
                                    }
                                }
                                av_frame_unref(filtered_frame);
                            }
                        }
                    }
                    av_frame_unref(decoded_frame);
                }
            }
        } else if (in_packet->stream_index == audio_stream_idx) {
            // [音频管道:直接平移写入,或送入 SwrContext 重采样后编码]
            if (current_pts_time >= startTime) {
                // (此处省略音频平移或重编码的写入逻辑)
            }
        }
        av_packet_unref(in_packet);
    }

    // 刷新编码器缓存帧 (Flush Encoder)
    avcodec_send_frame(out_codec_ctx, nullptr);
    while (avcodec_receive_packet(out_codec_ctx, out_packet) == 0) {
        av_packet_rescale_ts(out_packet, out_codec_ctx->time_base, out_stream->time_base);
        av_interleaved_write_frame(ofmt_ctx, out_packet);
        av_packet_unref(out_packet);
    }

    // 回收所有的资源
    av_packet_free(&in_packet);
    av_frame_free(&decoded_frame);
    av_frame_free(&filtered_frame);
    av_packet_free(&out_packet);
    closePipeline();
    
    if (callback) callback(100); // 强刷 100% 确保状态完结
    return execution_success;
}

void VideoCropEngine::closePipeline() {
    if (ifmt_ctx) { avformat_close_input(&ifmt_ctx); ifmt_ctx = nullptr; }
    if (ofmt_ctx) { av_write_trailer(ofmt_ctx); avformat_free_context(ofmt_ctx); ofmt_ctx = nullptr; }
}

Step 3.5: AVFilterGraph & Output Context Initialization

Detailed look at initializing libavfilter for high-performance rendering

VideoCropEngine.cpp
C++ Extended Implementation
// 补全:初始化 FFmpeg 7 画面缩放滤镜链条的真实实现
bool VideoCropEngine::initFilterGraph(int target_width, int target_height) {
    char filter_args[512];
    AVFilterGraph *graph = avfilter_graph_alloc();
    const AVFilter *buffersrc = avfilter_get_by_name("buffer");
    const AVFilter *buffersink = avfilter_get_by_name("buffersink");

    AVStream* video_stream = ifmt_ctx->streams[video_stream_idx];
    AVCodecParameters* codecpar = video_stream->codecpar;

    // 格式化输入源描述符:必须精准传递像素格式(pix_fmt)、宽高和时间基(time_base)
    snprintf(filter_args, sizeof(filter_args),
             "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
             codecpar->width, codecpar->height, codecpar->format,
             video_stream->time_base.num, video_stream->time_base.den,
             codecpar->sample_aspect_ratio.num, codecpar->sample_aspect_ratio.den);

    if (avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in", filter_args, nullptr, graph) < 0) return false;
    if (avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out", nullptr, nullptr, graph) < 0) return false;

    // 限定输出像素格式为现代标准的 YUV420P,防止编码器不可识别
    enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_YUV420P, AV_PIX_FMT_NONE };
    if (av_opt_set_int_list(buffersink_ctx, "pix_fmts", pix_fmts, AV_PIX_FMT_NONE, AV_OPT_SEARCH_CHILDREN) < 0) return false;

    // 声明滤镜描述:scale 宽/高。FFmpeg 7 推荐显式构建管线端点
    AVFilterInOut *outputs = avfilter_inout_alloc();
    AVFilterInOut *inputs  = avfilter_inout_alloc();

    outputs->name       = av_strdup("in");
    outputs->filter_ctx = buffersrc_ctx;
    outputs->pad_idx    = 0;
    outputs->next       = nullptr;

    inputs->name        = av_strdup("out");
    inputs->filter_ctx  = buffersink_ctx;
    inputs->pad_idx     = 0;
    inputs->next        = nullptr;

    char filter_spec[128];
    snprintf(filter_spec, sizeof(filter_spec), "scale=%d:%d", target_width, target_height);

    if (avfilter_graph_parse_ptr(graph, filter_spec, &inputs, &outputs, nullptr) < 0) return false;
    if (avfilter_graph_config(graph, nullptr) < 0) return false;

    avfilter_inout_free(&inputs);
    avfilter_inout_free(&outputs);
    return true;
}

Step 3.6: Audio Stream Demuxing & Absolute Realignment

Synchronize audio sample timestamps flawlessly alongside video timeline shifting

VideoCropEngine.cpp
C++ Implementation Block
} else if (in_packet->stream_index == audio_stream_idx) {
    // 补全:主处理循环中音频轨道的完整物理处理
    if (current_pts_time >= startTime) {
        AVStream* in_stream = ifmt_ctx->streams[audio_stream_idx];
        AVStream* out_stream = ofmt_ctx->streams[audio_stream_idx];

        // 记录音频裁剪起点首帧的绝对时间戳
        if (start_audio_pts == -1) {
            start_audio_pts = in_packet->pts;
        }

        // 音频时间戳平移公式:消灭剪切后播放器黑屏、无声的顽疾
        int64_t internal_pts = in_packet->pts - start_audio_pts;
        int64_t internal_dts = in_packet->dts - start_audio_pts;

        // 如果是纯流 Copy 转发 (Remuxing),进行 TimeBase 标尺转换
        in_packet->pts = av_rescale_q_rnd(internal_pts, in_stream->time_base, out_stream->time_base, 
                            static_cast<AVRounding>(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
        in_packet->dts = av_rescale_q_rnd(internal_dts, in_stream->time_base, out_stream->time_base, 
                            static_cast<AVRounding>(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
        
        in_packet->duration = av_rescale_q(in_packet->duration, in_stream->time_base, out_stream->time_base);
        in_packet->pos = -1; // 抹除源文件物理偏移,强制容器重新串行计算索引

        // 线程安全地交织写入输出容器
        if (av_interleaved_write_frame(ofmt_ctx, in_packet) < 0) {
            LOGE("Error muxing audio packet seamlessly.");
        }
    }
}

Step 4: JNI Environment Mapping Layer

Bridge native asynchronous threads safely back to Kotlin listeners

ffmpeg_jni.cpp
C++ JNI Bridge
#include <jni.h>
#include "VideoCropEngine.h"

extern "C"
JNIEXPORT jboolean JNICALL
Java_com_media_ffmpeg_FFmpegVideoEditor_nativeCropAndScale(
        JNIEnv *env, jobject thiz,
        jstring input_path, jstring output_path,
        jdouble start_time, jdouble end_time,
        jint target_width, jint target_height,
        jobject listener) {

    const char *c_input_path = env->GetStringUTFChars(input_path, nullptr);
    const char *c_output_path = env->GetStringUTFChars(output_path, nullptr);

    // 1. 获取全局唯一的 JVM 指针以支持底层跨线程安全回调
    JavaVM* jvm = nullptr;
    env->GetJavaVM(&jvm);

    // 2. 提升 listener 引用生命周期为全局,防止被自动内存回收
    jobject global_listener = env->NewGlobalRef(listener);
    jclass listener_class = env->GetObjectClass(global_listener);
    jmethodID on_progress_id = env->GetMethodID(listener_class, "onProgress", "(I)V");

    VideoCropEngine engine;
    
    // 3. 构造底层事件闭包 Lambda
    auto progress_lambda = [jvm, global_listener, on_progress_id](int progress) {
        JNIEnv* async_env = nullptr;
        // 动态附加当前转码线程到 Java 虚拟机
        if (jvm->AttachCurrentThread(&async_env, nullptr) == JNI_OK) {
            async_env->CallVoidMethod(global_listener, on_progress_id, progress);
        }
    };

    // 执行核心转码流程
    bool result = engine.cropAndScale(
            c_input_path, c_output_path,
            start_time, end_time,
            target_width, target_height,
            progress_lambda
    );

    // 4. 清理、释放在 JVM 中的占位资产
    env->DeleteGlobalRef(global_listener);
    env->ReleaseStringUTFChars(input_path, c_input_path);
    env->ReleaseStringUTFChars(output_path, c_output_path);

    return result ? JNI_TRUE : JNI_FALSE;
}

Step 5: App Toolkit Definition & Interoperability

Clean interface binding for smooth integration into downstream architectures

OnProgressListener.kt
package com.media.ffmpeg

/**
 * 引擎层高频进度上抛协议
 */
interface OnProgressListener {
    fun onProgress(progress: Int)
}
FFmpegVideoEditor.kt
package com.media.ffmpeg

object FFmpegVideoEditor {
    init {
        // 装载底层多层级合成的核心组件 so 依赖
        System.loadLibrary("ffmpeg_editor")
    }

    external fun nativeCropAndScale(
        inputPath: String,
        outputPath: String,
        startTime: Double,
        endTime: Double,
        targetWidth: Int,
        targetHeight: Int,
        listener: OnProgressListener?
    ): Boolean
}

Step 6: Production Launch & AV-Sync Strategy

Ensure perfect stream rendering with frame accurate alignment

API AV-Sync Precision Rule: When using CLI strings like '-ss', FFmpeg relies on external guesses at packet splitting points. In pure C++ code, when we catch packets inside the read loop, we subtract 'startTime' directly from the packet's PTS/DTS and adjust both media tracks to start precisely at 0. This forces container indexing blocks to realign perfectly, avoiding any audio drift or broken B-frame issues entirely.
Kotlin Coroutine Caller

使用调度器隔离,使高密度的底层 C++ 解码循环在后台稳健运行,同时不阻塞 Android App UI 渲染。

// 在架构层开启隔离的工作池线程
CoroutineScope(Dispatchers.IO).launch {
    val isJobDone = FFmpegVideoEditor.nativeCropAndScale(
        inputPath = "/sdcard/Movies/input_1080p.mp4",
        outputPath = "/sdcard/Movies/output_crop_480p.mp4",
        startTime = 5.0,   // 第 5.0 秒起
        endTime = 25.5,   // 第 25.5 秒止
        targetWidth = 854,
        targetHeight = 480, // 压缩并缩放至 480P 规格
        listener = object : OnProgressListener {
            override fun onProgress(progress: Int) {
                // 原生层跨线程直接抛回
                Log.i("Transcode", "当前视频处理中: $progress%")
            }
        }
    )
}
Native API Architecture Benefits

纯 C++ 重编码控制链在大型商业媒体架构中的优势:

精确可控 可以在帧解码后,随时插入定制图层、水印滤镜或改变像素格式(如 NV21 转 YUV420P)。
零漏帧百分比 进度非估算!进度由 `(current_frame_pts - start) / delta` 得出,线性平滑无跳跃。
内存安全 不产生外部进程,避免了 CLI 模式频繁拉起 `fork` 子进程导致的 Android OOM 崩溃。