安卓使用ffmpeg給視訊新增字幕

語言: CN / TW / HK

本文已參與「新人創作禮」活動,一起開啟掘金創作之路。

包含新增字幕能力的ffmpeg和相關so編譯

  1. 需要下載的原始碼

    https://github.com/tanersener/mobile-ffmpeg https://github.com/tanersener/FFmpeg https://github.com/tanersener/x264

    考慮原始碼較大,github不好下載成功,可以從gitee映象倉庫下載 https://gitee.com/catface7/tanersener-mobile-ffmpeg https://gitee.com/catface7/tanersener-FFmpeg https://gitee.com/catface7/tanersener-x264

    FFmpeg原始碼下載後解壓到mpbile-ffmpeg/src/ffmpeg目錄下 x264原始碼下載後解壓到mobile-ffmpeg/src/x264目錄下

  2. 編譯

    • 配置sdk、ndk環境

      export ANDROID_HOME=/opt/sdk export ANDROID_NDK_ROOT=/opt/ndk/android-ndk-r21b-linux-x86_64/android-ndk-r21b

    • 執行編譯命令./android.sh

      此時編譯出的so不支援x264編碼,表現為不支援preset、crf等指令引數;不支援字幕新增,表現為不支援subtitles、FontName等指令引數

    • 執行編譯命令./android.sh --enable-gpl --enable-x264 --enable-fontconfig --enable-libass

      編譯後的so支援x264編碼和字幕新增能力,可排除指定平臺,如下: ./android.sh --enable-gpl --disable-arm-v7a-neon --disable-x86 --disable-x86-64 --enable-x264 --enable-fontconfig --enable-libass

  3. 失敗記錄

    aclocal-1.16: 未找到命令

    詳細錯誤

    命令列錯誤日誌:
    Building mobile-ffmpeg library for Android
    
    Architectures: arm-v7a
    Libraries: android-zlib, cpu-features, fontconfig, freetype, fribidi, libass, libiconv, x264
    
    Building arm-v7a platform on API level 24
    
    fribidi: failed
    
    build.log錯誤日誌:
    CDPATH="${ZSH_VERSION+.}:" && cd . && /bin/bash /opt/pj4as/tanersener-mobile-ffmpeg/src/fribidi/missing aclocal-1.16 -I m4
    /opt/pj4as/tanersener-mobile-ffmpeg/src/fribidi/missing: 行 81: aclocal-1.16: 未找到命令
    WARNING: 'aclocal-1.16' is missing on your system.
             You should only need it if you modified 'acinclude.m4' or
             'configure.ac' or m4 files included by 'configure.ac'.
             The 'aclocal' program is part of the GNU Automake package:
             <https://www.gnu.org/software/automake>
             It also requires GNU Autoconf, GNU m4 and Perl in order to run:
             <https://www.gnu.org/software/autoconf>
             <https://www.gnu.org/software/m4/>
             <https://www.perl.org/>
    Makefile:442: recipe for target 'aclocal.m4' failed
    make: *** [aclocal.m4] Error 127
    

    解決辦法-安裝automake

    進入root許可權
    sudo passwd root
    su
    
    下載地址http://ftp.gnu.org/gnu/automake/
    
    分別執行命令:
    ./configure
    make
    make install
    

新增字幕等能力封裝api

