javacv添加字幕 剧中显示

介绍

javacv目前不能像ffmpeg那样 直接加载字体文件到视频 参考这里

所以实现流程为:提取帧 -> 转图片 -> 编辑图片增加文字 -> 转回帧 -> 输出视频

上代码

java 复制代码
/**
     * 添加字幕
     *
     * @param videoFile      原始视频文件
     * @param outputFile     输出视频文件
     * @param subtitleFrames 字幕帧数组,帧是要有序的 根据时间由小到大;
     */
    public static void addSubtitle(File videoFile, File outputFile, List<SubtitleFrame> subtitleFrames) {
        Font font;
        // 加载字体文件,防止中文乱码
        try (InputStream fontStream = CvUtil.class.getResourceAsStream("/fonts/msyh.ttc")) {
            if (fontStream == null) {
                log.error("字体文件加载失败");
                throw new BusinessException(ErrorCodeEnum.Unknow);
            }
            font = Font.createFont(Font.TRUETYPE_FONT, fontStream).deriveFont(Font.BOLD, 24);
        } catch (Exception e) {
            log.error("字体文件加载失败", e);
            throw new BusinessException(e);
        }

        try (FFmpegFrameGrabber grabberVideo = new FFmpegFrameGrabber(videoFile)) {
            grabberVideo.start();

            try (FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(outputFile, grabberVideo.getImageWidth(), grabberVideo.getImageHeight())) {
                // 视频参数
                recorder.setFrameRate(grabberVideo.getFrameRate());
                recorder.setVideoCodec(grabberVideo.getVideoCodec());
                recorder.setFormat("mp4");
                recorder.setVideoBitrate(grabberVideo.getVideoBitrate());
                // 音频参数
                recorder.setAudioCodec(grabberVideo.getAudioCodec()); // 音频编码
                recorder.setAudioBitrate(grabberVideo.getAudioBitrate());
                recorder.setSampleRate(grabberVideo.getSampleRate()); // 采样率
                recorder.setAudioChannels(grabberVideo.getAudioChannels()); // 必须大于0,如立体声为2
                recorder.start();

                Frame frame;
                Iterator<SubtitleFrame> subtitleFrameIterator = subtitleFrames.iterator();
                SubtitleFrame subtitleFrame = subtitleFrameIterator.next();
                // 是否使用过当前字幕帧,只有只用过才可以迭代下一个帧; 这个标志可以帮助实现这种 0-2秒 2-4秒 7-10秒 这种时间不连续的字幕
                boolean useSubtitleFrame = false;

                while ((frame = grabberVideo.grabImage()) != null) {
                    // 转换为秒
                    long time = frame.timestamp / 1000;
                    String subtitle = null;

                    if (time >= subtitleFrame.sta && time < subtitleFrame.end) {
                        subtitle = subtitleFrame.text;
                        useSubtitleFrame = true;
                    } else if (useSubtitleFrame && subtitleFrameIterator.hasNext()) {
                        // 当前的字幕帧使用过 才可以切换下一个
                        subtitleFrame = subtitleFrameIterator.next();
                        useSubtitleFrame = false; // 新字幕帧标志为未使用
                    }

                    if (subtitle != null) {
                        BufferedImage image = Java2DFrameUtils.toBufferedImage(frame);
                        Graphics2D g2 = image.createGraphics();
                        g2.setFont(font);
                        // 计算字体宽度,方便剧中
                        FontMetrics fm = g2.getFontMetrics();
                        int textWidth = fm.stringWidth(subtitleFrame.text);
                        // 字幕位置
                        int[] subtitlePosition = calcSubtitlePosition(grabberVideo.getImageWidth(), grabberVideo.getImageHeight(), textWidth);
                        g2.drawString(subtitle, subtitlePosition[0], subtitlePosition[1]);
                        g2.dispose();
                        recorder.record(Java2DFrameUtils.toFrame(image));
                    } else {
                        recorder.record(frame);
                    }
                }
            }

        } catch (Exception e) {
            throw new BusinessException(e);
        }
    }

    /**
     * 计算字幕位置
     * 字体高度 10%  横向剧中
     *
     * @return int[]{x,y}
     */
    public static int[] calcSubtitlePosition(int videoWidth, int videoHeight, int textWidth) {
        int y = videoHeight - videoHeight / 10;
        // 24号字体约等于 32px
        int x = videoWidth / 2 - textWidth / 2;

        return new int[]{x, y};
    }

    /**
     * 字幕帧
     */
    public static class SubtitleFrame {

        public SubtitleFrame() {
        }

        public SubtitleFrame(long sta, long end, String text) {
            this.sta = sta;
            this.end = end;
            this.text = text;
        }

        // 起始时间 毫秒
        public long sta;

        // 结束时间 毫秒
        public long end;

        // 字幕文本
        public String text;
    }
相关推荐
vipxieliang6 小时前
ValidX 验证器缓存机制:如何提升验证性能
java·spring boot
未秃头的程序猿6 小时前
Spring Cloud Gateway 做统一加解密,我在请求体上栽了三天
java·后端·架构
Rain的Java大神之路6 小时前
别再上来就堆 Redis!点赞系统从打挂数据库到 18w 写峰值,我把这套方案讲透了
java·后端·面试
SimonKing6 小时前
开源文档解析天花板 MinerU 4.0:PDF / 图片 / Office 等任意文档都支持
java·后端·程序员
咖啡八杯7 小时前
Spring MessageSource 国际化消息解析流程
java·后端·spring·架构·框架·若依
博.闻广见7 小时前
第10篇:Agent系统的性能优化 —— 缓存、压缩与动态模型路由
java·人工智能·后端·spring·agent
Irene19917 小时前
Flink 学习日志 01:前端转大数据的实战避坑指南
java·jvm·flink
Thomas.Sir7 小时前
第1课:SpringBoot 4.x 快速上手:新特性、环境准备与第一个HelloWorld程序
java·spring boot
devilnumber8 小时前
Java 内部迭代 vs 外部迭代:从原理到实战避坑
java·开发语言
杨运交8 小时前
080-缓存模块-Spring Cache缓存键前缀设计与租户隔离实践
java·spring·缓存