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