ffmpeg封装工具

格式转换/下载视频

ffmpeg -i index.m3u8 -c copy -y test.mp4
  • -i表示输入源,可以是本地路径,也可以是网络地址
  • -c表示复制视频编码
  • -y表示强制覆盖本地文件

使用显卡提速

通过添加 -vcodec h264_nvenc 来提速

Java工具类封装

封装了常见的方法:获取视频信息、截图、剪切、生成预览视频、格式转换等。
这里面用到了ws.schild.jave的一些方法,需要引入依赖:

<dependency>
   <groupId>ws.schildgroupId>
    <artifactId>jave-all-depsartifactId>
    <version>3.3.1version>
dependency>

FfmpegJob:

package mt.spring.tools.video.ffmpeg;

import lombok.extern.slf4j.Slf4j;
import ws.schild.jave.ConversionOutputAnalyzer;
import ws.schild.jave.EncoderException;
import ws.schild.jave.process.ProcessLocator;
import ws.schild.jave.process.ProcessWrapper;
import ws.schild.jave.process.ffmpeg.DefaultFFMPEGLocator;

import java.io.IOException;
import java.io.InputStreamReader;
import java.util.regex.Pattern;

/**
 * @Author Martin
 * @Date 2021/2/3
 */
@Slf4j
public class FfmpegJob {
	public final static ProcessLocator locator = new DefaultFFMPEGLocator();
	
	private static final Pattern SUCCESS_PATTERN = Pattern.compile("^\\s*video\\:\\S+\\s+audio\\:\\S+\\s+subtitle\\:\\S+\\s+global headers\\:\\S+.*$", Pattern.CASE_INSENSITIVE);
	
	public interface FfmpegWorker {
		void addArguments(ProcessWrapper ffmpeg);
	}
	
	public static void execute(FfmpegWorker ffmpegWorker) {
		ProcessWrapper ffmpeg = locator.createExecutor();
		ffmpegWorker.addArguments(ffmpeg);
		try {
			ffmpeg.execute();
			try (RBufferedReader reader = new RBufferedReader(new InputStreamReader(ffmpeg.getErrorStream()))) {
				String line;
				ConversionOutputAnalyzer outputAnalyzer = new ConversionOutputAnalyzer(0, null);
				while ((line = reader.readLine()) != null) {
					outputAnalyzer.analyzeNewLine(line);
				}
				if (outputAnalyzer.getLastWarning() != null) {
					String lastWarning = outputAnalyzer.getLastWarning();
					if (!SUCCESS_PATTERN.matcher(lastWarning).matches()) {
						throw new RuntimeException("No match for: " + SUCCESS_PATTERN + " in " + lastWarning);
					}
				}
			}
			int exitCode = ffmpeg.getProcessExitCode();
			if (exitCode != 0) {
				log.error("Process exit code: {}", exitCode);
				throw new RuntimeException("Exit code of ffmpeg encoding run is " + exitCode);
			}
		} catch (IOException | EncoderException e) {
			throw new RuntimeException(e);
		} finally {
			ffmpeg.destroy();
		}
	}
}

FfmpegUtils:

package mt.spring.tools.video;

import lombok.extern.slf4j.Slf4j;
import mt.spring.tools.video.ffmpeg.FfmpegJob;
import mt.utils.common.Assert;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import ws.schild.jave.EncoderException;
import ws.schild.jave.MultimediaObject;
import ws.schild.jave.ScreenExtractor;
import ws.schild.jave.info.MultimediaInfo;
import ws.schild.jave.info.VideoInfo;
import ws.schild.jave.info.VideoSize;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.DecimalFormat;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

/**
 * @Author Martin
 * @Date 2020/12/10
 */
@Slf4j
public class FfmpegUtils {
	
