編譯 Android 下可執行命令的 FFmpeg

使用 FFmpeg 生成視頻封面圖時,其實能夠直接使用 FFmpeg 相關命令截取一幀的圖像數據保存到本地,而後加載到 ImageView 上。android


有時候使用命令確實比寫代碼更加簡單和令人輕鬆一點。git


因此這一篇是講解如何導入 FFmpeg 相關源碼 而後如何執行命令行工具的博客,可是其實這只是個 Demo 而已,由於有不少細節須要處理,推薦直接使用開源庫。github

導入源碼

從FFmpeg源碼中導入 cmdutils.c、cmdutils.h、config.h、ffmpeg.c、ffmpeg.h、web

ffmpeg_filter.c、ffmpeg_hw.c、ffmpeg_opt.c 這幾個源碼。shell


通常存放在 fftools 目錄下。config.h 若是編譯生成目錄下沒有,就能夠直接使用ffmpeg 根目錄下的 config.h。api

編寫 CmakeList

# 設置構建本機庫文件所需的 CMake的最小版本
cmake_minimum_required(VERSION 3.4.1)

#添加頭文件的搜索路徑
include_directories(src/main/cpp/include)

#設置查找動態庫位置
set(LINK_DIR ${CMAKE_SOURCE_DIR}/libs/${CMAKE_ANDROID_ARCH_ABI})
link_directories(${LINK_DIR})
#找到全部的so庫,存放在全局變量SO_DIR中
file(GLOB SO_DIR ${LINK_DIR}/*.so)

#找到全部的源文件,存放在全局變量中
#file(GLOB FFMPEG_DIR src/main/cpp/ffmpeg/*.c)
#message("FFMPEG_DIR == ${FFMPEG_DIR}")

file(GLOB CPP_DIR src/main/cpp/*.cpp)
file(GLOB FFMPEG_DIR src/main/cpp/include/*.c)

# 添加本身寫的 C/C++源文件
add_library(utils #so名稱
        SHARED #動態庫
        ${CPP_DIR}
        ${FFMPEG_DIR}
        )

#  依賴 NDK中自帶的log庫
find_library(log-lib log)

#  連接庫
target_link_libraries(
        utils
        ${SO_DIR}
        jnigraphics
        ${log-lib})


我是將 ffmpeg 的源碼和以前生成的 ffmpeg 頭文件都放在了 cpp/include 目錄下。bash


這樣在 CmakeList 中使用 include_directories 就能夠直接找到全部的頭文件,而後將 ffmpeg 的源碼和本身寫的工具類源碼關聯起來就好了。微信

修改 FFmpeg 的源碼

修改ffmpeg.c的main方法名稱爲exe_cmd,並在ffmpeg.h頭文件加上一樣名稱的方法聲明。app


//ffmpeg.c
int exe_cmd(int argc, char **argv) {
  ...
}
//ffmpeg.h
int exe_cmd(int argc, char **argv);


原生命令行工具在執行完 FFmpeg 命令後都會退出程序,可是在 Android 裏面可不能這樣,因此咱們要修改 FFmpeg 結束程序的函數。
編輯器


修改 cmdutils.c 和 cmdutils.h,註釋掉退出程序的代碼,而且增長一個int的返回值。


//cmdutils.c
int exit_program(int ret)
{
//    if (program_exit)
//        program_exit(ret);

//    exit(ret);
    return ret;
}

//cmdutils.h
int exit_program(int ret);


而且在 Android 裏面咱們確定是執行完一條命令,接着還會繼續執行其餘命令,因此咱們須要從新初始化一些關鍵變量的值。


找到 ffmpeg.c 中的 ffmpeg_cleanup 函數,在末尾將一些關鍵變量從新初始化。 


//ffmpeg.c
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;
}


最後在 main 函數末尾調用 ffmpeg_cleanup 函數。


int exe_cmd(int argc, char **argv) {
  ...
    //    exit_program(received_nb_signals ? 255 : main_return_code);
    ffmpeg_cleanup(0);
}

增長 FFmpeg 日誌輸出

在 ffmpeg.c 中找到 log_callback_null 的函數,添加以下代碼,原代碼塊是空實現。


#include "android/log.h"
#define logDebug(...) __android_log_print(ANDROID_LOG_DEBUG,"MainActivity",__VA_ARGS__)

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

    logDebug("ffmpeg log ----- %s", line);
}

在 main 函數中調用 log_callback_null 函數。


int exe_cmd(int argc, char **argv) {
    av_log_set_callback(log_callback_null);
       int i, ret;
  ...
}

編寫工具類方法

在 MainActivity中 增長 exeCmd(String[] cmd) 方法。


public static native int exeCmd(String[] cmd);

在 ffmpeg_utils.cpp 增長 jni 方法。


JNIEXPORT jint JNICALL
Java_demo_simple_example_1ffmpeg_MainActivity_exeCmd(JNIEnv *env, jclass clazz, jobjectArray cmd) 
{
    int argc = env->GetArrayLength(cmd);
    logDebug("argc == %d", argc);
    char *argv[argc];

    for (int i = 0; i < argc; ++i) {
        jstring str = (jstring) env->GetObjectArrayElement(cmd, i);
        argv[i] = (char *) env->GetStringUTFChars(str, JNI_FALSE);
        logDebug("%s ", argv[i]);
    }

    return exe_cmd(argc, argv);
//    return 1;
}


執行命令。


private void exeCmd() {
        String path = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator
                + "get_cover1.mp4";
        String outPath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator
                + "video.flv";
        File outFile = new File(outPath);
        if (outFile.exists()) {
            outFile.delete();
        }
        //裁剪個1s視頻
        String cmd = "ffmpeg -ss 00:00:00 -t 00:00:10 -i " + path + " -vcodec copy -acodec copy " + outPath;
        String[] cmdArr = cmd.split(" ");
        int result = exeCmd(cmdArr);
        Log.d(TAG, "exe cmd result == " + result);
    }


查看日誌輸出


demo.simple.example_ffmpeg D/MainActivity: ffmpeg log ----- Output file #0 (/storage/emulated/0/video.flv):
demo.simple.example_ffmpeg D/MainActivity: exe cmd result == 0


執行命令的返回值 ==0,而且也看到確實文件已經生成出來了,咱們 adb pull 把文件導出到桌面用 ffprobe 或 ffplay 看看。


ffprobe video.flv

major_brand : mp42

Duration: 00:00:01.07, start: 0.033000, bitrate: 3130 kb/s

Stream #0:0: Video: h264 (Main), yuv420p(tv, bt709, progressive), 1080x1920, 3390 kb/s, 30 fps, 30 tbr, 1k tbn, 60 tbc

Stream #0:1: Audio: aac (LC), 48000 Hz, stereo, fltp, 317 kb/s

能夠看到確實裁剪生成了一個1秒的視頻,雖而後綴名咱們用的 .flv,可是其實咱們是拷貝的視頻編碼,因此仍是mp4的封裝格式。


源碼:

https://github.com/simplepeng/AndroidExamples/tree/master/example_ffmpeg

使用已有的輪子

上面的例子並非一個完善的工具類,好比缺乏 Native 層的線程支持,出現錯誤就會直接閃退,缺乏進度回調等,因此仍是直接使用現成的輪子比較靠譜,只是咱們須要知道輪子大概是怎麼造出來的就好了。


這裏我推薦使用 mobile-ffmpeg 這個開源庫,1.8k 的 star 足以證實其品質還行,直接導入編譯好的aar就能夠執行命令行工具鏈,並且能夠自行編譯連接不少有用的第三方library,好比 x26四、libwebp 等。


動手能力強或有特殊需求的可使用 android.sh 自行編譯出 FFmpeg 頭文件和動態庫,以及 Android 工具鏈的 aar。


好比說我如今只須要一個支持 arm64-v8a 和 api16 及以上的動態庫,那麼我就本身新建了一個shell腳本文件:


#!/bin/bash

export ANDROID_HOME="/Users/chenpeng/Library/Android/sdk/"
export ANDROID_NDK_ROOT="/Users/chenpeng/Desktop/work_space/ndk/android-ndk-r21b/"

build() {
    ./android.sh \
        --lts \
        --disable-arm-v7a \
        --disable-arm-v7a-neon \
        --disable-x86 \
        --disable-x86-64
}

build


執行完這個 shell 後,就會在 prebuilt 目錄下生產對應的頭文件,動態庫,以及 aar 文件,直接拿來用就能夠了。


來源:https://juejin.im/post/6850418116602642440
做者:simplepeng



-- END --


進技術交流羣,掃碼添加個人微信:Byte-Flo



獲取視頻教程和源碼



推薦:

字節流動 OpenGL ES 技術交流羣來啦

FFmpeg + OpenGL ES 實現 3D 全景播放器

FFmpeg + OpenGLES 實現視頻解碼播放和視頻濾鏡

一文掌握 YUV 圖像的基本處理

Android OpenGL ES 從入門到精通系統性學習教程

OpenGL ES 實現動態(水波紋)漣漪效果


以爲不錯,點個在看唄~

本文分享自微信公衆號 - 字節流動(google_developer)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索