交叉编译多平台 FFmpeg 库并提取视频帧

交叉编译多平台 FFmpeg 库并提取视频帧

本文档适用于 x86 平台编译 armeabi、armeabi-v7a、arm64-v8a、x86、x86_64 平台的 ffmpeg 运行库

编译新版 FFmpeg 请将 NDK 版本替换为 R17c 以上,将编译脚本中 extra-cflags 参数的值修改为 "$CFALGS -Os -fPIC -DANDROID -Wfatal-errors -Wno-deprecated -isysroot $NDK_PATH/sysroot -I$NDK_PATH/sysroot/usr/include/$4",并在 ./configure 命令结尾添加 --enable-avresample --enable-nonfree --enable-postproc

开发环境

编译环境: Ubuntu 1810 x64

开发环境: Windows 10

IDE: Android Studio 3.4.1

Android: 7.1

FFmpeg: 3.4.6

编译流程

下载 FFmpeg 源码: Download FFmpeg

解压后进入源码包,创建 build.sh 文件,并赋予执行权限

tar zxvf ffmpeg-3.4.6.tar.gz
cd ffmpeg-3.4.6
touch build.sh
chmod +x build.sh

将以下脚本写入 build.sh

NDK_PATH 建议下载 Revision 15C 版本
根据实际情况修改 NDK_PATH,TOOLCHAIN_VERSION 及 ANDROID_VERSION

#!/bin/sh

MY_LIBS_NAME=ffmpeg-3.4.6

# 编译产生的中间件目录
MY_BUILD_DIR=binary

# NDK 目录
NDK_PATH=/usr/android-sdk-linux/android-ndk-r15c
# 编译平台
BUILD_PLATFORM=linux-x86_64
# NDK 中交叉编译工具版本
TOOLCHAIN_VERSION=4.9
# Android API Level
ANDROID_VERSION=26

ANDROID_ARMV5_CFLAGS="-march=armv5te"
ANDROID_ARMV7_CFLAGS="-march=armv7-a -mfloat-abi=softfp -mfpu=neon"
ANDROID_ARMV8_CFLAGS="-march=armv8-a"
ANDROID_X86_CFLAGS="-march=i686 -mtune=intel -mssse3 -mfpmath=sse -m32"
ANDROID_X86_64_CFLAGS="-march=x86-64 -msse4.2 -mpopcnt -m64 -mtune=intel"


# params($1: arch, $2: arch_abi, $3: host, $4: cross_prefix, $5: cflags)
build_bin() {

    echo "------------------- Start build $2 -------------------------"

    ARCH=$1 # arm arm64 x86 x86_64
    ANDROID_ARCH_ABI=$2 # armeabi armeabi-v7a x86 mips

    PREFIX=$(pwd)/dist/${MY_LIBS_NAME}/${ANDROID_ARCH_ABI}/

    HOST=$3
    SYSROOT=${NDK_PATH}/platforms/android-${ANDROID_VERSION}/arch-${ARCH}

    CFALGS=$5


    TOOLCHAIN=${NDK_PATH}/toolchains/${HOST}-${TOOLCHAIN_VERSION}/prebuilt/${BUILD_PLATFORM}
    CROSS_PREFIX=${TOOLCHAIN}/bin/$4-

    # build 中间件
    mkdir -p ${MY_BUILD_DIR}/${ANDROID_ARCH_ABI}
    BUILD_DIR=./${MY_BUILD_DIR}/${ANDROID_ARCH_ABI}

    echo "pwd==$(pwd)"
    echo "ARCH==${ARCH}"
    echo "PREFIX==${PREFIX}"
    echo "HOST==${HOST}"
    echo "SYSROOT=${SYSROOT}"
    echo "CFALGS=$5"
    echo "CFALGS=${CFALGS}"
    echo "TOOLCHAIN==${TOOLCHAIN}"
    echo "CROSS_PREFIX=${CROSS_PREFIX}"

    mkdir -p ${BUILD_DIR}
    cd ${BUILD_DIR}


    sh ../../configure \
        --prefix=${PREFIX} \
        --target-os=linux \
        --arch=${ARCH} \
        --sysroot=$SYSROOT \
        --enable-cross-compile \
        --cross-prefix=${CROSS_PREFIX} \
        --extra-cflags="$CFALGS -Os -fPIC -DANDROID -Wfatal-errors -Wno-deprecated" \
        --extra-cxxflags="-D__thumb__ -fexceptions -frtti" \
        --extra-ldflags="-L${SYSROOT}/usr/lib" \
        --enable-shared \
        --enable-asm \
        --enable-neon \
        --disable-encoders \
        --enable-encoder=aac \
        --enable-encoder=mjpeg \
        --enable-encoder=png \
        --disable-decoders \
        --enable-decoder=aac \
        --enable-decoder=aac_latm \
        --enable-decoder=h264 \
        --enable-decoder=mpeg4 \
        --enable-decoder=mjpeg \
        --enable-decoder=png \
        --disable-demuxers \
        --enable-demuxer=image2 \
        --enable-demuxer=h264 \
        --enable-demuxer=aac \
        --disable-parsers \
        --enable-parser=aac \
        --enable-parser=ac3 \
        --enable-parser=h264 \
        --enable-gpl \
        --disable-doc \
        --disable-ffmpeg \
        --disable-ffplay \
        --disable-ffprobe \
        --disable-symver \
        --disable-debug \
        --enable-small


    make clean
    make
    make install

    cd ../../

    echo "------------------- $2 Build finish -------------------------"
}

