天天看點

Java使用FFmpeg處理視訊檔案指南

Java使用FFmpeg處理視訊檔案指南

本文主要講述如何使用Java + FFmpeg實作對視訊檔案的資訊提取、碼率壓縮、分辨率轉換等功能;

之前在網上浏覽了一大圈Java使用FFmpeg處理音視訊的文章,大多都講的比較簡單,樓主在實操過程中踩了很多坑也填了很多坑,希望這份詳細的踩坑&填坑指南能幫助到大家;

1. 什麼是FFmpeg

點我了解

2. 開發前準備

在使用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: h264 (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: h264【視訊編碼格式】 (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

    這兩個内部類執行個體的操作)

3.3 在CentOS伺服器安裝FFmpeg指南

因項目最後部署在CentOS伺服器上,需提前在伺服器上安裝好FFmpeg程式,這過程中也踩了不少坑,針對此寫了另一篇總結文章,參考這裡 點我哦

4. 源碼下載下傳

這裡提供兩種版本的源碼供大家下載下傳參考:

  • 引入封裝了FFmpeg的開源架構Jave.jar的版本 點我下載下傳
  • 在系統中手動安裝FFmpeg的版本 點我下載下傳

有問題可以在評論區留言,歡迎大家一起交流讨論 _ ~