	/**
	 * 获取视频信息
	 *
	 * @param object   媒体文件
	 * @param timeout  超时
	 * @param timeUnit 超时单位
	 * @return 视频信息
	 * @throws Exception 异常
	 */
	public static mt.spring.tools.video.entity.VideoInfo getVideoInfo(MultimediaObject object, long timeout, TimeUnit timeUnit) throws Exception {
		ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
		try {
			Future<mt.spring.tools.video.entity.VideoInfo> future = singleThreadExecutor.submit(() -> {
				MultimediaInfo info = object.getInfo();
				VideoInfo video = info.getVideo();
				Assert.notNull(video, "video parsed error");
				mt.spring.tools.video.entity.VideoInfo videoInfo = new mt.spring.tools.video.entity.VideoInfo();
				long duration = info.getDuration();
				videoInfo.setDuring(duration);
				if (duration > 0) {
					videoInfo.setVideoLength(secondToTime(duration / 1000));
				}
				videoInfo.setFormat(info.getFormat());
				videoInfo.setWidth(video.getSize().getWidth());
				videoInfo.setHeight(video.getSize().getHeight());
				videoInfo.setBitRate(video.getBitRate());
				videoInfo.setFrameRate(video.getFrameRate());
				videoInfo.setDecoder(video.getDecoder());
				return videoInfo;
			});
			return future.get(timeout, timeUnit);
		} finally {
			singleThreadExecutor.shutdownNow();
		}
	}
	
	/**
	 * 获取视频信息
	 *
	 * @param source   源文件
	 * @param timeout  超时
	 * @param timeUnit 超时单位
	 * @return 视频信息
	 * @throws Exception 异常
	 */
	public static mt.spring.tools.video.entity.VideoInfo getVideoInfo(File source, long timeout, TimeUnit timeUnit) throws Exception {
		return getVideoInfo(new MultimediaObject(source), timeout, timeUnit);
	}
	
	/**
	 * 获取视频信息
	 *
	 * @param url      视频地址
	 * @param timeout  超时
	 * @param timeUnit 超时单位
	 * @return 视频信息
	 * @throws Exception 异常
	 */
	public static mt.spring.tools.video.entity.VideoInfo getVideoInfo(URL url, long timeout, TimeUnit timeUnit) throws Exception {
		return getVideoInfo(new MultimediaObject(url), timeout, timeUnit);
	}
	
	/**
	 * 获取视频长度
	 *
	 * @param url 视频地址
	 * @return 视频长度
	 * @throws MalformedURLException 异常
	 * @throws EncoderException      异常
	 */
	public static String getVideoLength(String url) throws MalformedURLException, EncoderException {
		URL url1 = new URL(url);
		MultimediaObject object = new MultimediaObject(url1);
		MultimediaInfo info = object.getInfo();
		long duration = info.getDuration();
		return secondToTime(duration / 1000);
	}
	
	/**
	 * 获取视频长度
	 *
	 * @param file 文件
	 * @return 视频长度
	 * @throws MalformedURLException 异常
	 * @throws EncoderException      异常
	 */
	public static String getVideoLength(File file) throws MalformedURLException, EncoderException {
		MultimediaObject object = new MultimediaObject(file);
		MultimediaInfo info = object.getInfo();
		long duration = info.getDuration();
		return secondToTime(duration / 1000);
	}
	
	/**
	 * 将时间转换成00:00格式
	 *
	 * @param second 秒
	 * @return 转换结果
	 */
	public static String secondToTime(long second) {
		DecimalFormat format = new DecimalFormat("00");
		long days = second / 86400;            //转换天数
		second = second % 86400;            //剩余秒数
		long hours = second / 3600;            //转换小时
		second = second % 3600;                //剩余秒数
		long minutes = second / 60;            //转换分钟
		second = second % 60;                //剩余秒数
		String dd = format.format(days);
		String HH = format.format(hours);
		String mm = format.format(minutes);
		String ss = format.format(second);
		StringBuilder result = new StringBuilder();
		if (days > 0) {
			result.append(":").append(dd);
		}
		if (hours > 0) {
			result.append(":").append(HH);
		}
		result.append(":").append(mm);
		result.append(":").append(ss);
		return result.substring(1, result.length());
	}
	