# build for armeabi
#build_bin arm armeabi arm-linux-androideabi arm-linux-androideabi "$ANDROID_ARMV5_CFLAGS"

# build for armeabi-v7a
#build_bin arm armeabi-v7a arm-linux-androideabi arm-linux-androideabi "$ANDROID_ARMV7_CFLAGS"

# build for arm64-v8a
build_bin arm64 arm64-v8a aarch64-linux-android aarch64-linux-android "$ANDROID_ARMV8_CFLAGS"

# build for x86
#build_bin x86 x86 x86 i686-linux-android "$ANDROID_X86_CFLAGS"

# build for x86_64
#build_bin x86_64 x86_64 x86_64 x86_64-linux-android "$ANDROID_X86_64_CFLAGS"

根据需要选择脚本最后的编译命令,直接运行脚本即可自动编译

注意: 由于 JNI 只接受 .so 结尾的库文件,而 FFmpeg 的 configure 中指定了库名以版本号结尾,所以需要修改 configure 中的配置

下面的配置在 FFmpeg-3.4.6 版本中位于第 3416 行

SLIBNAME_WITH_MAJOR='$(SLIBNAME).$(LIBMAJOR)'
LIB_INSTALL_EXTRA_CMD='$$(RANLIB) "$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_VERSION)'
SLIB_INSTALL_LINKS='$(SLIBNAME_WITH_MAJOR) $(SLIBNAME)'

修改为下面的格式

SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)'
LIB_INSTALL_EXTRA_CMD='$$(RANLIB) "$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)'
SLIB_INSTALL_LINKS='$(SLIBNAME)'

注意: 有些版本的 FFmpeg 编译时会出现下列报错

