安卓使用ffmpeg给视频添加字幕

1,538 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

包含添加字幕能力的ffmpeg和相关so编译

  1. 需要下载的源码

    github.com/tanersener/… github.com/tanersener/… github.com/tanersener/…

    考虑源码较大,github不好下载成功,可以从gitee镜像仓库下载 gitee.com/catface7/ta… gitee.com/catface7/ta… gitee.com/catface7/ta…

    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

参考github.com/itCatface/c… app_ffmpeg_demo模块,以下为使用示例

  • 音频文件裁剪

    // 方法说明
    /**
     *  裁剪音频文件
     *
     * @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);
        }
    });
    
  • 视频文件裁剪

    // 方法说明
    /**
     * 裁剪视频文件
     *
     * @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);
        }
    });
    
  • 获取媒体文件时长

    // 方法说明
    /**
     * 获取视频文件时长
     *
     * @param filepath 视频文件绝对路径
     * @return 时长(ms)
     */
    public long getVideoDuration(String filepath)
     
     
    // 调用示例
    long duration = FFmpegUtils.getInstance().getVideoDuration("/sdcard/5m.mp4");
    
  • 添加字幕

    // Application中注册字体
    /* 注册字幕字体 */
    SubtitleFont fontTljt = new SubtitleFont(R.raw.tljt, "tljt", "叶根友特隶简体");    // "叶根友特隶简体"为ttf文件打开的第一行文本
    SubtitleFont fontKxjt = new SubtitleFont(R.raw.kxjt, "kxjt", "叶根友空心简体");
    List<SubtitleFont> 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操作

    FFmpegUtils.getInstance().cancel()
    
  • 同步/异步执行命令

    // 异步
    long excutionId = FFmpeg.executeAsync(cmd, ExecuteCallback);
     
     
    // 同步-ret0成功255用户取消其他失败
    int ret = FFmpeg.execute(cmd);