	/**
	 * 截图
	 *
	 * @param srcFile 源文件
	 * @param desFile 目标文件
	 * @param width   宽度
	 * @param seconds 第几秒
	 * @throws Exception 异常
	 */
	public static void screenShot(File srcFile, File desFile, int width, int seconds) throws Exception {
		screenShot(new MultimediaObject(srcFile), desFile, width, seconds, 60, TimeUnit.SECONDS);
	}
	
	/**
	 * 截图
	 *
	 * @param url     视频地址
	 * @param desFile 目标文件
	 * @param width   宽度
	 * @param seconds 第几秒
	 * @throws Exception 异常
	 */
	public static void screenShot(URL url, File desFile, int width, int seconds) throws Exception {
		screenShot(new MultimediaObject(url), desFile, width, seconds, 60, TimeUnit.SECONDS);
	}
	
	/**
	 * 截图
	 *
	 * @param object   媒体文件
	 * @param desFile  目标文件
	 * @param width    宽度
	 * @param seconds  第几秒
	 * @param timeout  超时
	 * @param timeUnit 超时单位
	 * @throws Exception 异常
	 */
	public static void screenShot(MultimediaObject object, File desFile, int width, final int seconds, long timeout, TimeUnit timeUnit) throws Exception {
		if (desFile.exists()) {
			return;
		}
		ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
		try {
			Future<?> submit = singleThreadExecutor.submit(() -> {
				try {
					File parentFile = desFile.getParentFile();
					if (!parentFile.exists()) {
						parentFile.mkdirs();
					}
					double maxSeconds = 0;
					int s = seconds;
					if (s > 0) {
						try {
							long duration = object.getInfo().getDuration();
							maxSeconds = Math.floor(duration * 1.0 / 1000) - 5;
							if (maxSeconds < 0) {
								maxSeconds = 0;
							}
						} catch (Exception ignored) {
							s = 0;
						}
					}
					ScreenExtractor screenExtractor = new ScreenExtractor();
					VideoSize size = object.getInfo().getVideo().getSize();
					int height = (int) Math.ceil(width * size.getHeight() * 1.0 / size.getWidth());
					screenExtractor.render(object, width, height, (int) Math.min(maxSeconds, s), desFile, 1);
				} catch (Exception e) {
					log.error(e.getMessage(), e);
					throw new RuntimeException(e);
				}
			});
			submit.get(timeout, timeUnit);
		} finally {
			singleThreadExecutor.shutdownNow();
		}
	}
	
	/**
	 * 连续截图20分钟
	 *
	 * @param srcFile 源文件
	 * @param dstPath 目标目录
	 */
	public static void screenshotsTwentyMinutes(@NotNull File srcFile, @NotNull File dstPath) {
		screenshots(srcFile, dstPath, 0.01667, "00:00", "20:00", 400);
	}
	
	/**
	 * 连续截图
	 * ffmpeg -ss 00:00 -i 5.mp4 -f image2 -r 0.01667 -t 20:00 -filter:v scale=400:-1 thumb/%3d.jpg
	 *
	 * @param srcFile    源文件
	 * @param dstPath    目标目录
	 * @param rate       每秒播放的帧  1 = 间隔秒数 * rate,例如5秒截图一次,那就是rate = 0.2
	 * @param startTime  开始时间,格式xx:xx,例如00:00
	 * @param duringRime 持续时间,格式xx:xx,例如20:00
	 * @param width      宽度
	 */
	public static void screenshots(@NotNull File srcFile, @NotNull File dstPath, double rate, @NotNull String startTime, @NotNull String duringRime, int width) {
		dstPath.mkdirs();
		FfmpegJob.execute(ffmpeg -> {
			ffmpeg.addArgument("-ss");
			ffmpeg.addArgument(startTime);
			ffmpeg.addArgument("-i");
			ffmpeg.addArgument(srcFile.getAbsolutePath());
			ffmpeg.addArgument("-f");
			ffmpeg.addArgument("image2");
			ffmpeg.addArgument("-r");
			ffmpeg.addArgument(rate + "");
			ffmpeg.addArgument("-t");
			ffmpeg.addArgument(duringRime);
			ffmpeg.addArgument("-filter:v");
			ffmpeg.addArgument("scale=" + width + ":-1");
			ffmpeg.addArgument(dstPath.getAbsolutePath() + "/%3d.jpg");
		});
	}
	