libavcodec/aaccoder.c:函数’search_for_ms’:
libavcodec/aaccoder.c:803:25:错误:预期的标识符或’(‘数字常量之前
libavcodec/hevc_mvs.c:函数’derive_spatial_merge_candidates’
libavcodec/hevc_mvs.c:368:23:错误:’y0000000’未声明(首次在此函数中使用

修改 path_to_ffmpeg_src/libavcodec/ 目录下的 aaccooder.c hevc_mvs.c 两个文件

将文件中 B0 变量修改为其他字符,例如 BB

编译成功后目录结构

ffmpeg-3.4.6
├─ binary   # 编译产生的中间件
├─ build.sh # 编译脚本
├─ Changelog
├─ compat
├─ configure
├─ CONTRIBUTING.md
├─ COPYING.GPLv2
├─ COPYING.GPLv3
├─ COPYING.LGPLv2.1
├─ COPYING.LGPLv3
├─ CREDITS
├─ dist     # 编译输出的库和头文件目录
│   └─ ffmpeg-3.4.6 # 该文件夹名由 MY_LIBS_NAME 指定
│       └─ arm64-v8a    # 与编译的目标平台 ABI 名称相同
│           ├─ bin
│           ├─ include  # 头文件目录
│           │   ├─ libavcodec
│           │   ├─ libavdevice
│           │   ├─ libavfilter
│           │   ├─ libavformat
│           │   ├─ libavutil
│           │   ├─ libpostproc
│           │   ├─ libswresample
│           │   └─ libswscale
│           ├─ lib      # 库目录,包含动态库和静态库
│           │   ├─ libavcodec-57.so
│           │   ├─ libavcodec.a
│           │   ├─ libavcodec.so -> libavcodec-57.so
│           │   ├─ libavdevice-57.so
│           │   ├─ libavdevice.a
│           │   ├─ libavdevice.so -> libavdevice-57.so
│           │   ├─ libavfilter-6.so
│           │   ├─ libavfilter.a
│           │   ├─ libavfilter.so -> libavfilter-6.so
│           │   ├─ libavformat-57.so
│           │   ├─ libavformat.a
│           │   ├─ libavformat.so -> libavformat-57.so
│           │   ├─ libavutil-55.so
│           │   ├─ libavutil.a
│           │   ├─ libavutil.so -> libavutil-55.so
│           │   ├─ libpostproc-54.so
│           │   ├─ libpostproc.a
│           │   ├─ libpostproc.so -> libpostproc-54.so
│           │   ├─ libswresample-2.so
│           │   ├─ libswresample.a
│           │   ├─ libswresample.so -> libswresample-2.so
│           │   ├─ libswscale-4.so
│           │   ├─ libswscale.a
│           │   ├─ libswscale.so -> libswscale-4.so
│           │   └─ pkgconfig
│           └─ share
├─ doc
├─ ffbuild
├─ fftools
├─ INSTALL.md
├─ libavcodec
├─ libavdevice
├─ libavfilter
├─ libavformat
├─ libavresample
├─ libavutil
├─ libpostproc
├─ libswresample
├─ libswscale
├─ LICENSE.md
├─ MAINTAINERS
├─ Makefile
├─ presets
├─ README.md
├─ RELEASE
├─ RELEASE_NOTES
├─ tests
├─ tools
└─ VERSION

将运行库导入到项目中

目录结构

ffmpegtest
├─ app
│  ├─ build
│  ├─ libs
│  └─ src
│      ├─ androidTest
│      ├─ main
│      │  ├─ java
│      │  │  └─ com
│      │  │      └─ example
│      │  │          └─ ffmpegtest
│      │  │              └─ MainActivity.java
│      │  ├─ jni    # C/C++ 源码目录
│      │  │  └─ include     # 需要导入的头文件
│      │  │      ├─ libavcodec
│      │  │      ├─ libavdevice
│      │  │      ├─ libavfilter
│      │  │      ├─ libavformat
│      │  │      ├─ libavutil
│      │  │      ├─ libpostproc
│      │  │      ├─ libswresample
│      │  │      └─ libswscale
│      │  ├─ jniLibs    # JNI 需要调用的运行库
│      │  │  └─ arm64-v8a   # 对应 ABI 版本建立文件夹
│      │  │      ├─ libavcodec-57.so
│      │  │      ├─ libavdevice-57.so
│      │  │      ├─ libavfilter-6.so
│      │  │      ├─ libavformat-57.so
│      │  │      ├─ libavutil-55.so
│      │  │      ├─ libpostproc-54.so
│      │  │      ├─ libswresample-2.so
│      │  │      ├─ libswscale-4.so
│      │  │      └─ libswscale-4.so
│      │  └─ res
│      └─ test
└─ gradle

CMakeLists.txt 添加以下配置

include_directories(${PROJECT_SOURCE_DIR}/src/main/jni/include)

add_library(ffmpegTest
        SHARED
        src/main/jni/ffmpegTest.cpp )

add_library(avcodec-57 SHARED IMPORTED)
set_target_properties(avcodec-57
        PROPERTIES
        IMPORTED_LOCATION ${PROJECT_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libavcodec-57.so )

add_library(avfilter-6 SHARED IMPORTED)
set_target_properties(avfilter-6
        PROPERTIES
        IMPORTED_LOCATION ${PROJECT_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libavfilter-6.so )

add_library(avformat-57 SHARED IMPORTED)
set_target_properties(avformat-57
        PROPERTIES
        IMPORTED_LOCATION ${PROJECT_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libavformat-57.so )

add_library(avutil-55 SHARED IMPORTED)
set_target_properties(avutil-55
        PROPERTIES
        IMPORTED_LOCATION ${PROJECT_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libavutil-55.so )

add_library(postproc-54 SHARED IMPORTED)
set_target_properties(postproc-54
        PROPERTIES
        IMPORTED_LOCATION ${PROJECT_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libpostproc-54.so )

add_library(avdevice-57 SHARED IMPORTED)
set_target_properties(avdevice-57
        PROPERTIES
        IMPORTED_LOCATION ${PROJECT_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libavdevice-57.so )

add_library(swscale-4 SHARED IMPORTED)
set_target_properties(swscale-4
        PROPERTIES
        IMPORTED_LOCATION ${PROJECT_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libswscale-4.so )

add_library(swresample-2 SHARED IMPORTED)
set_target_properties(swresample-2
        PROPERTIES
        IMPORTED_LOCATION ${PROJECT_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libswresample-2.so )

target_link_libraries(ffmpegTest
        ${log-lib}
        avcodec-57 avfilter-6 avformat-57 avutil-55 postproc-54 avdevice-57 swscale-4 swresample-2)

提取视频帧并保存为图片

#include <jni.h>
#include <android/log.h>

extern "C" {
#include <libavformat/avformat.h>
}

#define DEBUG

#ifdef DEBUG
#define LOG    "ffmpegLOG"
#define LOGD(...)  __android_log_print(ANDROID_LOG_DEBUG, LOG, __VA_ARGS__)
#define LOGI(...)  __android_log_print(ANDROID_LOG_INFO, LOG, __VA_ARGS__)
#define LOGW(...)  __android_log_print(ANDROID_LOG_WARN, LOG, __VA_ARGS__)
#define LOGE(...)  __android_log_print(ANDROID_LOG_ERROR, LOG, __VA_ARGS__)
#define LOGF(...)  __android_log_print(ANDROID_LOG_FATAL, LOG, __VA_ARGS__)
#else
#define LOG
#define LOGD(...)
#define LOGI(...)
#define LOGW(...)
#define LOGE(...)
#define LOGF(...)
#endif

int writeJPEG(AVFrame* frame, int width, int height, char* output_ath, int image_index);

extern "C"
JNIEXPORT jint JNICALL
Java_com_example_ffmpegtest_MainActivity_videoFrame(JNIEnv *env, jobject instance,
                                                    jstring filePath_, jstring outputPath_) {
    const char *filePath = env->GetStringUTFChars(filePath_);
    const char *outputPath = env->GetStringUTFChars(outputPath_);

    LOGE("======================= ffmpeg start =======================");

    clock_t time_start, time_finish;
    double total_time;
    time_start = clock();

    // 注册所有模块
    av_register_all();

    AVFormatContext *formatContext = nullptr;
    int ret = 0;

    LOGD("Video path: [%s]", filePath);
    // 打开媒体
    ret = avformat_open_input(&formatContext, filePath, nullptr, nullptr);
    if (ret < 0) {
        LOGE("Cannot open file, error code: [%d]", ret);
        return -1;
    }

    // 获取媒体信息
    ret = avformat_find_stream_info(formatContext, nullptr);
    if (ret < 0) {
        LOGE("Cannot find stream, error code: [%d]", ret);
        return -1;
    }

    int video_index = -1;
    // 遍历媒体流
    for (int i = 0; i < formatContext->nb_streams; i++) {
        if (formatContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
            video_index = i;
            break;
        }
    }

    if (video_index == -1) {
        LOGE("Cannot find video stream");
        return -1;
    }

    // 找出一个有效码流的 AVCodecID,根据标准寻找对应的解码器
    AVCodecContext *codecContext = formatContext->streams[video_index]->codec;
    enum AVCodecID codecId = codecContext->codec_id;
    AVCodec *codec = avcodec_find_decoder(codecId);
    if(!codec){
        LOGE("Cannot find decoder");
        return -1;
    }

    // 初始化解码器
    ret = avcodec_open2(codecContext, codec, nullptr);
    if (ret < 0) {
        LOGE("Cannot open decoder, error code: [%d]", ret);
        return -1;
    }

    // 分配内存
    AVPacket *packet = av_packet_alloc();
    AVFrame *frame = av_frame_alloc();
    int image_index = 0;

    // 当剩余帧数大于 0 时
    while (av_read_frame(formatContext, packet) >= 0) {
        if (packet && packet->stream_index == video_index) {
            int gotFrame = 0;
            // 将 AVPacket 中的数据解码为原始数据(YUV、RGB 以及 PCM),存储在 AVFrame 上
            avcodec_decode_video2(codecContext, frame, &gotFrame, packet);
            if (gotFrame) {
                image_index++;
                // 将视频帧保存在本地
                ret = writeJPEG(frame, codecContext->width, codecContext->height, (char*)outputPath, image_index);
                if(ret == 0){
                    LOGI("Save frame in %s and rename to video_frame_%d.jpg", outputPath, image_index);
                }
            }
        }
    }
    time_finish = clock();
    total_time = (double)(time_finish - time_start) / CLOCKS_PER_SEC;
    LOGE("Total time: [%f]s --- ffmpeg", total_time);
    av_packet_unref(packet);
    if (frame) {
        av_frame_free(&frame);
    }
    avcodec_close(codecContext);
    avformat_free_context(formatContext);
    LOGE("======================= ffmpeg finish =======================");

    env->ReleaseStringUTFChars(filePath_, filePath);
    env->ReleaseStringUTFChars(outputPath_, outputPath);

    return 0;
}

int writeJPEG(AVFrame *frame, int width, int height, char *output_path, int image_index) {
    char out_file[1024];
    sprintf(out_file, "%s/video_frame_%d.jpg", output_path, image_index);
    // 分配内存空间
    AVFormatContext *formatContext = avformat_alloc_context();
    // 初始化 AVFormatContext 结构体
    avformat_alloc_output_context2(&formatContext, nullptr, "singlejpeg", out_file);

    // 打开(创建?)要写入的文件
    if (avio_open2(&formatContext->pb, out_file, AVIO_FLAG_READ_WRITE, nullptr, nullptr) < 0) {
        LOGE("Open file failed---write JPEG");
        return -1;
    }

    // 创建流通道,例如 Video - H.264, Audio - AAC
    AVStream *stream = avformat_new_stream(formatContext, nullptr);
    if (&stream == nullptr) {
        LOGE("Create stream failed---write JPEG");
        return -1;
    }

    AVCodecContext *codecContext = stream->codec;

    // 保存文件头信息(帧信息)
    codecContext->codec_id = formatContext->oformat->video_codec;
    codecContext->codec_type = AVMEDIA_TYPE_VIDEO;
    codecContext->pix_fmt = AV_PIX_FMT_YUVJ420P;
    codecContext->height = height;
    codecContext->width = width;
    codecContext->time_base.num = 1;
    codecContext->time_base.den = 25;

    // 寻找解码器
    AVCodec *codec = avcodec_find_encoder(codecContext->codec_id);
    if (!codec) {
        LOGE("Cannot find encoder---write JPEG");
        return -1;
    }

    // 初始化解码器
    if (avcodec_open2(codecContext, codec, nullptr) < 0) {
        LOGE("Cannot open encoder---write JPEG");
        return -1;
    }
    // 将文件头保存到 codecpar 中
    avcodec_parameters_from_context(stream->codecpar, codecContext);

    // 写入头数据
    avformat_write_header(formatContext, nullptr);
    int size = codecContext->width * codecContext->height;

    // 创建并初始化 AVPacket 内存空间
    AVPacket *packet = av_packet_alloc();
    av_new_packet(packet, size * 3);

    int got_image = 0;
    // 调用编码器,编码为指定格式
    int result = avcodec_encode_video2(codecContext, packet, frame, &got_image);
    if (result < 0) {
        LOGE("Encode failed---write JPEG");
        return -1;
    }
    if (got_image == 1) {
        // 输出一帧数据
        av_write_frame(formatContext, packet);
    }
    // 释放包内存
    av_packet_unref(packet);
    // 写文件尾
    av_write_trailer(formatContext);

    // 关闭文件
    avio_close(formatContext->pb);
    // 释放解码器
    avcodec_close(codecContext);
    avformat_free_context(formatContext);

    return 0;
}

在 Activity 中调用

public class MainActivity extends AppCompatActivity {
    // 导入运行库
    static {
        System.loadLibrary("ffmpegTest");
    }
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 获取存储设备路径
        String storagePath = Environment.getExternalStorageDirectory().getPath();
        File videoPath = new File(storagePath + "/Download/testVideo.mp4");
        videoFrame(videoPath.toString(), storagePath + "/Download/video_frames/");
    }
}

// 实例化运行库中的方法
public native int videoFrame(String filePath, String outputPath);
LICENSED UNDER CC BY-NC-SA 4.0
Comment