Java使用FFmpeg处理视频文件指南

您所在的位置:网站首页 java调用工具类 Java使用FFmpeg处理视频文件指南

Java使用FFmpeg处理视频文件指南

2024-06-02 15:13| 来源: 网络整理| 查看: 265

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坐标如下:

org.ffmpeg sdk 1.0.2 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 使用前需要注意的几点

指定正确的FFmpeg程序执行路径 MacOS安装好FFmpeg后,可以在控制台中通过which ffmpeg命令获取FFmpeg程序的执行路径,在调用MediaUtil.java前先通过其 setFFmpegPath() 方法设置好FFmpeg程序在系统中的执行路径,然后才能顺利调用到FFmpeg去解析音视频; Windows系统下该路径理论上应设置为:FFmpeg可执行程序在系统中的绝对路径(实际情况有待大家补充)指定解析音视频信息时需要的正则表达式 因项目需要解析后缀格式为 .MP4 .WMV .AAC 的视频和音频文件,所以我研究了JAVE.jar底层调用FFmpeg时的解析逻辑后,在MediaUtil.java中设置好了匹配这三种格式的正则表达式供解析时使用(参考程序中的 durationRegex videoStreamRegex musicStreamRegex 这三个表达式值);注意:如果你需要解析其他后缀格式如 .MKV .MP3 这样的媒体文件时,你很可能需要根据实际情况修改durationRegex videoStreamRegex musicStreamRegex 这三个正则表达式的值,否则可能无法解析出正确的信息;程序中的很多默认值你可以根据实际需要修改,比如视频帧抽取的默认宽度或高度值、时长等等;

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 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 commonds) { if (CollectionUtils.isEmpty(commonds)) { log.error("--- 指令执行失败,因为要执行的FFmpeg指令为空! ---"); return null; } LinkedList 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 commond = new ArrayList(); 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


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3