音視頻的Java框架找了一大圈,除了JavaCV,目前找不到其他的。JavaCV封裝了對底層C的調用,最終實際上執行的都是FFMPEG的函數。現在有個頭疼的問題,FFMPEG的字幕合成用命令行一敲就完事了,比如想往input.mkv里合入字幕subtitles.srt,輸出到output.mkv,簡單的很:
ffmpeg -i input.mkv -vf subtitles=subtitles.srt output.mkv
上面的-vf參數表示內嵌字幕,也就是不用手動在播放器里設置,字幕就自己出來了。JavaCV怎么實現字幕合成呢?找了一大圈,沒有。OpenCV倒是可以實現,就是比較麻煩,大概思路是:音視頻幀 -> 視頻幀 -> 視頻幀轉圖片 -> 字幕文本畫到圖片指定位置上 -> 圖片轉回視頻幀 -> 寫視頻幀 -> 寫音頻幀。說白了就是文本水印。可惜OpenCV不支持中文,畫出來都是問號。
OpenCV就是一個畫圖工具而已,換,用Java自帶的awt畫圖工具對象Graphics2D。不廢話,上代碼:
1、pom.xml添加Java CV依賴:
<dependency> <groupId>org.bytedeco</groupId> <artifactId>javacv-platform</artifactId> <version>1.5.2</version> </dependency>
2、示例代碼,這里用幾句話模擬字幕循環輸出,並展示合入字幕的時間:
import org.bytedeco.ffmpeg.global.avcodec; import org.bytedeco.javacv.*; import org.bytedeco.javacv.Frame; import sun.font.FontDesignMetrics; import java.awt.*; import java.awt.image.BufferedImage; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; public class SubtitleMix { private final static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static void main(String[] args) throws FrameGrabber.Exception, FrameRecorder.Exception { // 構造測試字幕 String[] test = { "世上無難事", "只怕有心人", "只要思想不滑坡", "辦法總比困難多", "長江后浪推前浪", "前浪死在沙灘上" }; // 為連續的50幀設置同一個測試字幕文本 List<String> testStr = new ArrayList<>(); for (int i = 0; i < 300; i++) { testStr.add(test[i / 50]); } // 設置源視頻、加字幕后的視頻文件路徑 FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault("E:\\BaiduNetdiskDownload\\testout.mkv"); grabber.start(); FFmpegFrameRecorder recorder = new FFmpegFrameRecorder("E:\\BaiduNetdiskDownload\\outtest.mkv", 1280, 720, 2); // 視頻相關配置,取原視頻配置 recorder.setFrameRate(grabber.getFrameRate()); recorder.setVideoCodec(grabber.getVideoCodec()); recorder.setVideoBitrate(grabber.getVideoBitrate()); // 音頻相關配置,取原音頻配置 recorder.setSampleRate(grabber.getSampleRate()); recorder.setAudioCodec(avcodec.AV_CODEC_ID_MP3); recorder.start(); System.out.println("准備開始推流..."); Java2DFrameConverter converter = new Java2DFrameConverter(); Frame frame; int i = 0; while ((frame = grabber.grab()) != null) { // 從視頻幀中獲取圖片 if (frame.image != null) { BufferedImage bufferedImage = converter.getBufferedImage(frame); // 對圖片進行文本合入 bufferedImage = addSubtitle(bufferedImage, testStr.get(i++ % 300)); // 視頻幀賦值,寫入輸出流 frame.image = converter.getFrame(bufferedImage).image; recorder.record(frame); } // 音頻幀寫入輸出流 if(frame.samples != null) { recorder.record(frame); } } System.out.println("推流結束..."); grabber.stop(); recorder.stop(); } /** * 圖片添加文本 * * @param bufImg * @param subTitleContent * @return */ private static BufferedImage addSubtitle(BufferedImage bufImg, String subTitleContent) { // 添加字幕時的時間 Font font = new Font("微軟雅黑", Font.BOLD, 32); String timeContent = sdf.format(new Date()); FontDesignMetrics metrics = FontDesignMetrics.getMetrics(font); Graphics2D graphics = bufImg.createGraphics(); graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); graphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER)); //設置圖片背景 graphics.drawImage(bufImg, 0, 0, bufImg.getWidth(), bufImg.getHeight(), null); //設置左上方時間顯示 graphics.setColor(Color.orange); graphics.setFont(font); graphics.drawString(timeContent, 0, metrics.getAscent()); // 計算文字長度,計算居中的x點坐標 int textWidth = metrics.stringWidth(subTitleContent); int widthX = (bufImg.getWidth() - textWidth) / 2; graphics.setColor(Color.red); graphics.setFont(font); graphics.drawString(subTitleContent, widthX, bufImg.getHeight() - 100); graphics.dispose(); return bufImg; } }
打完收工,看看效果: