https://www.cnblogs.com/Dreamer-1/p/10394011.html

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();// 创建临时文件存储多张静态图用于生成gifString 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);// 生成gifString 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;}@Overridepublic 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;}@Overridepublic 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的版本 点我下载

有问题可以在评论区留言,欢迎大家一起交流讨论 _ ~

Java使用FFmpeg处理视频文件指南相关推荐

  1. Java使用FFmpeg处理视频文件的方法教程

    这篇文章主要给大家介绍了关于Java使用FFmpeg处理视频文件的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧 前言 本文主要 ...

  2. mysql+视频文件转成流_详解java调用ffmpeg转换视频格式为flv

    详解java调用ffmpeg转换视频格式为flv 注意:下面的程序是在Linux下运行的,如果在windows下rmvb转换成avi会出现问题,想成功需要下载下个drv43260.dll东西放到C:W ...

  3. ffmpeg入门及java操作ffmpeg对视频进行处理

    一.ffmpeg 1.简介 FFmpeg是一个开源免费跨平台的视频和音频流方案,属于自由软件,采用LGPL或GPL许可证(依据你选择的组件).它提供了录制.转换以及流化音视频的完整解决方案.它包含了非 ...

  4. 使用ffmpeg进行视频文件转换成FLV整理

    本系列文章导航 Windows下FFmpeg快速入门 ffmpeg参数解释 mencoder和ffmpeg参数详解(Java处理视频) Java 生成视频缩略图(ffmpeg) 使用ffmpeg进行视 ...

  5. Java通过FFMPEG获取视频时长

    2019独角兽企业重金招聘Python工程师标准>>> Java通过FFMPEG获取视频时长 详见https://www.yz1618.cn/view/19 转载于:https:// ...

  6. ffmpeg合并视频文件

    ffmpeg合并视频文件 创建列表文件filelist.txt,将需要合并的文件按顺序加入,内容如下所示: ffconcat version 1.0 file path/test_1.mp4 file ...

  7. ffmpeg解码视频文件并播放

    最近学习了一下如何使用ffmpeg解码音视频,网上的教程挺多但是也挺杂的,搞了好几天,明白了ffmpeg解码音视频的大体流程,这里记录一下ffmpeg解码视频并播放音视频的例子,但并没有做音频.视频播 ...

  8. ffmpeg音视频文件音视频流抽取,初步尝试人声分离

    文章目录 ffmpeg抽取音视频文件中的音频流 音频流类型 AAC与m4a的区别 AAC与mp3的区别 用ffmpeg查看视频的信息 用ffmpeg抽取AAC音频流 从AAC文件中获取音轨 音轨是什么 ...

  9. Java调用ffmpeg进行视频.H264抽帧,并保存为图片

    Java调用ffmpeg进行视频.H264抽帧,并保存为图片 1. 需求 2. 解决 3. 源码 参考 1. 需求 对视频 D:\data\01-test.H264进行抽帧并保存为图片,图片命名为1. ...

最新文章

  1. 《Microduino实战》——第2章 Microduino
  2. webservice 简介 跨编程语言 跨操作系统 远程调用技术
  3. duilib拖动控制功能的实现(源代码)
  4. 畅通工程再续_MST(hdu 1875)
  5. boost::geometry::strategy::distance::haversine用法的测试程序
  6. java实现人脸识别源码【含测试效果图】——Dao层(IUserDao)
  7. 字段类型:mysql中int(3)与int(11)有什么区别吗?优化数据库字段占据磁盘的大小
  8. 分账汇总少了一笔 和 对账和商户汇总比基础表少了一笔 问题处理方式
  9. Onedark风格配色方案
  10. lol新加坡服务器怎么修复,英雄联盟差点被新加坡服翻译“毁了”,7张图片看到想要吐血...
  11. 集合源码(一)之hashMap、ArrayList
  12. spring boot 打war包部署,打jar包
  13. Layer success 层弹出后的成功回调方法
  14. Hadoop学习之hdfs集群搭建详解
  15. 把统计代码改成“量子统计”了
  16. SCI分区:JCR分区和中科院分区 的差别
  17. python制作日历并保存成excel_python台历代码--涉及知识点为Excel表格合并等操作
  18. 收藏——硬币(五大天王和四小龙)
  19. 飞机的各参数指标matlab,通达信飞机起飞及选股指标公式
  20. 银联手机网页如何调用云闪付(银联钱包)

热门文章

  1. memtest86+
  2. 单因素方差分析——R语言实战
  3. 必须是可修改的左值_成考出分后,志愿还能修改吗?
  4. 从黑白mask图提取模板(np.where实现)
  5. c语言婚姻管理程序,C语言实现婚姻匹配有关问题
  6. 在香港的日子(二)--第一周生活
  7. win2003 玩魔兽争霸的方法.
  8. CS229 Fall 2020 Python Tutorial
  9. 服务器支持缓存,怎么部署缓存服务器
  10. win10里的hosts文件不在了,也不能修改