当前位置: 移动技术网 > IT编程>开发语言>Java > Java使用FFmpeg处理视频文件的方法教程

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

2019年07月19日  | 移动技术网IT编程  | 我要评论
前言 本文主要讲述如何使用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压缩视频时常用的几种压缩速率值;

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的版本

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对移动技术网的支持。

如您对本文有疑问或者有任何想说的,请 点击进行留言回复,万千网友为您解惑!

相关文章:

验证码:
移动技术网