	/**
	 * 压缩图片
	 *
	 * @param srcFile 源文件
	 * @param desFile 目标文件
	 * @param width   宽度
	 * @throws Exception 异常
	 */
	public static void compressImage(File srcFile, File desFile, int width) throws Exception {
		screenShot(srcFile, desFile, width, 0);
	}
	
	/**
	 * 剪切视频
	 * 命令:ffmpeg -i 1.mp4 -ss 00:00:00 -to 00:00:20 -y -f mp4 -vcodec copy -acodec copy -q:v 1 thumb.mp4
	 *
	 * @param srcFile 源文件
	 * @param desFile 目标文件
	 * @param from    从,例:00:00:00
	 * @param to      到,例:00:00:20
	 */
	public static void cutVideo(@NotNull File srcFile, @NotNull File desFile, @NotNull String from, @NotNull String to, @Nullable String vCodec) {
		if (StringUtils.isBlank(vCodec)) {
			vCodec = "copy";
		}
		String finalVCodec = vCodec;
		FfmpegJob.execute(ffmpeg -> {
			ffmpeg.addArgument("-i");
			ffmpeg.addArgument(srcFile.getAbsolutePath());
			ffmpeg.addArgument("-ss");
			ffmpeg.addArgument(from);
			ffmpeg.addArgument("-to");
			ffmpeg.addArgument(to);
			ffmpeg.addArgument("-y");
			ffmpeg.addArgument("-f");
			ffmpeg.addArgument("mp4");
			ffmpeg.addArgument("-vcodec");
			ffmpeg.addArgument(finalVCodec);
			ffmpeg.addArgument("-acodec");
			ffmpeg.addArgument("copy");
			ffmpeg.addArgument("-q:v");
			ffmpeg.addArgument("1");
			ffmpeg.addArgument(desFile.getAbsolutePath());
		});
	}
	
	/**
	 * 生成预览视频
	 * 命令:ffmpeg -i 1.mp4 -vf "select='lte(mod(t, 122),1)',scale=400:-2,setpts=N/FRAME_RATE/TB" -an -y preview.mp4
	 *
	 * @param srcFile  源文件
	 * @param dstFile  目标文件,例如:preview.mp4
	 * @param segments 分段,每段1秒
	 * @param width    宽度
	 * @return 是否生成
	 * @throws Exception 异常
	 */
	public static boolean generatePreviewVideo(@NotNull File srcFile, @NotNull File dstFile, int segments, int width, @Nullable String vCodec) throws Exception {
		return generatePreviewVideo(srcFile, dstFile, segments, width, -2, vCodec);
	}
	
	/**
	 * 生成预览视频
	 * 命令:ffmpeg -i 1.mp4 -vf "select='lte(mod(t, 122),1)',scale=400:-2,setpts=N/FRAME_RATE/TB" -an -y preview.mp4
	 * F
	 *
	 * @param srcFile  源文件
	 * @param dstFile  目标文件,例如:preview.mp4
	 * @param segments 分段,每段1秒
	 * @param width    宽度
	 * @param height   高度
	 * @return 是否生成
	 * @throws Exception 异常
	 */
	public static boolean generatePreviewVideo(@NotNull File srcFile, @NotNull File dstFile, int segments, int width, int height, @Nullable String vCodec) throws Exception {
		mt.spring.tools.video.entity.VideoInfo videoInfo = getVideoInfo(srcFile, 1, TimeUnit.MINUTES);
		long during = videoInfo.getDuring();
		long second = during / 1000 / segments;
		if (second > segments) {
			FfmpegJob.execute(ffmpeg -> {
				ffmpeg.addArgument("-i");
				ffmpeg.addArgument(srcFile.getAbsolutePath());
				if (StringUtils.isNotBlank(vCodec)) {
					ffmpeg.addArgument("-vcodec");
					ffmpeg.addArgument(vCodec);
				}
				ffmpeg.addArgument("-vf");
				ffmpeg.addArgument("\"select='lte(mod(t, " + second + "),1)',scale=" + width + ":" + height + ",setpts=N/FRAME_RATE/TB\"");
				ffmpeg.addArgument("-an");
				ffmpeg.addArgument("-y");
				ffmpeg.addArgument(dstFile.getAbsolutePath());
			});
			return true;
		}
		log.info("视频长度小于分片长度,不能生成预览视频,segments={}", segments);
		return false;
	}
	
