安卓使用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); ```