介绍
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;
}