	/**
	 * 转换格式
	 * ffmpeg -i 1.wmv -y 1.mp4
	 *
	 * @param srcFile 源文件
	 * @param dstFile 目标文件
	 */
	public static void convert(@NotNull File srcFile, @NotNull File dstFile, @Nullable String vCodec) {
		FfmpegJob.execute(ffmpeg -> {
			ffmpeg.addArgument("-i");
			ffmpeg.addArgument(srcFile.getAbsolutePath());
			if (StringUtils.isNotBlank(vCodec)) {
				ffmpeg.addArgument("-vcodec");
				ffmpeg.addArgument(vCodec);
			}
			ffmpeg.addArgument("-vf");
			ffmpeg.addArgument("scale=iw:-2");
			ffmpeg.addArgument("-y");
			ffmpeg.addArgument(dstFile.getAbsolutePath());
		});
	}
}

m3u8相关的封装

package mt.spring.tools.video;

import lombok.extern.slf4j.Slf4j;
import mt.spring.tools.video.ffmpeg.FfmpegJob;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.Nullable;
import ws.schild.jave.EncoderException;
import ws.schild.jave.MultimediaObject;

import java.io.File;
import java.util.concurrent.TimeUnit;

/**
 * @Author Martin
 * @Date 2021/2/3
 */
@Slf4j
public class HlsUtils {
	public static final int MB = 1024 * 1024;
	
	/**
	 * 将源文件转换成ts格式,并且分割成多个ts文件
	 *
	 * @param source            源文件
	 * @param target            目标文件 例:index.m3u8
	 * @param segmentMB         每段ts文件大小
	 * @param minSegmentSeconds 分段最小视频长度
	 */
	public static void convertToHlsBySize(File source, File target, int segmentMB, @Nullable Integer minSegmentSeconds, @Nullable String vCodec) {
		if (minSegmentSeconds == null) {
			minSegmentSeconds = 15;
		}
		//每个分片按10MB计算,但时长不能小于5s
		MultimediaObject object = new MultimediaObject(source);
		try {
			long duration = object.getInfo().getDuration();
			long length = source.length();
			double sizeMb = Math.ceil(length * 1.0 / MB);
			double perSecondMb = sizeMb / TimeUnit.MILLISECONDS.toSeconds(duration);
			int segmentSeconds = (int) Math.ceil(segmentMB * 1.0 / perSecondMb);
			if (segmentSeconds < minSegmentSeconds) {
				segmentSeconds = minSegmentSeconds;
			}
			convertToHlsBySeconds(source, target, segmentSeconds, vCodec);
		} catch (EncoderException e) {
			throw new RuntimeException(e);
		}
	}
	
	/**
	 * 将源文件转换成ts格式,并且分割成多个ts文件
	 *
	 * @param source         源文件
	 * @param target         目标文件 例:index.m3u8
	 * @param segmentSeconds 每段视频长度
	 * @param vCodec         视频编码
	 */
	public static void convertToHlsBySeconds(File source, File target, int segmentSeconds, @Nullable String vCodec) {
		File tsFile = new File(target.getParentFile(), target.getName() + ".ts");
		convertTs(source, tsFile, vCodec);
		splitTs(tsFile, target, segmentSeconds);
		tsFile.delete();
	}
	
