您好,登錄后才能下訂單哦!
今天就跟大家聊聊有關如何在Java使用FFmpeg處理視頻文件,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結了以下內容,希望大家根據這篇文章可以有所收獲。
開發前準備
在使用Java調用FFmpeg處理音視頻之前,需要先安裝FFmpeg,安裝方法分為兩種:
引入封裝了FFmpeg的開源框架
在系統中手動安裝FFmpeg
2.1 引入封裝了FFmpeg的開源框架
JAVE.jar(官網點我) 是一個封裝了FFmpeg的Java框架,在項目中能直接調用它的API來處理音視頻文件;
優點:使用方便,直接在項目中引入JAVE.jar即可處理媒體文件,且開發完成后可以隨工程一起打包發布,不需要在目標運行環境內手動安裝FFmpeg相關的類庫
缺點:JAVE.jar最后一次更新是2009年,其封裝的FFmpeg版本是09年或更早前的版本,比較老舊,無法使用一些新特性
(當然也可以看看有沒有其他比較新的封裝了FFmpeg的框架)
Maven坐標如下:
<dependency> <groupId>org.ffmpeg</groupId> <artifactId>sdk</artifactId> <version>1.0.2</version> </dependency>
2.2 在系統中手動安裝FFmpeg
在運行環境中手動安裝FFmpeg稍微有一些麻煩,可以百度 windows/mac安裝FFmpeg 這樣的關鍵字,根據網上的安裝教程將FFmpeg安裝到系統中;
懶人鏈接:Windows安裝教程 Mac安裝教程
優點:可以直接調用FFmpeg的相關API處理音視頻,FFmpeg版本可控
缺點:手動安裝較為麻煩,開發環境與目標運行環境都需要先安裝好FFmpeg
3. 使用FFmpeg處理音視頻
使用JAVE.jar進行開發與直接使用FFmpeg開發的代碼有一些不同,這里以直接使用FFmpeg進行開發的代碼進行講解(開發環境MacOS);(使用JAVE的代碼、直接使用FFmpeg的代碼都會附在文末供大家下載參考)
通過MediaUtil.java類及其依賴的類,你將可以實現:
解析源視頻的基本信息,包括視頻格式、時長、碼率等;
解析音頻、圖片的基本信息;
將源視頻轉換成不同分辨率、不同碼率、帶或不帶音頻的新視頻;
抽取源視頻中指定時間點的幀畫面,來生成一張靜態圖;
抽取源視頻中指定時間段的幀畫面,來生成一個GIF動態圖;
截取源視頻中的一段來形成一個新視頻;
抽取源視頻中的音頻信息,生成單獨的MP3文件;
對音視頻等媒體文件執行自定義的FFmpeg命令;
3.1 代碼結構梳理
MediaUtil.java是整個解析程序中的核心類,封裝了各種常用的解析方法供外部調用;
MetaInfo.java定義了多媒體數據共有的一些屬性,VideoMetaInfo.java MusicMetaInfo.java ImageMetaInfo.java都繼承自MetaInfo.java,分別定義了視頻、音頻、圖片數據相關的一些屬性;
AnimatedGifEncoder.java LZWEncoder.java NeuQuant.java在抽取視頻幀數、制作GIF動態圖的時候會使用到;
CrfValueEnum.java 定義了三種常用的FFmpeg壓縮視頻時使用到的crf值,PresetVauleEnum.java定義了FFmpeg壓縮視頻時常用的幾種壓縮速率值;
有關crf、preset的延伸閱讀點我
3.2 MediaUtil.java主程序類解析
3.2.1 使用前需要注意的幾點
1、指定正確的FFmpeg程序執行路徑
MacOS安裝好FFmpeg后,可以在控制臺中通過which ffmpeg命令獲取FFmpeg程序的執行路徑,在調用MediaUtil.java前先通過其 setFFmpegPath() 方法設置好FFmpeg程序在系統中的執行路徑,然后才能順利調用到FFmpeg去解析音視頻;
Windows系統下該路徑理論上應設置為:FFmpeg可執行程序在系統中的絕對路徑(實際情況有待大家補充)
2、指定解析音視頻信息時需要的正則表達式
因項目需要解析后綴格式為 .MP4 .WMV .AAC 的視頻和音頻文件,所以我研究了JAVE.jar底層調用FFmpeg時的解析邏輯后,在MediaUtil.java中設置好了匹配這三種格式的正則表達式供解析時使用(參考程序中的 durationRegex videoStreamRegex musicStreamRegex 這三個表達式值);
注意:如果你需要解析其他后綴格式如 .MKV .MP3 這樣的媒體文件時,你很可能需要根據實際情況修改durationRegex videoStreamRegex musicStreamRegex 這三個正則表達式的值,否則可能無法解析出正確的信息;
3、程序中的很多默認值你可以根據實際需要修改,比如視頻幀抽取的默認寬度或高度值、時長等等;
3.2.2 MediaUtil.java代碼
package media; import lombok.extern.slf4j.Slf4j; import media.domain.ImageMetaInfo; import media.domain.MusicMetaInfo; import media.domain.VideoMetaInfo; import media.domain.gif.AnimatedGifEncoder; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.*; import java.sql.Time; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * 基于FFmpeg內核來編解碼音視頻信息; * 使用前需手動在運行環境中安裝FFmpeg運行程序,然后正確設置FFmpeg運行路徑后MediaUtil.java才能正常調用到FFmpeg程序去處理音視頻; * * Author: dreamer-1 * * version: 1.0 * */ @Slf4j public class MediaUtil { /** * 可以處理的視頻格式 */ public final static String[] VIDEO_TYPE = { "MP4", "WMV" }; /** * 可以處理的圖片格式 */ public final static String[] IMAGE_TYPE = { "JPG", "JPEG", "PNG", "GIF" }; /** * 可以處理的音頻格式 */ public final static String[] AUDIO_TYPE = { "AAC" }; /** * 視頻幀抽取時的默認時間點,第10s(秒) * (Time類構造參數的單位:ms) */ private static final Time DEFAULT_TIME = new Time(0, 0, 10); /** * 視頻幀抽取的默認寬度值,單位:px */ private static int DEFAULT_WIDTH = 320; /** * 視頻幀抽取的默認時長,單位:s(秒) */ private static int DEFAULT_TIME_LENGTH = 10; /** * 抽取多張視頻幀以合成gif動圖時,gif的播放速度 */ private static int DEFAULT_GIF_PLAYTIME = 110; /** * FFmpeg程序執行路徑 * 當前系統安裝好ffmpeg程序并配置好相應的環境變量后,值為ffmpeg可執行程序文件在實際系統中的絕對路徑 */ private static String FFMPEG_PATH = "/usr/bin/ffmpeg"; // /usr/bin/ffmpeg /** * 視頻時長正則匹配式 * 用于解析視頻及音頻的時長等信息時使用; * * (.*?)表示:匹配任何除\r\n之外的任何0或多個字符,非貪婪模式 * */ private static String durationRegex = "Duration: (\\d*?):(\\d*?):(\\d*?)\\.(\\d*?), start: (.*?), bitrate: (\\d*) kb\\/s.*"; private static Pattern durationPattern; /** * 視頻流信息正則匹配式 * 用于解析視頻詳細信息時使用; */ private static String videoStreamRegex = "Stream #\\d:\\d[\\(]??\\S*[\\)]??: Video: (\\S*\\S$?)[^\\,]*, (.*?), (\\d*)x(\\d*)[^\\,]*, (\\d*) kb\\/s, (\\d*[\\.]??\\d*) fps"; private static Pattern videoStreamPattern; /** * 音頻流信息正則匹配式 * 用于解析音頻詳細信息時使用; */ private static String musicStreamRegex = "Stream #\\d:\\d[\\(]??\\S*[\\)]??: Audio: (\\S*\\S$?)(.*), (.*?) Hz, (.*?), (.*?), (\\d*) kb\\/s";; private static Pattern musicStreamPattern; /** * 靜態初始化時先加載好用于音視頻解析的正則匹配式 */ static { durationPattern = Pattern.compile(durationRegex); videoStreamPattern = Pattern.compile(videoStreamRegex); musicStreamPattern = Pattern.compile(musicStreamRegex); } /** * 獲取當前多媒體處理工具內的ffmpeg的執行路徑 * @return */ public static String getFFmpegPath() { return FFMPEG_PATH; } /** * 設置當前多媒體工具內的ffmpeg的執行路徑 * @param ffmpeg_path ffmpeg可執行程序在實際系統中的絕對路徑 * @return */ public static boolean setFFmpegPath(String ffmpeg_path) { if (StringUtils.isBlank(ffmpeg_path)) { log.error("--- 設置ffmpeg執行路徑失敗,因為傳入的ffmpeg可執行程序路徑為空! ---"); return false; } File ffmpegFile = new File(ffmpeg_path); if (!ffmpegFile.exists()) { log.error("--- 設置ffmpeg執行路徑失敗,因為傳入的ffmpeg可執行程序路徑下的ffmpeg文件不存在! ---"); return false; } FFMPEG_PATH = ffmpeg_path; log.info("--- 設置ffmpeg執行路徑成功 --- 當前ffmpeg可執行程序路徑為: " + ffmpeg_path); return true; } /** * 測試當前多媒體工具是否可以正常工作 * @return */ public static boolean isExecutable() { File ffmpegFile = new File(FFMPEG_PATH); if (!ffmpegFile.exists()) { log.error("--- 工作狀態異常,因為傳入的ffmpeg可執行程序路徑下的ffmpeg文件不存在! ---"); return false; } List<String> cmds = new ArrayList<>(1); cmds.add("-version"); String ffmpegVersionStr = executeCommand(cmds); if (StringUtils.isBlank(ffmpegVersionStr)) { log.error("--- 工作狀態異常,因為ffmpeg命令執行失敗! ---"); return false; } log.info("--- 工作狀態正常 ---"); return true; } /** * 執行FFmpeg命令 * @param commonds 要執行的FFmpeg命令 * @return FFmpeg程序在執行命令過程中產生的各信息,執行出錯時返回null */ public static String executeCommand(List<String> commonds) { if (CollectionUtils.isEmpty(commonds)) { log.error("--- 指令執行失敗,因為要執行的FFmpeg指令為空! ---"); return null; } LinkedList<String> ffmpegCmds = new LinkedList<>(commonds); ffmpegCmds.addFirst(FFMPEG_PATH); // 設置ffmpeg程序所在路徑 log.info("--- 待執行的FFmpeg指令為:---" + ffmpegCmds); Runtime runtime = Runtime.getRuntime(); Process ffmpeg = null; try { // 執行ffmpeg指令 ProcessBuilder builder = new ProcessBuilder(); builder.command(ffmpegCmds); ffmpeg = builder.start(); log.info("--- 開始執行FFmpeg指令:--- 執行線程名:" + builder.toString()); // 取出輸出流和錯誤流的信息 // 注意:必須要取出ffmpeg在執行命令過程中產生的輸出信息,如果不取的話當輸出流信息填滿jvm存儲輸出留信息的緩沖區時,線程就回阻塞住 PrintStream errorStream = new PrintStream(ffmpeg.getErrorStream()); PrintStream inputStream = new PrintStream(ffmpeg.getInputStream()); errorStream.start(); inputStream.start(); // 等待ffmpeg命令執行完 ffmpeg.waitFor(); // 獲取執行結果字符串 String result = errorStream.stringBuffer.append(inputStream.stringBuffer).toString(); // 輸出執行的命令信息 String cmdStr = Arrays.toString(ffmpegCmds.toArray()).replace(",", ""); String resultStr = StringUtils.isBlank(result) ? "【異常】" : "正常"; log.info("--- 已執行的FFmepg命令: ---" + cmdStr + " 已執行完畢,執行結果: " + resultStr); return result; } catch (Exception e) { log.error("--- FFmpeg命令執行出錯! --- 出錯信息: " + e.getMessage()); return null; } finally { if (null != ffmpeg) { ProcessKiller ffmpegKiller = new ProcessKiller(ffmpeg); // JVM退出時,先通過鉤子關閉FFmepg進程 runtime.addShutdownHook(ffmpegKiller); } } } /** * 視頻轉換 * * 注意指定視頻分辨率時,寬度和高度必須同時有值; * * @param fileInput 源視頻路徑 * @param fileOutPut 轉換后的視頻輸出路徑 * @param withAudio 是否保留音頻;true-保留,false-不保留 * @param crf 指定視頻的質量系數(值越小,視頻質量越高,體積越大;該系數取值為0-51,直接影響視頻碼率大小),取值參考:CrfValueEnum.code * @param preset 指定視頻的編碼速率(速率越快壓縮率越低),取值參考:PresetVauleEnum.presetValue * @param width 視頻寬度;為空則保持源視頻寬度 * @param height 視頻高度;為空則保持源視頻高度 */ public static void convertVideo(File fileInput, File fileOutPut, boolean withAudio, Integer crf, String preset, Integer width, Integer height) { if (null == fileInput || !fileInput.exists()) { throw new RuntimeException("源視頻文件不存在,請檢查源視頻路徑"); } if (null == fileOutPut) { throw new RuntimeException("轉換后的視頻路徑為空,請檢查轉換后的視頻存放路徑是否正確"); } if (!fileOutPut.exists()) { try { fileOutPut.createNewFile(); } catch (IOException e) { log.error("視頻轉換時新建輸出文件失敗"); } } String format = getFormat(fileInput); if (!isLegalFormat(format, VIDEO_TYPE)) { throw new RuntimeException("無法解析的視頻格式:" + format); } List<String> commond = new ArrayList<String>(); commond.add("-i"); commond.add(fileInput.getAbsolutePath()); if (!withAudio) { // 設置是否保留音頻 commond.add("-an"); // 去掉音頻 } if (null != width && width > 0 && null != height && height > 0) { // 設置分辨率 commond.add("-s"); String resolution = width.toString() + "x" + height.toString(); commond.add(resolution); } commond.add("-vcodec"); // 指定輸出視頻文件時使用的編碼器 commond.add("libx264"); // 指定使用x264編碼器 commond.add("-preset"); // 當使用x264時需要帶上該參數 commond.add(preset); // 指定preset參數 commond.add("-crf"); // 指定輸出視頻質量 commond.add(crf.toString()); // 視頻質量參數,值越小視頻質量越高 commond.add("-y"); // 當已存在輸出文件時,不提示是否覆蓋 commond.add(fileOutPut.getAbsolutePath()); executeCommand(commond); } /** * 視頻幀抽取 * 默認抽取第10秒的幀畫面 * 抽取的幀圖片默認寬度為300px * * 轉換后的文件路徑以.gif結尾時,默認截取從第10s開始,后10s以內的幀畫面來生成gif * * @param videoFile 源視頻路徑 * @param fileOutPut 轉換后的文件路徑 */ public static void cutVideoFrame(File videoFile, File fileOutPut) { cutVideoFrame(videoFile, fileOutPut, DEFAULT_TIME); } /** * 視頻幀抽取(抽取指定時間點的幀畫面) * 抽取的視頻幀圖片寬度默認為320px * * 轉換后的文件路徑以.gif結尾時,默認截取從指定時間點開始,后10s以內的幀畫面來生成gif * * @param videoFile 源視頻路徑 * @param fileOutPut 轉換后的文件路徑 * @param time 指定抽取視頻幀的時間點(單位:s) */ public static void cutVideoFrame(File videoFile, File fileOutPut, Time time) { cutVideoFrame(videoFile, fileOutPut, time, DEFAULT_WIDTH); } /** * 視頻幀抽取(抽取指定時間點、指定寬度值的幀畫面) * 只需指定視頻幀的寬度,高度隨寬度自動計算 * * 轉換后的文件路徑以.gif結尾時,默認截取從指定時間點開始,后10s以內的幀畫面來生成gif * * @param videoFile 源視頻路徑 * @param fileOutPut 轉換后的文件路徑 * @param time 指定要抽取第幾秒的視頻幀(單位:s) * @param width 抽取的視頻幀圖片的寬度(單位:px) */ public static void cutVideoFrame(File videoFile, File fileOutPut, Time time, int width) { if (null == videoFile || !videoFile.exists()) { throw new RuntimeException("源視頻文件不存在,請檢查源視頻路徑"); } if (null == fileOutPut) { throw new RuntimeException("轉換后的視頻路徑為空,請檢查轉換后的視頻存放路徑是否正確"); } VideoMetaInfo info = getVideoMetaInfo(videoFile); if (null == info) { log.error("--- 未能解析源視頻信息,視頻幀抽取操作失敗 --- 源視頻: " + videoFile); return; } int height = width * info.getHeight() / info.getWidth(); // 根據寬度計算適合的高度,防止畫面變形 cutVideoFrame(videoFile, fileOutPut, time, width, height); } /** * 視頻幀抽取(抽取指定時間點、指定寬度值、指定高度值的幀畫面) * * 轉換后的文件路徑以.gif結尾時,默認截取從指定時間點開始,后10s以內的幀畫面來生成gif * * @param videoFile 源視頻路徑 * @param fileOutPut 轉換后的文件路徑 * @param time 指定要抽取第幾秒的視頻幀(單位:s) * @param width 抽取的視頻幀圖片的寬度(單位:px) * @param height 抽取的視頻幀圖片的高度(單位:px) */ public static void cutVideoFrame(File videoFile, File fileOutPut, Time time, int width, int height) { if (null == videoFile || !videoFile.exists()) { throw new RuntimeException("源視頻文件不存在,請檢查源視頻路徑"); } if (null == fileOutPut) { throw new RuntimeException("轉換后的視頻路徑為空,請檢查轉換后的視頻存放路徑是否正確"); } String format = getFormat(fileOutPut); if (!isLegalFormat(format, IMAGE_TYPE)) { throw new RuntimeException("無法生成指定格式的幀圖片:" + format); } String fileOutPutPath = fileOutPut.getAbsolutePath(); if (!"GIF".equals(StringUtils.upperCase(format))) { // 輸出路徑不是以.gif結尾,抽取并生成一張靜態圖 cutVideoFrame(videoFile, fileOutPutPath, time, width, height, 1, false); } else { // 抽取并生成一個gif(gif由10張靜態圖構成) String path = fileOutPut.getParent(); String name = fileOutPut.getName(); // 創建臨時文件存儲多張靜態圖用于生成gif String tempPath = path + File.separator + System.currentTimeMillis() + "_" + name.substring(0, name.indexOf(".")); File file = new File(tempPath); if (!file.exists()) { file.mkdir(); } try { cutVideoFrame(videoFile, tempPath, time, width, height, DEFAULT_TIME_LENGTH, true); // 生成gif String images[] = file.list(); for (int i = 0; i < images.length; i++) { images[i] = tempPath + File.separator + images[i]; } createGifImage(images, fileOutPut.getAbsolutePath(), DEFAULT_GIF_PLAYTIME); } catch (Exception e) { log.error("--- 截取視頻幀操作出錯 --- 錯誤信息:" + e.getMessage()); } finally { // 刪除用于生成gif的臨時文件 String images[] = file.list(); for (int i = 0; i < images.length; i++) { File fileDelete = new File(tempPath + File.separator + images[i]); fileDelete.delete(); } file.delete(); } } } /** * 視頻幀抽取(抽取指定時間點、指定寬度值、指定高度值、指定時長、指定單張/多張的幀畫面) * * @param videoFile 源視頻 * @param path 轉換后的文件輸出路徑 * @param time 開始截取視頻幀的時間點(單位:s) * @param width 截取的視頻幀圖片的寬度(單位:px) * @param height 截取的視頻幀圖片的高度(單位:px,需要大于20) * @param timeLength 截取的視頻幀的時長(從time開始算,單位:s,需小于源視頻的最大時長) * @param isContinuty false - 靜態圖(只截取time時間點的那一幀圖片),true - 動態圖(截取從time時間點開始,timelength這段時間內的多張幀圖) */ private static void cutVideoFrame(File videoFile, String path, Time time, int width, int height, int timeLength, boolean isContinuty) { if (videoFile == null || !videoFile.exists()) { throw new RuntimeException("源視頻文件不存在,源視頻路徑: "); } if (null == path) { throw new RuntimeException("轉換后的文件路徑為空,請檢查轉換后的文件存放路徑是否正確"); } VideoMetaInfo info = getVideoMetaInfo(videoFile); if (null == info) { throw new RuntimeException("未解析到視頻信息"); } if (time.getTime() + timeLength > info.getDuration()) { throw new RuntimeException("開始截取視頻幀的時間點不合法:" + time.toString() + ",因為截取時間點晚于視頻的最后時間點"); } if (width <= 20 || height <= 20) { throw new RuntimeException("截取的視頻幀圖片的寬度或高度不合法,寬高值必須大于20"); } try { List<String> commond = new ArrayList<String>(); commond.add("-ss"); commond.add(time.toString()); if (isContinuty) { commond.add("-t"); commond.add(timeLength + ""); } else { commond.add("-vframes"); commond.add("1"); } commond.add("-i"); commond.add(videoFile.getAbsolutePath()); commond.add("-an"); commond.add("-f"); commond.add("image2"); if (isContinuty) { commond.add("-r"); commond.add("3"); } commond.add("-s"); commond.add(width + "*" + height); if (isContinuty) { commond.add(path + File.separator + "foo-%03d.jpeg"); } else { commond.add(path); } executeCommand(commond); } catch (Exception e) { log.error("--- 視頻幀抽取過程出錯 --- 錯誤信息: " + e.getMessage()); } } /** * 截取視頻中的某一段,生成新視頻 * * @param videoFile 源視頻路徑 * @param outputFile 轉換后的視頻路徑 * @param startTime 開始抽取的時間點(單位:s) * @param timeLength 需要抽取的時間段(單位:s,需小于源視頻最大時長);例如:該參數值為10時即抽取從startTime開始之后10秒內的視頻作為新視頻 */ public static void cutVideo(File videoFile, File outputFile, Time startTime, int timeLength) { if (videoFile == null || !videoFile.exists()) { throw new RuntimeException("視頻文件不存在:"); } if (null == outputFile) { throw new RuntimeException("轉換后的視頻路徑為空,請檢查轉換后的視頻存放路徑是否正確"); } VideoMetaInfo info = getVideoMetaInfo(videoFile); if (null == info) { throw new RuntimeException("未解析到視頻信息"); } if (startTime.getTime() + timeLength > info.getDuration()) { throw new RuntimeException("截取時間不合法:" + startTime.toString() + ",因為截取時間大于視頻的時長"); } try { if (!outputFile.exists()) { outputFile.createNewFile(); } List<String> commond = new ArrayList<String>(); commond.add("-ss"); commond.add(startTime.toString()); commond.add("-t"); commond.add("" + timeLength); commond.add("-i"); commond.add(videoFile.getAbsolutePath()); commond.add("-vcodec"); commond.add("copy"); commond.add("-acodec"); commond.add("copy"); commond.add(outputFile.getAbsolutePath()); executeCommand(commond); } catch (IOException e) { log.error("--- 視頻截取過程出錯 ---"); } } /** * 抽取視頻里的音頻信息 * 只能抽取成MP3文件 * @param videoFile 源視頻文件 * @param audioFile 從源視頻提取的音頻文件 */ public static void getAudioFromVideo(File videoFile, File audioFile) { if (null == videoFile || !videoFile.exists()) { throw new RuntimeException("源視頻文件不存在: "); } if (null == audioFile) { throw new RuntimeException("要提取的音頻路徑為空:"); } String format = getFormat(audioFile); if (!isLegalFormat(format, AUDIO_TYPE)) { throw new RuntimeException("無法生成指定格式的音頻:" + format + " 請檢查要輸出的音頻文件是否是AAC類型"); } try { if (!audioFile.exists()) { audioFile.createNewFile(); } List<String> commond = new ArrayList<String>(); commond.add("-i"); commond.add(videoFile.getAbsolutePath()); commond.add("-vn"); // no video,去除視頻信息 commond.add("-y"); commond.add("-acodec"); commond.add("copy"); commond.add(audioFile.getAbsolutePath()); executeCommand(commond); } catch (Exception e) { log.error("--- 抽取視頻中的音頻信息的過程出錯 --- 錯誤信息: " + e.getMessage()); } } /** * 解析視頻的基本信息(從文件中) * * 解析出的視頻信息一般為以下格式: * Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '6.mp4': * Duration: 00:00:30.04, start: 0.000000, bitrate: 19031 kb/s * Stream #0:0(eng): Video: h364 (Main) (avc1 / 0x31637661), yuv420p(tv, bt709), 1920x1080, 18684 kb/s, 25 fps, 25 tbr, 25k tbn, 50 tbc (default) * Stream #0:1(eng): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 317 kb/s (default) * * 注解: * Duration: 00:00:30.04【視頻時長】, start: 0.000000【視頻開始時間】, bitrate: 19031 kb/s【視頻比特率/碼率】 * Stream #0:0(eng): Video: h364【視頻編碼格式】 (Main) (avc1 / 0x31637661), yuv420p(tv, bt709), 1920x1080【視頻分辨率,寬x高】, 18684【視頻比特率】 kb/s, 25【視頻幀率】 fps, 25 tbr, 25k tbn, 50 tbc (default) * Stream #0:1(eng): Audio: aac【音頻格式】 (LC) (mp4a / 0x6134706D), 48000【音頻采樣率】 Hz, stereo, fltp, 317【音頻碼率】 kb/s (default) * * @param videoFile 源視頻路徑 * @return 視頻的基本信息,解碼失敗時返回null */ public static VideoMetaInfo getVideoMetaInfo(File videoFile) { if (null == videoFile || !videoFile.exists()) { log.error("--- 解析視頻信息失敗,因為要解析的源視頻文件不存在 ---"); return null; } VideoMetaInfo videoInfo = new VideoMetaInfo(); String parseResult = getMetaInfoFromFFmpeg(videoFile); Matcher durationMacher = durationPattern.matcher(parseResult); Matcher videoStreamMacher = videoStreamPattern.matcher(parseResult); Matcher videoMusicStreamMacher = musicStreamPattern.matcher(parseResult); Long duration = 0L; // 視頻時長 Integer videoBitrate = 0; // 視頻碼率 String videoFormat = getFormat(videoFile); // 視頻格式 Long videoSize = videoFile.length(); // 視頻大小 String videoEncoder = ""; // 視頻編碼器 Integer videoHeight = 0; // 視頻高度 Integer videoWidth = 0; // 視頻寬度 Float videoFramerate = 0F; // 視頻幀率 String musicFormat = ""; // 音頻格式 Long samplerate = 0L; // 音頻采樣率 Integer musicBitrate = 0; // 音頻碼率 try { // 匹配視頻播放時長等信息 if (durationMacher.find()) { long hours = (long)Integer.parseInt(durationMacher.group(1)); long minutes = (long)Integer.parseInt(durationMacher.group(2)); long seconds = (long)Integer.parseInt(durationMacher.group(3)); long dec = (long)Integer.parseInt(durationMacher.group(4)); duration = dec * 100L + seconds * 1000L + minutes * 60L * 1000L + hours * 60L * 60L * 1000L; //String startTime = durationMacher.group(5) + "ms"; videoBitrate = Integer.parseInt(durationMacher.group(6)); } // 匹配視頻分辨率等信息 if (videoStreamMacher.find()) { videoEncoder = videoStreamMacher.group(1); String s2 = videoStreamMacher.group(2); videoWidth = Integer.parseInt(videoStreamMacher.group(3)); videoHeight = Integer.parseInt(videoStreamMacher.group(4)); String s5 = videoStreamMacher.group(5); videoFramerate = Float.parseFloat(videoStreamMacher.group(6)); } // 匹配視頻中的音頻信息 if (videoMusicStreamMacher.find()) { musicFormat = videoMusicStreamMacher.group(1); // 提取音頻格式 //String s2 = videoMusicStreamMacher.group(2); samplerate = Long.parseLong(videoMusicStreamMacher.group(3)); // 提取采樣率 //String s4 = videoMusicStreamMacher.group(4); //String s5 = videoMusicStreamMacher.group(5); musicBitrate = Integer.parseInt(videoMusicStreamMacher.group(6)); // 提取比特率 } } catch (Exception e) { log.error("--- 解析視頻參數信息出錯! --- 錯誤信息: " + e.getMessage()); return null; } // 封裝視頻中的音頻信息 MusicMetaInfo musicMetaInfo = new MusicMetaInfo(); musicMetaInfo.setFormat(musicFormat); musicMetaInfo.setDuration(duration); musicMetaInfo.setBitRate(musicBitrate); musicMetaInfo.setSampleRate(samplerate); // 封裝視頻信息 VideoMetaInfo videoMetaInfo = new VideoMetaInfo(); videoMetaInfo.setFormat(videoFormat); videoMetaInfo.setSize(videoSize); videoMetaInfo.setBitRate(videoBitrate); videoMetaInfo.setDuration(duration); videoMetaInfo.setEncoder(videoEncoder); videoMetaInfo.setFrameRate(videoFramerate); videoMetaInfo.setHeight(videoHeight); videoMetaInfo.setWidth(videoWidth); videoMetaInfo.setMusicMetaInfo(musicMetaInfo); return videoMetaInfo; } /** * 獲取視頻的基本信息(從流中) * * @param inputStream 源視頻流路徑 * @return 視頻的基本信息,解碼失敗時返回null */ public static VideoMetaInfo getVideoMetaInfo(InputStream inputStream) { VideoMetaInfo videoInfo = new VideoMetaInfo(); try { File file = File.createTempFile("tmp", null); if (!file.exists()) { return null; } FileUtils.copyInputStreamToFile(inputStream, file); videoInfo = getVideoMetaInfo(file); file.deleteOnExit(); return videoInfo; } catch (Exception e) { log.error("--- 從流中獲取視頻基本信息出錯 --- 錯誤信息: " + e.getMessage()); return null; } } /** * 獲取音頻的基本信息(從文件中) * @param musicFile 音頻文件路徑 * @return 音頻的基本信息,解碼失敗時返回null */ public static MusicMetaInfo getMusicMetaInfo(File musicFile) { if (null == musicFile || !musicFile.exists()) { log.error("--- 無法獲取音頻信息,因為要解析的音頻文件為空 ---"); return null; } // 獲取音頻信息字符串,方便后續解析 String parseResult = getMetaInfoFromFFmpeg(musicFile); Long duration = 0L; // 音頻時長 Integer musicBitrate = 0; // 音頻碼率 Long samplerate = 0L; // 音頻采樣率 String musicFormat = ""; // 音頻格式 Long musicSize = musicFile.length(); // 音頻大小 Matcher durationMacher = durationPattern.matcher(parseResult); Matcher musicStreamMacher = musicStreamPattern.matcher(parseResult); try { // 匹配音頻播放時長等信息 if (durationMacher.find()) { long hours = (long)Integer.parseInt(durationMacher.group(1)); long minutes = (long)Integer.parseInt(durationMacher.group(2)); long seconds = (long)Integer.parseInt(durationMacher.group(3)); long dec = (long)Integer.parseInt(durationMacher.group(4)); duration = dec * 100L + seconds * 1000L + minutes * 60L * 1000L + hours * 60L * 60L * 1000L; //String startTime = durationMacher.group(5) + "ms"; musicBitrate = Integer.parseInt(durationMacher.group(6)); } // 匹配音頻采樣率等信息 if (musicStreamMacher.find()) { musicFormat = musicStreamMacher.group(1); // 提取音頻格式 //String s2 = videoMusicStreamMacher.group(2); samplerate = Long.parseLong(musicStreamMacher.group(3)); // 提取采樣率 //String s4 = videoMusicStreamMacher.group(4); //String s5 = videoMusicStreamMacher.group(5); musicBitrate = Integer.parseInt(musicStreamMacher.group(6)); // 提取比特率 } } catch (Exception e) { log.error("--- 解析音頻參數信息出錯! --- 錯誤信息: " + e.getMessage()); return null; } // 封裝視頻中的音頻信息 MusicMetaInfo musicMetaInfo = new MusicMetaInfo(); musicMetaInfo.setFormat(musicFormat); musicMetaInfo.setDuration(duration); musicMetaInfo.setBitRate(musicBitrate); musicMetaInfo.setSampleRate(samplerate); musicMetaInfo.setSize(musicSize); return musicMetaInfo; } /** * 獲取音頻的基本信息(從流中) * @param inputStream 源音樂流路徑 * @return 音頻基本信息,解碼出錯時返回null */ public static MusicMetaInfo getMusicMetaInfo(InputStream inputStream) { MusicMetaInfo musicMetaInfo = new MusicMetaInfo(); try { File file = File.createTempFile("tmp", null); if (!file.exists()) { return null; } FileUtils.copyInputStreamToFile(inputStream, file); musicMetaInfo = getMusicMetaInfo(file); file.deleteOnExit(); return musicMetaInfo; } catch (Exception e) { log.error("--- 從流中獲取音頻基本信息出錯 --- 錯誤信息: " + e.getMessage()); return null; } } /** * 獲取圖片的基本信息(從流中) * * @param inputStream 源圖片路徑 * @return 圖片的基本信息,獲取信息失敗時返回null */ public static ImageMetaInfo getImageInfo(InputStream inputStream) { BufferedImage image = null; ImageMetaInfo imageInfo = new ImageMetaInfo(); try { image = ImageIO.read(inputStream); imageInfo.setWidth(image.getWidth()); imageInfo.setHeight(image.getHeight()); imageInfo.setSize(Long.valueOf(String.valueOf(inputStream.available()))); return imageInfo; } catch (Exception e) { log.error("--- 獲取圖片的基本信息失敗 --- 錯誤信息: " + e.getMessage()); return null; } } /** * 獲取圖片的基本信息 (從文件中) * * @param imageFile 源圖片路徑 * @return 圖片的基本信息,獲取信息失敗時返回null */ public static ImageMetaInfo getImageInfo(File imageFile) { BufferedImage image = null; ImageMetaInfo imageInfo = new ImageMetaInfo(); try { if (null == imageFile || !imageFile.exists()) { return null; } image = ImageIO.read(imageFile); imageInfo.setWidth(image.getWidth()); imageInfo.setHeight(image.getHeight()); imageInfo.setSize(imageFile.length()); imageInfo.setFormat(getFormat(imageFile)); return imageInfo; } catch (Exception e) { log.error("--- 獲取圖片的基本信息失敗 --- 錯誤信息: " + e.getMessage()); return null; } } /** * 檢查文件類型是否是給定的類型 * @param inputFile 源文件 * @param givenFormat 指定的文件類型;例如:{"MP4", "AVI"} * @return */ public static boolean isGivenFormat(File inputFile, String[] givenFormat) { if (null == inputFile || !inputFile.exists()) { log.error("--- 無法檢查文件類型是否滿足要求,因為要檢查的文件不存在 --- 源文件: " + inputFile); return false; } if (null == givenFormat || givenFormat.length <= 0) { log.error("--- 無法檢查文件類型是否滿足要求,因為沒有指定的文件類型 ---"); return false; } String fomat = getFormat(inputFile); return isLegalFormat(fomat, givenFormat); } /** * 使用FFmpeg的"-i"命令來解析視頻信息 * @param inputFile 源媒體文件 * @return 解析后的結果字符串,解析失敗時為空 */ public static String getMetaInfoFromFFmpeg(File inputFile) { if (inputFile == null || !inputFile.exists()) { throw new RuntimeException("源媒體文件不存在,源媒體文件路徑: "); } List<String> commond = new ArrayList<String>(); commond.add("-i"); commond.add(inputFile.getAbsolutePath()); String executeResult = MediaUtil.executeCommand(commond); return executeResult; } /** * 檢測視頻格式是否合法 * @param format * @param formats * @return */ private static boolean isLegalFormat(String format, String formats[]) { for (String item : formats) { if (item.equals(StringUtils.upperCase(format))) { return true; } } return false; } /** * 創建gif * * @param image 多個jpg文件名(包含路徑) * @param outputPath 生成的gif文件名(包含路徑) * @param playTime 播放的延遲時間,可調整gif的播放速度 */ private static void createGifImage(String image[], String outputPath, int playTime) { if (null == outputPath) { throw new RuntimeException("轉換后的GIF路徑為空,請檢查轉換后的GIF存放路徑是否正確"); } try { AnimatedGifEncoder encoder = new AnimatedGifEncoder(); encoder.setRepeat(0); encoder.start(outputPath); BufferedImage src[] = new BufferedImage[image.length]; for (int i = 0; i < src.length; i++) { encoder.setDelay(playTime); // 設置播放的延遲時間 src[i] = ImageIO.read(new File(image[i])); // 讀入需要播放的jpg文件 encoder.addFrame(src[i]); // 添加到幀中 } encoder.finish(); } catch (Exception e) { log.error("--- 多張靜態圖轉換成動態GIF圖的過程出錯 --- 錯誤信息: " + e.getMessage()); } } /** * 獲取指定文件的后綴名 * @param file * @return */ private static String getFormat(File file) { String fileName = file.getName(); String format = fileName.substring(fileName.indexOf(".") + 1); return format; } /** * 在程序退出前結束已有的FFmpeg進程 */ private static class ProcessKiller extends Thread { private Process process; public ProcessKiller(Process process) { this.process = process; } @Override public void run() { this.process.destroy(); log.info("--- 已銷毀FFmpeg進程 --- 進程名: " + process.toString()); } } /** * 用于取出ffmpeg線程執行過程中產生的各種輸出和錯誤流的信息 */ static class PrintStream extends Thread { InputStream inputStream = null; BufferedReader bufferedReader = null; StringBuffer stringBuffer = new StringBuffer(); public PrintStream(InputStream inputStream) { this.inputStream = inputStream; } @Override public void run() { try { if (null == inputStream) { log.error("--- 讀取輸出流出錯!因為當前輸出流為空!---"); } bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); String line = null; while ((line = bufferedReader.readLine()) != null) { log.info(line); stringBuffer.append(line); } } catch (Exception e) { log.error("--- 讀取輸入流出錯了!--- 錯誤信息:" + e.getMessage()); } finally { try { if (null != bufferedReader) { bufferedReader.close(); } if (null != inputStream) { inputStream.close(); } } catch (IOException e) { log.error("--- 調用PrintStream讀取輸出流后,關閉流時出錯!---"); } } } } }
3.2.3 踩坑&填坑
1、在Linux等服務器上部署Java程序進行視頻壓縮時,多注意一下運行賬號的權限問題,有時候可能是由于運行程序沒有足夠的文件操作權限,導致壓縮過程失敗;
2、第一版程序上線后,偶爾會出現這樣的問題:
調用MediaUtil.java進行視頻壓縮過程中,整個程序突然“卡住”,后臺也沒有日志再打印出來,此時整個壓縮過程還沒有完成,像是線程突然阻塞住了;
經過多番查找,發現Java調用FFmpeg時,實際是在JVM里產生一個子進程來執行壓縮過程,這個子進程與JVM建立三個通道鏈接(包括標準輸入、標準輸出、標準錯誤流),在壓縮過程中,實際會不停地向標準輸出和錯誤流中寫入信息;
因為本地系統對標準輸出及錯誤流提供的緩沖區大小有限,當寫入標準輸出和錯誤流的信息填滿緩沖區時,執行壓縮的進程就會阻塞住;
所以在壓縮過程中,需要單獨創建兩個線程不停讀取標準輸出及錯誤流中的信息,防止整個壓縮進程阻塞;(參考MediaUtil.java中的 executeCommand() 方法中的 errorStream 和 inputStream 這兩個內部類實例的操作)
java基本數據類型有哪些
Java的基本數據類型分為:1、整數類型,用來表示整數的數據類型。2、浮點類型,用來表示小數的數據類型。3、字符類型,字符類型的關鍵字是“char”。4、布爾類型,是表示邏輯值的基本數據類型。
看完上述內容,你們對如何在Java使用FFmpeg處理視頻文件有進一步的了解嗎?如果還想了解更多知識或者相關內容,請關注億速云行業資訊頻道,感謝大家的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。