1. 程式人生 > >編譯FFmpeg4.0.1並移植到Android app中使用(最詳細的FFmpeg-Android編譯教程)

編譯FFmpeg4.0.1並移植到Android app中使用(最詳細的FFmpeg-Android編譯教程)

1.搭建編譯環境

1.安裝ubuntu14.04,安裝完成後執行以下命令

apt-get update
apt-get install yasm
apt-get install pkg-config

2.下載ndk

我用的是ndk r14b,附上下載地址:https://dl.google.com/android/repository/android-ndk-r14b-linux-x86_64.zip
將ndk下載到 /home/ndk/目錄下,下載完成後執行tar -zxvf android-ndk-r14b-linux-x86_64.zip解壓

3.下載FFmpeg4.0.1

下載地址:

https://codeload.github.com/FFmpeg/FFmpeg/tar.gz/n4.0.1
下載完成後執行tar -zxvf n4.0.1解壓

2.編譯FFmpeg

1.修改configure

進入原始碼根目錄,用vim開啟configure,找到

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)'  

2.配置編譯指令碼

在原始碼根目錄新建build.sh,內容如下:

#!/bin/bash
NDK=/home/ndk/android-ndk-r14b
SYSROOT=$NDK/platforms/android-21/arch-arm/
CPU=armv7-a
TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64
PREFIX=$(pwd)/android/$CPU
OPTIMIZE_CFLAGS="-mfloat-abi=softfp -mfpu=vfp -marm -march=$CPU "

function build_android
{
./configure \
--prefix=$PREFIX \
--enable-neon \
--enable-hwaccels \
--enable-shared \
--enable-jni \
--enable-mediacodec \
--enable-decoder=h264_mediacodec \
--disable-static \
--disable-doc \
--enable-ffmpeg \
--disable-ffplay \
--disable-ffprobe \
--enable-avdevice \
--disable-doc \
--disable-symver \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
--target-os=android \
--arch=arm \
--cpu=armv7-a \
--enable-cross-compile \
--sysroot=$SYSROOT \
--extra-cflags="-Os -fpic $OPTIMIZE_CFLAGS" \
--extra-ldflags="$ADDI_LDFLAGS" \
$ADDITIONAL_CONFIGURE_FLAG
}
build_android

賦予指令碼執行許可權:chmod +x build.sh
執行指令碼:./build.sh
這裡寫圖片描述

編譯:make -j4(-j<改成你的CPU核心數量>)
這裡寫圖片描述

生成動態連結庫:make install
如果一切順利就可以在原始碼更目錄下的android/armv7-a下找到我們需要的.so檔案了
這裡寫圖片描述

3.移植到Android App中

1.JNI編譯

(1)將編譯後的FFmpeg原始碼目錄打包並複製到Windows中,在任意目錄下新建jni資料夾,在解壓後的 …\FFmpeg-n4.0.1\android\armv7-a\lib目錄下有7個.so檔案,將所有的.so檔案放到 …\jni\prebuilt\目錄下;
將 …\FFmpeg-n4.0.1\android\armv7-a\include目錄下的所有資料夾複製到 …\jni\目錄下;
將 …\FFmpeg-n4.0.1\fftools\目錄下的以下檔案複製到 …\jni\目錄下
cmdutils.h
ffmpeg.h
ffmpeg.c
ffmpeg_opt.c
config.h
ffmpeg_filter.c
cmdutils.c
ffmpeg_hw.c

(2)修改cmdutils
開啟cmdutils.h,將
void show_help_children(const AVClass *class, int flags);
改為
void show_help_children(const AVClass *clazz, int flags);
否則和C++一起編譯會出問題

開啟cmdutils.c
void exit_program(int ret)中的退出函式註釋掉,否則命令執行完會導致APP退出:

void exit_program(int ret)
{
    //if (program_exit)
    //    program_exit(ret);
    //exit(ret);
}

(3)修改ffmpeg.c
找到入口函式int main(int argc, char **argv)
將其修改為int ffmpeg_exec(int argc, char **argv)
並將該函式末尾此行程式碼註釋掉:

//exit_program(received_nb_signals ? 255 : main_return_code);

同時在ffmpeg.h中新增函式申明:
int ffmpeg_exec(int argc, char **argv);

在函式static void ffmpeg_cleanup(int ret)末尾加上以下程式碼:

nb_filtergraphs = 0;
nb_output_files = 0;
nb_output_streams = 0;
nb_input_files = 0;
nb_input_streams = 0;

(4)輸出ADB日誌
在ffmpeg.c中引入標頭檔案

#include "android/log.h"

#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG  , "ffmpeg.c", __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR  , "ffmpeg.c", __VA_ARGS__)

實現log_callback_null函式

static void log_callback_null(void *ptr, int level, const char *fmt, va_list vl)
{
    static int print_prefix = 1;
    static int count;
    static char prev[1024];
    char line[1024];
    static int is_atty;

    av_log_format_line(ptr, level, fmt, vl, line, sizeof(line), &print_prefix);

    strcpy(prev, line);
    //sanitize((uint8_t *)line);

    if (level <= AV_LOG_WARNING) {
        LOGE("%s", line);
    } else {
        LOGD("%s", line);
    }
}

(5)實現JNI介面
編寫ffmpeg-invoke.cpp並放到 …\jni\目錄下:

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

extern "C"{
#include "ffmpeg.h"
}

#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG  , "ffmpeg-invoke", __VA_ARGS__)


extern "C"
JNIEXPORT jstring JNICALL
Java_com_gamepp_ffmpegtest_jni_FFmpegInvoke_test(JNIEnv *env, jclass type) {

    std::string retValue = "FFmpeg invoke test";
    return env->NewStringUTF(retValue.c_str());
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_gamepp_ffmpegtest_jni_FFmpegInvoke_run(JNIEnv *env, jclass type, jint cmdLen,
                                                       jobjectArray cmd) {

    char *argCmd[cmdLen] ;
    jstring buf[cmdLen];
    LOGD("length=%d",cmdLen);

    for (int i = 0; i < cmdLen; ++i) {
        buf[i] = static_cast<jstring>(env->GetObjectArrayElement(cmd, i));
        char *string = const_cast<char *>(env->GetStringUTFChars(buf[i], JNI_FALSE));
        argCmd[i] = string;
        LOGD("argCmd=%s",argCmd[i]);
    }

    ffmpeg_exec(cmdLen, argCmd);
  
    return 0;

}

注意:將com_gamepp_ffmpegtest_jni_FFmpegInvoke改為自己工程中的JAVA檔案對應的包名和路徑
(6)編寫.mk檔案
編寫Android.mk檔案並放到 …\jni\目錄

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE :=  libavutil
LOCAL_SRC_FILES := prebuilt/libavutil.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE :=  libswresample
LOCAL_SRC_FILES := prebuilt/libswresample.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE :=  libswscale
LOCAL_SRC_FILES := prebuilt/libswscale.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := libavcodec
LOCAL_SRC_FILES := prebuilt/libavcodec.so
include $(PREBUILT_SHARED_LIBRARY)


include $(CLEAR_VARS)
LOCAL_MODULE := libavformat
LOCAL_SRC_FILES := prebuilt/libavformat.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := libavfilter
LOCAL_SRC_FILES := prebuilt/libavfilter.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := libavdevice
LOCAL_SRC_FILES := prebuilt/libavdevice.so
include $(PREBUILT_SHARED_LIBRARY)


include $(CLEAR_VARS)
LOCAL_MODULE := ffmpeg-invoke


LOCAL_SRC_FILES :=ffmpeg-invoke.cpp \
                 cmdutils.c \
                 ffmpeg_filter.c \
                 ffmpeg_opt.c \
                 ffmpeg_hw.c \
                 ffmpeg.c


LOCAL_C_INCLUDES := D:\libs\FFmpeg-n4.0.1

LOCAL_LDLIBS := -llog -ljnigraphics -lz -landroid -lm -pthread -L$(SYSROOT)/usr/lib
LOCAL_SHARED_LIBRARIES := libavcodec libavfilter libavformat libavutil libswresample libswscale libavdevice

include $(BUILD_SHARED_LIBRARY)

注意:將LOCAL_C_INCLUDES改為自己電腦中FFmpeg原始碼所在目錄

編寫Application.mk並放到 …\jni\目錄下

APP_ABI := armeabi-v7a
APP_PLATFORM=android-21
APP_OPTIM := release
APP_STL := stlport_static

(7)編譯
確認所需檔案都準備就緒:
這裡寫圖片描述
開啟CMD,進入該目錄,執行ndk-build(確保ndk路徑已經配置到環境變數中)
編譯成功:
這裡寫圖片描述
編譯成功後可以再jni同級目錄下找到libs目錄,libs\armeabi-v7a目錄下的.so檔案就是我們最終需要的動態連結庫:
這裡寫圖片描述

2.新建Android Studio測試工程 FFmpegTest

(1)在FFmpegTest\app\src\main\目錄下新建jniLibs目錄,將上一步編譯生成的 \libs\armeabi-v7a資料夾複製到jniLibs目錄下;
在com.gamepp.ffmpegtest.jni路徑下新建FFmpegInvoke.java

public class FFmpegInvoke {
    static {
        System.loadLibrary("avutil");
        System.loadLibrary("avcodec");
        System.loadLibrary("swresample");
        System.loadLibrary("avformat");
        System.loadLibrary("swscale");
        System.loadLibrary("avfilter");
        System.loadLibrary("avdevice");
        System.loadLibrary("ffmpeg-invoke");
    }

    private static native int run(int cmdLen, String[] cmd);
    public static native String test();

    public static int run(String[] cmd){
        return run(cmd.length,cmd);
    }
}

這裡寫圖片描述

(2)呼叫測試
測試是否能夠成功呼叫動態連結庫函式

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TextView tvMessage = findViewById(R.id.tv_message);
        tvMessage.setText(FFmpegInvoke.test());
    }
}

這裡寫圖片描述
正是ffmpeg-invoke.cpp中Java_com_gamepp_ffmpegtest_jni_FFmpegInvoke_test(JNIEnv *env, jclass type)函式返回的字串,說明java呼叫C++函式成功。

(3)測試FFmpeg命令

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
                    100);
        }

        TextView tvMessage = findViewById(R.id.tv_message);
        tvMessage.setText(FFmpegInvoke.test());

        ffmpegTest();
    }

    private void ffmpegTest() {
        new Thread(){
            @Override
            public void run() {
                long startTime = System.currentTimeMillis();
                String input = "/sdcard/Movies/Replay_2018.05.08-13.46.mp4";
                String output = "/sdcard/Movies/output.mp4";
                //剪下視訊從00:20-00:28的片段
                String cmd = "ffmpeg -d -ss 00:00:20 -t 00:00:08 -i %s -vcodec copy -acodec copy %s";
                cmd = String.format(cmd,input,output);
                FFmpegInvoke.run(cmd.split(" "));
                Log.d("FFmpegTest", "run: 耗時:"+(System.currentTimeMillis()-startTime));
            }
        }.start();


    }
}

執行完畢後再/sdcard/Movies/目錄下找到output.mp4,開啟播放確實是我們想要的結果,說明FFmpeg的命令成功執行了。