赞
踩
ffmpeg -i index.m3u8 -c copy -y test.mp4
通过添加 -vcodec h264_nvenc 来提速
封装了常见的方法:获取视频信息、截图、剪切、生成预览视频、格式转换等。
这里面用到了ws.schild.jave
的一些方法,需要引入依赖:
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-all-deps</artifactId>
<version>3.3.1</version>
</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()); }); } }
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
下
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。