參考https://github.com/itCatface/catface_app或者https://gitee.com/catface7/catface_app專案的 app_ffmpeg_demo模組,以下為使用示例

  • 音訊檔案裁剪

    ```java // 方法說明 /* * 裁剪音訊檔案 * * @param filepath 檔案路徑 * @param saveFilepath 裁剪後文件儲存路徑 * @param coverSaveFilepath 是否覆蓋已存在檔案(預設不覆蓋) * @param startTime 擷取開始時間戳-00:00:30 * @param duration 擷取時長-00:03:00 * @param callback 結果回撥 / public void runCutAudio(String filepath, String saveFilepath, boolean coverSaveFilepath, String startTime, String duration, ExecuteCallback callback)

    // 呼叫示例 FFmpegUtils.getInstance().runCutAudio("/sdcard/wav.wav", "/sdcard/dest_cut_audio_" + System.currentTimeMillis() + ".wav", "00:00:02", "00:00:03", new ExecuteCallback() { @Override public void apply(long executionId, int returnCode) { Log.i(TAG, "apply: cut audio finish:" + returnCode); } }); ```

  • 視訊檔案裁剪

    ```java // 方法說明 /* * 裁剪視訊檔案 * * @param filepath 檔案路徑 * @param saveFilepath 裁剪後文件儲存路徑 * @param coverSaveFilepath 是否覆蓋已存在檔案(預設不覆蓋) * @param startTime 擷取開始時間戳-00:00:30 * @param duration 擷取時長-00:03:00 * @param callback 結果回撥 / public void runCutVideo(String filepath, String saveFilepath, boolean coverSaveFilepath, String startTime, String duration, ExecuteCallback callback)

    // 呼叫示例 FFmpegUtils.getInstance().runCutVideo("/sdcard/5m.mp4", "/sdcard/dest_cut_video_" + System.currentTimeMillis() + ".mp4", "00:00:01", "00:00:03", new ExecuteCallback() { @Override public void apply(long executionId, int returnCode) { Log.i(TAG, "apply: cut video finish:" + returnCode); } }); ```

  • 獲取媒體檔案時長

    ```java // 方法說明 /* * 獲取視訊檔案時長 * * @param filepath 視訊檔案絕對路徑 * @return 時長(ms) / public long getVideoDuration(String filepath)

    // 呼叫示例 long duration = FFmpegUtils.getInstance().getVideoDuration("/sdcard/5m.mp4"); ```

  • 新增字幕

    ```java // Application中註冊字型 / 註冊字幕字型 / SubtitleFont fontTljt = new SubtitleFont(R.raw.tljt, "tljt", "葉根友特隸簡體"); // "葉根友特隸簡體"為ttf檔案開啟的第一行文字 SubtitleFont fontKxjt = new SubtitleFont(R.raw.kxjt, "kxjt", "葉根友空心簡體"); List fonts = new ArrayList<>(); fonts.add(fontTljt); fonts.add(fontKxjt); FFmpegUtils.getInstance().initRegisterFonts(this, fonts);

    // 方法說明 /* * 新增字幕 * * @param videoFilepath 視訊檔案路徑 * @param subtitleFilepath 字幕檔案路徑 * @param saveFilepath 合成後視訊檔案儲存路徑 * @param coverSaveFilepath 是否覆蓋已存在檔案(預設覆蓋) * @param fontName 字型名 * @param fontSize 字型大小 * @param preset 合成速度,空間換取時間(預設ultrafast) * @param crf 合成質量,0-51遞減,18-25基本無損(預設25) * @param statisticsCallback 進度回撥 * @param executeCallback 結果回撥 / public void compressSubtitle(String videoFilepath, String subtitleFilepath, String saveFilepath, boolean coverSaveFilepath, String fontName, String fontSize, String preset, String crf, StatisticsCallback statisticsCallback, ExecuteCallback executeCallback)

    // 呼叫示例 findViewById(R.id.btCompressSubtitle).setOnClickListener(v -> { long startTime = System.currentTimeMillis();

    String videoFilepath = "/sdcard/6s.mp4";
    String subtitleFilepath = "/sdcard/6s.srt";
    mDuration = FFmpegUtils.getInstance().getVideoDuration(videoFilepath);  // mDuration為當前檔案時長
    
    String fontName = ((EditText) findViewById(R.id.etSubtitleFontName)).getText().toString().trim();
    String fontSize = ((EditText) findViewById(R.id.etSubtitleFontSize)).getText().toString().trim();
    String saveFilepath = "/sdcard/dest_compress_" + System.currentTimeMillis() + ".mp4";
    
    FFmpegUtils.getInstance().compressSubtitle(videoFilepath, subtitleFilepath, saveFilepath, fontName, fontSize, new StatisticsCallback() {
        @Override
        public void apply(Statistics statistics) {
            if (mDuration == 0) return;
            // String progress = statistics.getTime() / 1_000 / mDuration + "";
            String progress = new BigDecimal(statistics.getTime() / 1_000).multiply(new BigDecimal(100)).divide(new BigDecimal(mDuration), 0, BigDecimal.ROUND_HALF_UP).toString();
    
            String msg = String.format("subtitle compress progress:" + progress + "-frame: %d, time: %d, quality: %s", statistics.getVideoFrameNumber(), statistics.getTime(), statistics.getVideoQuality() + "-duration:" + mDuration);
            Log.d(TAG, msg);
        }
    }, new ExecuteCallback() {
        @Override
        public void apply(long executionId, int returnCode) {
            Log.i(TAG, "subtitle compress finish:" + returnCode + "-time used(ms):" + (System.currentTimeMillis() - startTime) + "-save filepath:" + saveFilepath + "-fontName:" + fontName + "-fontSize:" + fontSize);
    
        }
    });
    

    }); ```

  • 取消ffmpeg操作

    java FFmpegUtils.getInstance().cancel()

  • 同步/非同步執行命令

    ```java // 非同步 long excutionId = FFmpeg.executeAsync(cmd, ExecuteCallback);

    // 同步-ret0成功255使用者取消其他失敗 int ret = FFmpeg.execute(cmd); ```