	/**
	 * 将源文件转换成.ts格式
	 * 命令:ffmpeg -y -i "IMG_8308.MOV"  -vcodec copy -acodec copy -vbsf h264_mp4toannexb test.ts
	 *
	 * @param source 源文件,例如:IMG_8308.MOV
	 * @param target 目标文件,例如:test.ts
	 * @param vCodec 视频编码
	 */
	public static void convertTs(File source, File target, @Nullable String vCodec) {
		log.info("转换为ts文件:{}", source);
		File parentFile = target.getParentFile();
		if (!parentFile.exists()) {
			parentFile.mkdirs();
		}
		try {
			FfmpegJob.execute(ffmpeg -> {
				ffmpeg.addArgument("-y");
				ffmpeg.addArgument("-i");
				ffmpeg.addArgument(source.getAbsolutePath());
				ffmpeg.addArgument("-vf");
				ffmpeg.addArgument("scale=iw:-2");
				ffmpeg.addArgument("-vcodec");
				if (StringUtils.isNotBlank(vCodec)) {
					ffmpeg.addArgument(vCodec);
				} else {
					ffmpeg.addArgument("copy");
				}
				ffmpeg.addArgument("-acodec");
				ffmpeg.addArgument("copy");
				ffmpeg.addArgument("-vbsf");
				ffmpeg.addArgument("h264_mp4toannexb");
				ffmpeg.addArgument(target.getAbsolutePath());
			});
		} catch (Exception e) {
			FfmpegJob.execute(ffmpeg -> {
				ffmpeg.addArgument("-y");
				ffmpeg.addArgument("-i");
				ffmpeg.addArgument(source.getAbsolutePath());
				ffmpeg.addArgument("-vf");
				ffmpeg.addArgument("scale=iw:-2");
				ffmpeg.addArgument("-vcodec");
				ffmpeg.addArgument("h264");
				ffmpeg.addArgument("-acodec");
				ffmpeg.addArgument("copy");
				ffmpeg.addArgument("-vbsf");
				ffmpeg.addArgument("h264_mp4toannexb");
				ffmpeg.addArgument(target.getAbsolutePath());
			});
		}
	}
	
	/**
	 * 将源文件分割成多个ts文件
	 * 命令:ffmpeg -i test.ts -c copy -map 0 -f segment -segment_list test.m3u8 -segment_time 60 "60s_%3d.ts"
	 *
	 * @param source         源文件
	 * @param target         目标文件 例:index.m3u8
	 * @param segmentSeconds 每段视频长度
	 */
	public static void splitTs(File source, File target, @Nullable Integer segmentSeconds) {
		log.info("分割ts文件:{}", source);
		target.getParentFile().mkdirs();
		if (segmentSeconds == null) {
			segmentSeconds = 30;
		}
		Integer finalSegmentSeconds = segmentSeconds;
		FfmpegJob.execute(ffmpeg -> {
			ffmpeg.addArgument("-i");
			ffmpeg.addArgument(source.getAbsolutePath());
			ffmpeg.addArgument("-c");
			ffmpeg.addArgument("copy");
			ffmpeg.addArgument("-map");
			ffmpeg.addArgument("0");
			ffmpeg.addArgument("-f");
			ffmpeg.addArgument("segment");
			ffmpeg.addArgument("-segment_list");
			ffmpeg.addArgument(target.getAbsolutePath());
			ffmpeg.addArgument("-segment_time");
			ffmpeg.addArgument(finalSegmentSeconds + "");
			ffmpeg.addArgument(new File(target.getParentFile(), "segment_%3d.ts").getAbsolutePath());
		});
	}
	
}

完整的代码

完整的代码我放到github代码库了:https://github.com/668mt/mt-spring-web.git
视频处理相关的代码在mt-tools/video-tools

你可能感兴趣的:(ffmpeg,java,开发语言)