|
@@ -0,0 +1,1617 @@
|
|
|
|
|
+package com.tzld.supply.service.ffmpeg.impl;
|
|
|
|
|
+
|
|
|
|
|
+import cn.hutool.core.collection.CollectionUtil;
|
|
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
|
|
+import com.stuuudy.commons.external.filestorage.enums.EnumPublicBuckets;
|
|
|
|
|
+import com.tzld.supply.common.enums.ExceptionEnum;
|
|
|
|
|
+import com.tzld.supply.common.exception.CommonException;
|
|
|
|
|
+import com.tzld.supply.model.dto.ali.AliVoiceResultSentenceData;
|
|
|
|
|
+import com.tzld.supply.model.param.FFmpeg.*;
|
|
|
|
|
+import com.tzld.supply.service.ffmpeg.FFmpegService;
|
|
|
|
|
+import com.tzld.supply.service.image.ImageService;
|
|
|
|
|
+import com.tzld.supply.service.image.exception.DownloadImageError;
|
|
|
|
|
+import com.tzld.supply.service.tools.ToolsAudioTransService;
|
|
|
|
|
+import com.tzld.supply.util.*;
|
|
|
|
|
+import com.tzld.supply.util.ffmpeg.FFmpegUtil;
|
|
|
|
|
+import com.tzld.supply.util.http.HttpClientUtils;
|
|
|
|
|
+import com.tzld.supply.util.http.HttpResponseContent;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import okhttp3.Call;
|
|
|
|
|
+import okhttp3.OkHttpClient;
|
|
|
|
|
+import okhttp3.Request;
|
|
|
|
|
+import okhttp3.Response;
|
|
|
|
|
+import org.apache.http.Header;
|
|
|
|
|
+import org.apache.http.util.TextUtils;
|
|
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+import org.springframework.util.StringUtils;
|
|
|
|
|
+
|
|
|
|
|
+import javax.imageio.ImageIO;
|
|
|
|
|
+import java.awt.*;
|
|
|
|
|
+import java.awt.image.BufferedImage;
|
|
|
|
|
+import java.io.*;
|
|
|
|
|
+import java.math.BigDecimal;
|
|
|
|
|
+import java.math.RoundingMode;
|
|
|
|
|
+import java.nio.file.Files;
|
|
|
|
|
+import java.nio.file.Path;
|
|
|
|
|
+import java.nio.file.Paths;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.*;
|
|
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
|
|
+import java.util.stream.Collectors;
|
|
|
|
|
+
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+@Service
|
|
|
|
|
+public class FFmpegServiceImpl implements FFmpegService {
|
|
|
|
|
+
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ ToolsAudioTransService toolsAudioTransService;
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ ImageService imageService;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${ffmpeg.api.url:http://ffmpeg.aiddit.com}")
|
|
|
|
|
+ private String ffmpegApiUrl;
|
|
|
|
|
+
|
|
|
|
|
+ public static OkHttpClient client = new OkHttpClient().newBuilder()
|
|
|
|
|
+ .connectTimeout(15, TimeUnit.SECONDS)
|
|
|
|
|
+ .readTimeout(5, TimeUnit.MINUTES)
|
|
|
|
|
+ .writeTimeout(5, TimeUnit.MINUTES)
|
|
|
|
|
+ .retryOnConnectionFailure(true)
|
|
|
|
|
+ .build();
|
|
|
|
|
+
|
|
|
|
|
+ private void downloadFile(String url, String filePath) throws Exception {
|
|
|
|
|
+ for (int i = 0; i < 3; i++) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ Request request = new Request.Builder()
|
|
|
|
|
+ .url(url)
|
|
|
|
|
+ .build();
|
|
|
|
|
+
|
|
|
|
|
+ Call call = client.newCall(request);
|
|
|
|
|
+ Response response = call.execute();
|
|
|
|
|
+ InputStream is = response.body().byteStream();
|
|
|
|
|
+
|
|
|
|
|
+ Path path = Paths.get(filePath);
|
|
|
|
|
+ Path parentDir = path.getParent();
|
|
|
|
|
+ if (!Files.exists(parentDir)) {
|
|
|
|
|
+ Files.createDirectories(parentDir);
|
|
|
|
|
+ }
|
|
|
|
|
+ File file = new File(filePath);
|
|
|
|
|
+ FileOutputStream outputStream = new FileOutputStream(file);
|
|
|
|
|
+ byte[] buffer = new byte[4096];
|
|
|
|
|
+ int bytesRead;
|
|
|
|
|
+ while ((bytesRead = is.read(buffer)) != -1) {
|
|
|
|
|
+ outputStream.write(buffer, 0, bytesRead);
|
|
|
|
|
+ }
|
|
|
|
|
+ is.close();
|
|
|
|
|
+ break;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ if (i == 2) {
|
|
|
|
|
+ throw e;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ log.error("downloadFile error,{}", url, e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public String getFFmpegApiBaseUrl() {
|
|
|
|
|
+ List<String> apiUrlList = new ArrayList<>();
|
|
|
|
|
+ for (String apiUrl : ffmpegApiUrl.split(",")) {
|
|
|
|
|
+ apiUrlList.add(apiUrl);
|
|
|
|
|
+ }
|
|
|
|
|
+ String result = apiUrlList.get(new Random().nextInt(apiUrlList.size()));
|
|
|
|
|
+ log.info("getFFmpegApiBaseUrl,result:{}", result);
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public String mergeBgVideo(MergeBgVideoParam param) {
|
|
|
|
|
+ Set<String> allLocalFilePathList = new HashSet<>();
|
|
|
|
|
+ String mainVideoSuffix = ".mp4";
|
|
|
|
|
+ if (param.getMainVideo().contains(".mov")) {
|
|
|
|
|
+ mainVideoSuffix = ".mov";
|
|
|
|
|
+ }
|
|
|
|
|
+ String mainVideoFilePath = "/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + mainVideoSuffix;
|
|
|
|
|
+ String bgMediaFilePath = "/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4";
|
|
|
|
|
+ if ("image".equals(param.getType())) {
|
|
|
|
|
+ bgMediaFilePath = "/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".png";
|
|
|
|
|
+ }
|
|
|
|
|
+ String adjustBgMediaFilePath = bgMediaFilePath;
|
|
|
|
|
+ String outputFilePath = "/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4";
|
|
|
|
|
+ String ossKey = "ffmpeg/mergeBgVideo/" + BaseUtils.getUUIDStr() + ".mp4";
|
|
|
|
|
+
|
|
|
|
|
+ allLocalFilePathList.add(mainVideoFilePath);
|
|
|
|
|
+ allLocalFilePathList.add(bgMediaFilePath);
|
|
|
|
|
+ allLocalFilePathList.add(adjustBgMediaFilePath);
|
|
|
|
|
+ allLocalFilePathList.add(outputFilePath);
|
|
|
|
|
+ try {
|
|
|
|
|
+ downloadFile(CdnUtil.getOssHttpUrl(param.getMainVideo()), mainVideoFilePath);
|
|
|
|
|
+ downloadFile(CdnUtil.getOssHttpUrl(param.getBgMedia()), bgMediaFilePath);
|
|
|
|
|
+ // 调整背景视频/图片的尺寸、时长
|
|
|
|
|
+ adjustBgMediaFilePath = adjustBgMediaSizeAndDuration(mainVideoFilePath, bgMediaFilePath, param.getType(), allLocalFilePathList);
|
|
|
|
|
+ FFmpegUtil.mergeBgVideo(mainVideoFilePath, adjustBgMediaFilePath, outputFilePath);
|
|
|
|
|
+ // 上传到OSS
|
|
|
|
|
+ InputStream inputStream = new FileInputStream(outputFilePath);
|
|
|
|
|
+ AliOssFileTool.saveInPublic(EnumPublicBuckets.PUBBUCKET.getBucketName(), ossKey, inputStream, "video/mp4");
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ ossKey = null;
|
|
|
|
|
+ log.error("putObject error,{}", outputFilePath, e);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (CollectionUtil.isNotEmpty(allLocalFilePathList)) {
|
|
|
|
|
+ for (String filePath : allLocalFilePathList) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ File file = new File(filePath);
|
|
|
|
|
+ file.delete();
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("delete file error,", filePath);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return CdnUtil.getOssHttpUrl(ossKey);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String adjustBgMediaSizeAndDuration(String mainVideoFilePath, String bgMediaFilePath, String type,
|
|
|
|
|
+ Set<String> allLocalFilePathList) throws IOException {
|
|
|
|
|
+ // 背景视频/图片 按短边缩放再居中裁剪
|
|
|
|
|
+ FFmpegUtil.MediaInfo mainVideoMediaInfo = FFmpegUtil.getMediaInfo(mainVideoFilePath);
|
|
|
|
|
+ int width = mainVideoMediaInfo.getWidth();
|
|
|
|
|
+ int height = mainVideoMediaInfo.getHeight();
|
|
|
|
|
+ int duration = mainVideoMediaInfo.getDuration();
|
|
|
|
|
+ if ("image".equals(type)) {
|
|
|
|
|
+ BufferedImage bgImage = ImageIO.read(new File(bgMediaFilePath));
|
|
|
|
|
+ if (bgImage.getWidth() == width && bgImage.getHeight() == height) {
|
|
|
|
|
+ return bgMediaFilePath;
|
|
|
|
|
+ }
|
|
|
|
|
+ String resizeImageFilePath = "/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".png";
|
|
|
|
|
+ allLocalFilePathList.add(resizeImageFilePath);
|
|
|
|
|
+ BigDecimal widthRate = new BigDecimal(width).divide(new BigDecimal(bgImage.getWidth()), 2,
|
|
|
|
|
+ RoundingMode.UP);
|
|
|
|
|
+ BigDecimal heightRate = new BigDecimal(height).divide(new BigDecimal(bgImage.getHeight()), 2,
|
|
|
|
|
+ RoundingMode.UP);
|
|
|
|
|
+ if (widthRate.compareTo(heightRate) > 0) {
|
|
|
|
|
+ int resizeHeight = widthRate.multiply(new BigDecimal(bgImage.getHeight())).intValue();
|
|
|
|
|
+ BufferedImage resizeImage = resizeImage(bgImage, width, resizeHeight);
|
|
|
|
|
+ if (resizeHeight > height) {
|
|
|
|
|
+ resizeImage = resizeImage.getSubimage(0, (resizeHeight - height) / 2, width, height);
|
|
|
|
|
+ }
|
|
|
|
|
+ ImageIO.write(resizeImage, "png", new File(resizeImageFilePath));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ int resizeWidth = heightRate.multiply(new BigDecimal(bgImage.getWidth())).intValue();
|
|
|
|
|
+ BufferedImage resizeImage = resizeImage(bgImage, resizeWidth, height);
|
|
|
|
|
+ if (resizeWidth > width) {
|
|
|
|
|
+ resizeImage = resizeImage.getSubimage((resizeWidth - width) / 2, 0, width, height);
|
|
|
|
|
+ }
|
|
|
|
|
+ ImageIO.write(resizeImage, "png", new File(resizeImageFilePath));
|
|
|
|
|
+ }
|
|
|
|
|
+ return resizeImageFilePath;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ FFmpegUtil.MediaInfo bgVideoMediaInfo = FFmpegUtil.getMediaInfo(bgMediaFilePath);
|
|
|
|
|
+ String resizeBgVideoFilePath = "/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4";
|
|
|
|
|
+ allLocalFilePathList.add(resizeBgVideoFilePath);
|
|
|
|
|
+ if (bgVideoMediaInfo.getWidth() == width && bgVideoMediaInfo.getHeight() == height) {
|
|
|
|
|
+ resizeBgVideoFilePath = bgMediaFilePath;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ BigDecimal widthRate = new BigDecimal(width).divide(new BigDecimal(bgVideoMediaInfo.getWidth()), 2,
|
|
|
|
|
+ RoundingMode.UP);
|
|
|
|
|
+ BigDecimal heightRate = new BigDecimal(height).divide(new BigDecimal(bgVideoMediaInfo.getHeight()), 2,
|
|
|
|
|
+ RoundingMode.UP);
|
|
|
|
|
+ String clipParams;
|
|
|
|
|
+ if (widthRate.compareTo(heightRate) > 0) {
|
|
|
|
|
+ int cropX = 0;
|
|
|
|
|
+ int resizeHeight = widthRate.multiply(new BigDecimal(bgVideoMediaInfo.getHeight())).intValue();
|
|
|
|
|
+ int cropY = (resizeHeight - height) / 2;
|
|
|
|
|
+ clipParams = "scale=" + width + ":-1,crop=" + width + ":" + height + ":" + cropX + ":" + cropY;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ int resizeWidth = heightRate.multiply(new BigDecimal(bgVideoMediaInfo.getWidth())).intValue();
|
|
|
|
|
+ int cropX = (resizeWidth - width) / 2;
|
|
|
|
|
+ int cropY = 0;
|
|
|
|
|
+ clipParams = "scale=-1:" + height + ",crop=" + width + ":" + height + ":" + cropX + ":" + cropY;
|
|
|
|
|
+ }
|
|
|
|
|
+ FFmpegUtil.clipVideo(bgMediaFilePath, clipParams, resizeBgVideoFilePath);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (bgVideoMediaInfo.getDuration() < duration) {
|
|
|
|
|
+ // 循环播放
|
|
|
|
|
+ int count = duration / bgVideoMediaInfo.getDuration();
|
|
|
|
|
+ int mod = duration % bgVideoMediaInfo.getDuration();
|
|
|
|
|
+ int loop = count - 1;
|
|
|
|
|
+ if (mod > 0) {
|
|
|
|
|
+ loop = loop + 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ String loopBgVideoFilePath = "/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4";
|
|
|
|
|
+ allLocalFilePathList.add(loopBgVideoFilePath);
|
|
|
|
|
+ FFmpegUtil.loopVideo(resizeBgVideoFilePath, loop, loopBgVideoFilePath);
|
|
|
|
|
+ FFmpegUtil.MediaInfo loopMediaInfo = FFmpegUtil.getMediaInfo(loopBgVideoFilePath);
|
|
|
|
|
+ if (loopMediaInfo.getDuration() > duration) {
|
|
|
|
|
+ // 截断
|
|
|
|
|
+ String cutBgVideoFilePath = "/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4";
|
|
|
|
|
+ allLocalFilePathList.add(cutBgVideoFilePath);
|
|
|
|
|
+ String startTime = "00:00:00.000";
|
|
|
|
|
+ String cutTime = covertFFmpegCutTime(duration);
|
|
|
|
|
+ FFmpegUtil.cutVideo(loopBgVideoFilePath, startTime, cutTime, cutBgVideoFilePath);
|
|
|
|
|
+ return cutBgVideoFilePath;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return loopBgVideoFilePath;
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (bgVideoMediaInfo.getDuration() > duration) {
|
|
|
|
|
+ // 截断
|
|
|
|
|
+ String cutBgVideoFilePath = "/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4";
|
|
|
|
|
+ allLocalFilePathList.add(cutBgVideoFilePath);
|
|
|
|
|
+ String startTime = "00:00:00.000";
|
|
|
|
|
+ String cutTime = covertFFmpegCutTime(duration);
|
|
|
|
|
+ FFmpegUtil.cutVideo(resizeBgVideoFilePath, startTime, cutTime, cutBgVideoFilePath);
|
|
|
|
|
+ return cutBgVideoFilePath;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return resizeBgVideoFilePath;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String covertFFmpegCutTime(int millis) {
|
|
|
|
|
+ int second = millis / 1000;
|
|
|
|
|
+ String mod = millis % 1000 + "";
|
|
|
|
|
+ // 补0
|
|
|
|
|
+ if (mod.length() == 0) {
|
|
|
|
|
+ mod = "000";
|
|
|
|
|
+ } else if (mod.length() == 1) {
|
|
|
|
|
+ mod = "00" + mod;
|
|
|
|
|
+ } else if (mod.length() == 2) {
|
|
|
|
|
+ mod = "0" + mod;
|
|
|
|
|
+ }
|
|
|
|
|
+ return second + "." + mod;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static BufferedImage resizeImage(BufferedImage originalImage, int width, int height) {
|
|
|
|
|
+ Image scaledImage = originalImage.getScaledInstance(width, height, Image.SCALE_SMOOTH);
|
|
|
|
|
+ BufferedImage resizedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
|
|
|
|
+
|
|
|
|
|
+ Graphics2D g2d = resizedImage.createGraphics();
|
|
|
|
|
+ g2d.drawImage(scaledImage, 0, 0, null);
|
|
|
|
|
+ g2d.dispose();
|
|
|
|
|
+
|
|
|
|
|
+ return resizedImage;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public String removeBg(RemoveBgParam param) {
|
|
|
|
|
+ String inputFilePath = "/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4";
|
|
|
|
|
+ String outputFilePath = "/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mov";
|
|
|
|
|
+ String ossKey = "ffmpeg/removeBg/" + BaseUtils.getUUIDStr() + ".mov";
|
|
|
|
|
+ String contentType = "video/mov";
|
|
|
|
|
+ if ("image".endsWith(param.getMediaType())) {
|
|
|
|
|
+ inputFilePath = "/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".png";
|
|
|
|
|
+ outputFilePath = "/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".png";
|
|
|
|
|
+ ossKey = "ffmpeg/removeBg/" + BaseUtils.getUUIDStr() + ".png";
|
|
|
|
|
+ contentType = "image/png";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String webmOutputFilePath = "";
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ downloadFile(CdnUtil.getOssHttpUrl(param.getMediaUrl()), inputFilePath);
|
|
|
|
|
+ String colorAndSF = param.getColor();
|
|
|
|
|
+ if ("0xFFFFFF".equals(colorAndSF)) {
|
|
|
|
|
+ // 去除白幕,相似度和混合度需要特殊设置
|
|
|
|
|
+ colorAndSF = colorAndSF + ":0.01:0.01";
|
|
|
|
|
+ } else if ("0x00FF00".equals(colorAndSF)) {
|
|
|
|
|
+ colorAndSF = colorAndSF + ":0.2:0.1";
|
|
|
|
|
+ }
|
|
|
|
|
+ FFmpegUtil.removeBg(inputFilePath, colorAndSF, outputFilePath);
|
|
|
|
|
+ // 上传到OSS
|
|
|
|
|
+ InputStream inputStream = new FileInputStream(outputFilePath);
|
|
|
|
|
+ AliOssFileTool.saveInPublic(EnumPublicBuckets.PUBBUCKET.getBucketName(), ossKey, inputStream, contentType);
|
|
|
|
|
+
|
|
|
|
|
+ // 如果是mov视频,还需要多转一份webm格式的视频,才能在浏览器中显示
|
|
|
|
|
+ if (outputFilePath.contains(".mov")) {
|
|
|
|
|
+ webmOutputFilePath = outputFilePath.replace(".mov", ".webm");
|
|
|
|
|
+ FFmpegUtil.movToWebm(outputFilePath, webmOutputFilePath);
|
|
|
|
|
+ String webmOssKey = ossKey.replace(".mov", ".webm");
|
|
|
|
|
+ InputStream inputStreamWebm = new FileInputStream(webmOutputFilePath);
|
|
|
|
|
+ AliOssFileTool.saveInPublic(EnumPublicBuckets.PUBBUCKET.getBucketName(), webmOssKey, inputStreamWebm, "video/webm");
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ ossKey = null;
|
|
|
|
|
+ log.error("putObject error,{}", outputFilePath, e);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ try {
|
|
|
|
|
+ File fileMain = new File(inputFilePath);
|
|
|
|
|
+ fileMain.delete();
|
|
|
|
|
+
|
|
|
|
|
+ File fileOut = new File(outputFilePath);
|
|
|
|
|
+ fileOut.delete();
|
|
|
|
|
+
|
|
|
|
|
+ if (StringUtils.hasText(webmOutputFilePath)) {
|
|
|
|
|
+ File fileWebmOut = new File(webmOutputFilePath);
|
|
|
|
|
+ fileWebmOut.delete();
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("delete file error {}", outputFilePath);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return CdnUtil.getOssHttpUrl(ossKey);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public String videoAddAssSubtitle(VideoAddAssSubtitleParam param) {
|
|
|
|
|
+ String inputFilePath = "/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4";
|
|
|
|
|
+ String outputFilePath = "/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4";
|
|
|
|
|
+ String assSubtitleFilePath = "/datalog/videoComposite/subtitle/" + BaseUtils.getUUIDStr() + ".ass";
|
|
|
|
|
+ String ossKey = "ffmpeg/videoAddAssSubtitle/" + BaseUtils.getUUIDStr() + ".mp4";
|
|
|
|
|
+ try {
|
|
|
|
|
+ downloadFile(CdnUtil.getOssHttpUrl(param.getVideoUrl()), inputFilePath);
|
|
|
|
|
+ FileUtils.saveFileContent(assSubtitleFilePath, param.getAssSubtitle().getBytes());
|
|
|
|
|
+ String inputSubtitleAndStyle = "subtitles=" + assSubtitleFilePath;
|
|
|
|
|
+ FFmpegUtil.addSubtitle(inputFilePath, inputSubtitleAndStyle, outputFilePath);
|
|
|
|
|
+ // 上传到OSS
|
|
|
|
|
+ InputStream inputStream = new FileInputStream(outputFilePath);
|
|
|
|
|
+ AliOssFileTool.saveInPublic(EnumPublicBuckets.PUBBUCKET.getBucketName(), ossKey, inputStream, "video/mp4");
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ ossKey = null;
|
|
|
|
|
+ log.error("videoAddAssSubtitle error,{}", outputFilePath, e);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ try {
|
|
|
|
|
+ File fileIn = new File(inputFilePath);
|
|
|
|
|
+ fileIn.delete();
|
|
|
|
|
+
|
|
|
|
|
+ File fileOut = new File(outputFilePath);
|
|
|
|
|
+ fileOut.delete();
|
|
|
|
|
+
|
|
|
|
|
+ File fileAss = new File(assSubtitleFilePath);
|
|
|
|
|
+ fileAss.delete();
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("delete file error {}", outputFilePath);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return CdnUtil.getOssHttpUrl(ossKey);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public List<String> videoExtraAudio(VideoUrlsParam param) {
|
|
|
|
|
+ if (CollectionUtil.isEmpty(param.getVideoUrls())) {
|
|
|
|
|
+ return new ArrayList<>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<String> result = new ArrayList<>();
|
|
|
|
|
+ for (String videoUrl : param.getVideoUrls()) {
|
|
|
|
|
+ String outputPath = null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ outputPath = FFmpegUtil.getVideoVoice(videoUrl,
|
|
|
|
|
+ FFmpegUtil.pathCreate("/datalog/videoExtractAudio/voice/" + BaseUtils.getUUIDStr() + ".mp3"));
|
|
|
|
|
+
|
|
|
|
|
+ if (StringUtils.hasText(outputPath)) {
|
|
|
|
|
+ String ossKey = "video/voice/" + BaseUtils.getUUIDStr() + ".mp3";
|
|
|
|
|
+ AliOssFileTool.saveInPublic(EnumPublicBuckets.PUBBUCKET.getBucketName(), ossKey, Files.newInputStream(Paths.get(outputPath)), "audio/mp3");
|
|
|
|
|
+ result.add(CdnUtil.getOssHttpUrl(ossKey));
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("ffmpeg videoExtraAudio error, videoUrl = {}", videoUrl, e);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (StringUtils.hasText(outputPath)) {
|
|
|
|
|
+ File file = new File(outputPath);
|
|
|
|
|
+ boolean delete = file.delete();
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("delete file error {}", outputPath);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public List<String> videoExtraText(VideoUrlsParam param) {
|
|
|
|
|
+ if (CollectionUtil.isEmpty(param.getVideoUrls())) {
|
|
|
|
|
+ return new ArrayList<>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<String> result = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ for (String videoUrl : param.getVideoUrls()) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ result.add(videoExtraText(videoUrl));
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("ffmpeg videoExtraText error, videoUrl = {}", videoUrl, e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public String extraVideoSrt(ExtraVideoSrtParam param) {
|
|
|
|
|
+ // 1,提取音频
|
|
|
|
|
+ List<String> videoUrls = videoExtraAudio(new VideoUrlsParam().setVideoUrls(Collections.singletonList(param.getVideoUrl())));
|
|
|
|
|
+ if (CollectionUtil.isEmpty(videoUrls)) {
|
|
|
|
|
+ throw new RuntimeException("提取音频步骤失败");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2,录音文件识别
|
|
|
|
|
+ String audioUrl = videoUrls.get(0);
|
|
|
|
|
+ List<AliVoiceResultSentenceData> sentences = toolsAudioTransService.getAudioTransResults(audioUrl, false).getSentences();
|
|
|
|
|
+ if (CollectionUtil.isEmpty(sentences)) {
|
|
|
|
|
+ throw new RuntimeException("录音文件识别步骤失败");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3,提取srt
|
|
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
|
|
+ for (int i = 0; i < sentences.size(); i++) {
|
|
|
|
|
+ AliVoiceResultSentenceData sentenceData = sentences.get(i);
|
|
|
|
|
+ if (StringUtils.hasText(sentenceData.getText())) {
|
|
|
|
|
+ sb.append(i + 1).append("\n");
|
|
|
|
|
+ sb.append(TimelineUtils.convertMillisToStringTime(sentenceData.getBeginTime(), ",")).append(" --> ")
|
|
|
|
|
+ .append(TimelineUtils.convertMillisToStringTime(sentenceData.getEndTime(), ",")).append("\n");
|
|
|
|
|
+ sb.append(sentenceData.getText()).append("\n");
|
|
|
|
|
+ sb.append("\n");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return sb.toString().trim();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public String audioVolume(AudioVolumeParam param) {
|
|
|
|
|
+ if (!StringUtils.hasText(param.getAudioUrl())) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ double volume = Optional.ofNullable(param.getVolume()).orElse(1d);
|
|
|
|
|
+
|
|
|
|
|
+ String url = param.getAudioUrl();
|
|
|
|
|
+ List<String> allLocalFilePathList = new ArrayList<>();
|
|
|
|
|
+ try {
|
|
|
|
|
+ String inputFilePath = FFmpegUtil.pathCreate("/datalog/audioVolume/audio/" + BaseUtils.getUUIDStr() + ".mp3");
|
|
|
|
|
+ downloadFile(url, inputFilePath);
|
|
|
|
|
+ allLocalFilePathList.add(inputFilePath);
|
|
|
|
|
+
|
|
|
|
|
+ // 音频降噪
|
|
|
|
|
+ String outputFilePath = FFmpegUtil.pathCreate("/datalog/audioVolume/audio/" + BaseUtils.getUUIDStr() + ".mp3");
|
|
|
|
|
+ allLocalFilePathList.add(outputFilePath);
|
|
|
|
|
+
|
|
|
|
|
+ String afParam = String.format("afftdn=nf=-20,volume=%s", volume);
|
|
|
|
|
+ FFmpegUtil.audioVolume(inputFilePath, outputFilePath, afParam);
|
|
|
|
|
+
|
|
|
|
|
+ String ossKey = String.format("audioVolume/%s.mp3", BaseUtils.getUUIDStr());
|
|
|
|
|
+ AliOssFileTool.saveInPublic(EnumPublicBuckets.PUBBUCKET.getBucketName(), ossKey, Files.newInputStream(Paths.get(outputFilePath)));
|
|
|
|
|
+ return CdnUtil.getOssHttpUrl(ossKey);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("ffmpeg audioVolume error, url = {}", url, e);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ for (String localFilePath : allLocalFilePathList) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ new File(localFilePath).delete();
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("delete file error, localFilePath = {}", localFilePath, e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public String audioSplit(AudioSplitParam param) {
|
|
|
|
|
+ log.info("audioSplit param = {}", param);
|
|
|
|
|
+ if (!StringUtils.hasText(param.getAudioUrl()) || Objects.isNull(param.getSegmentTime()) || param.getSegmentTime() <= 0) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<String> allLocalFilePathList = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ String inputFile = FFmpegUtil.pathCreate("/datalog/audioSplit/audio/" + BaseUtils.getUUIDStr() + ".mp3");
|
|
|
|
|
+ downloadFile(param.getAudioUrl(), inputFile);
|
|
|
|
|
+ allLocalFilePathList.add(inputFile);
|
|
|
|
|
+
|
|
|
|
|
+ String outputPath = "/datalog/audioSplit/split/" + BaseUtils.getUUIDStr();
|
|
|
|
|
+ String outputFilePath = FFmpegUtil.pathCreate(outputPath + "/" + BaseUtils.getUUIDStr() + "_%03d.mp3");
|
|
|
|
|
+ FFmpegUtil.audioSplit(inputFile, param.getSegmentTime(), outputFilePath);
|
|
|
|
|
+
|
|
|
|
|
+ List<String> files = FFmpegUtil.ls(outputPath);
|
|
|
|
|
+ if (CollectionUtil.isEmpty(files)) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+ allLocalFilePathList.addAll(files);
|
|
|
|
|
+
|
|
|
|
|
+ // 按照文件名的后几位数字排序
|
|
|
|
|
+ files = files.stream().sorted((f1, f2) -> {
|
|
|
|
|
+ String filename1 = Paths.get(f1).getFileName().toString();
|
|
|
|
|
+ String filename2 = Paths.get(f2).getFileName().toString();
|
|
|
|
|
+
|
|
|
|
|
+ // 提取数字部分
|
|
|
|
|
+ int num1 = extractNumber(filename1);
|
|
|
|
|
+ int num2 = extractNumber(filename2);
|
|
|
|
|
+
|
|
|
|
|
+ // 如果都能提取到数字,按数字排序
|
|
|
|
|
+ if (num1 != Integer.MAX_VALUE && num2 != Integer.MAX_VALUE) {
|
|
|
|
|
+ return Integer.compare(num1, num2);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 否则按文件名自然排序
|
|
|
|
|
+ return filename1.compareTo(filename2);
|
|
|
|
|
+ }).collect(Collectors.toList());
|
|
|
|
|
+
|
|
|
|
|
+ List<String> ossUrls = new ArrayList<>(files.size());
|
|
|
|
|
+ for (String file : files) {
|
|
|
|
|
+ Path path = Paths.get(file);
|
|
|
|
|
+ String ossKey = "audio/split/" + path.getFileName().toString();
|
|
|
|
|
+ AliOssFileTool.saveInPublic(EnumPublicBuckets.PUBBUCKET.getBucketName(), ossKey, Files.newInputStream(path));
|
|
|
|
|
+ ossUrls.add(CdnUtil.getOssHttpUrl(ossKey));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (ossUrls.size() != files.size()) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+ return String.join(",", ossUrls);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("ffmpeg audioSplit error, url = {}", param.getAudioUrl(), e);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ for (String localFilePath : allLocalFilePathList) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ new File(localFilePath).delete();
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("delete file error, localFilePath = {}", localFilePath, e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public String videoExtraText(String videoUrl) {
|
|
|
|
|
+ // 1,提取音频
|
|
|
|
|
+ List<String> videoUrls = videoExtraAudio(new VideoUrlsParam().setVideoUrls(Collections.singletonList(videoUrl)));
|
|
|
|
|
+ if (CollectionUtil.isEmpty(videoUrls)) {
|
|
|
|
|
+ throw new RuntimeException("提取音频步骤失败");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2,录音文件识别
|
|
|
|
|
+ String audioUrl = videoUrls.get(0);
|
|
|
|
|
+ List<AliVoiceResultSentenceData> sentences = toolsAudioTransService.getAudioTransResults(audioUrl, false).getSentences();
|
|
|
|
|
+ if (CollectionUtil.isEmpty(sentences)) {
|
|
|
|
|
+ throw new RuntimeException("录音文件识别步骤失败");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3,提取文字
|
|
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
|
|
+ for (int i = 0; i < sentences.size(); i++) {
|
|
|
|
|
+ AliVoiceResultSentenceData sentenceData = sentences.get(i);
|
|
|
|
|
+ if (StringUtils.hasText(sentenceData.getText())) {
|
|
|
|
|
+ sb.append(sentenceData.getText()).append("\n");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return sb.toString().trim();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public List<String> videoExtraFrame(VideoUrlsParam param) {
|
|
|
|
|
+ if (CollectionUtil.isEmpty(param.getVideoUrls())) {
|
|
|
|
|
+ return new ArrayList<>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<String> result = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ for (String videoUrl : param.getVideoUrls()) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ result.addAll(videoExtraFrame(videoUrl));
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("ffmpeg videoExtraFrame error, videoUrl = {}", videoUrl, e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public String videoTimeExtraFrame(VideoTimeExtraFrameParam param) {
|
|
|
|
|
+ if (!StringUtils.hasText(param.getVideoUrl())
|
|
|
|
|
+ || !StringUtils.hasText(param.getTime())) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String imageLocalPath = null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ imageLocalPath = FFmpegUtil.getTimeImage(param.getVideoUrl(), param.getTime(),
|
|
|
|
|
+ FFmpegUtil.pathCreate("/datalog/videoExtractKeyframe/image/" + BaseUtils.getUUIDStr() + ".jpg"), 0);
|
|
|
|
|
+ if (StringUtils.hasText(imageLocalPath)) {
|
|
|
|
|
+ String ossKey = "video/timeFrame/" + BaseUtils.getUUIDStr() + ".jpg";
|
|
|
|
|
+ AliOssFileTool.saveInPublic(EnumPublicBuckets.PUBBUCKET.getBucketName(), ossKey,
|
|
|
|
|
+ Files.newInputStream(Paths.get(imageLocalPath)), "image/jpeg");
|
|
|
|
|
+ return CdnUtil.getOssHttpUrl(ossKey);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("ffmpeg videoTimeExtraFrame error, params = {}, ", JSON.toJSONString(param), e);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (StringUtils.hasText(imageLocalPath)) {
|
|
|
|
|
+ File file = new File(imageLocalPath);
|
|
|
|
|
+ file.delete();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public String timeCutVideo(VideoTimeCutParam param) {
|
|
|
|
|
+ if (!StringUtils.hasText(param.getVideoUrl())
|
|
|
|
|
+ || !StringUtils.hasText(param.getStartTime())
|
|
|
|
|
+ || !StringUtils.hasText(param.getEndTime())) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String outputPath = "";
|
|
|
|
|
+ try {
|
|
|
|
|
+ outputPath = FFmpegUtil.pathCreate("/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4");
|
|
|
|
|
+ FFmpegUtil.cutVideoTime(param.getVideoUrl(), param.getStartTime(), param.getEndTime(), outputPath);
|
|
|
|
|
+ String ossKey = "video/video/" + BaseUtils.getUUIDStr() + ".mp4";
|
|
|
|
|
+ AliOssFileTool.saveInPublic(EnumPublicBuckets.PUBBUCKET.getBucketName(), ossKey,
|
|
|
|
|
+ Files.newInputStream(Paths.get(outputPath)), "video/mp4");
|
|
|
|
|
+ return CdnUtil.getOssHttpUrl(ossKey);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("ffmpeg timeCutVideo error, params = {}, ", JSON.toJSONString(param), e);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (StringUtils.hasText(outputPath)) {
|
|
|
|
|
+ File file = new File(outputPath);
|
|
|
|
|
+ file.delete();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public String timeExtraAudio(VideoTimeCutParam param) {
|
|
|
|
|
+ if (!StringUtils.hasText(param.getVideoUrl())
|
|
|
|
|
+ || !StringUtils.hasText(param.getStartTime())
|
|
|
|
|
+ || !StringUtils.hasText(param.getEndTime())) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String outputPath = null;
|
|
|
|
|
+ String inputVideoPath = null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ inputVideoPath = FFmpegUtil.pathCreate("/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4");
|
|
|
|
|
+ FileUtils.saveFileContent(inputVideoPath, imageService.imageDownloadV2(param.getVideoUrl()));
|
|
|
|
|
+ outputPath = FFmpegUtil.getVideoVoice(inputVideoPath, param.getStartTime(), param.getEndTime(),
|
|
|
|
|
+ FFmpegUtil.pathCreate("/datalog/videoExtractAudio/voice/" + BaseUtils.getUUIDStr() + ".mp3"));
|
|
|
|
|
+ if (StringUtils.hasText(outputPath)) {
|
|
|
|
|
+ String ossKey = "video/voice/" + BaseUtils.getUUIDStr() + ".mp3";
|
|
|
|
|
+ AliOssFileTool.saveInPublic(EnumPublicBuckets.PUBBUCKET.getBucketName(), ossKey, Files.newInputStream(Paths.get(outputPath)), "audio/mp3");
|
|
|
|
|
+ return CdnUtil.getOssHttpUrl(ossKey);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("ffmpeg timeExtraAudio error, params = {}", JSON.toJSONString(param), e);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (StringUtils.hasText(outputPath)) {
|
|
|
|
|
+ File file = new File(outputPath);
|
|
|
|
|
+ boolean delete = file.delete();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StringUtils.hasText(inputVideoPath)) {
|
|
|
|
|
+ File file = new File(inputVideoPath);
|
|
|
|
|
+ boolean delete = file.delete();
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("delete file error {}, {}", outputPath, inputVideoPath);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public String command(CommandParam param) {
|
|
|
|
|
+ String ffmpegCommand = param.getFfmpegCommand();
|
|
|
|
|
+ List<String> allLocalFilePathList = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ String outputFile = FFmpegUtil.pathCreate("/datalog/ffmpeg/command/result/" + param.getOutputFile());
|
|
|
|
|
+ allLocalFilePathList.add(outputFile);
|
|
|
|
|
+ ffmpegCommand = ffmpegCommand.replace(param.getOutputFile(), outputFile);
|
|
|
|
|
+ ffmpegCommand = ffmpegCommand.replace("\n", " ").replace("\\", "").trim();
|
|
|
|
|
+ List<String> commands = splitCommands(ffmpegCommand);
|
|
|
|
|
+ FFmpegUtil.FFmpegCommandResult fFmpegCommandResult = FFmpegUtil.executeFFmpegCommand(commands);
|
|
|
|
|
+ if (!fFmpegCommandResult.isSuccessful()) {
|
|
|
|
|
+ throw new CommonException(ExceptionEnum.SYSTEM_ERROR.getCode(),
|
|
|
|
|
+ "执行出错," + JSON.toJSONString(fFmpegCommandResult)
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if ("video".equals(param.getOutputType())) {
|
|
|
|
|
+ String ossKey = "videoComposite/" + BaseUtils.getUUIDStr() + ".mp4";
|
|
|
|
|
+ AliOssFileTool.saveInPublic(EnumPublicBuckets.PUBBUCKET.getBucketName(), ossKey, Files.newInputStream(Paths.get(outputFile)), "video/mp4");
|
|
|
|
|
+ return CdnUtil.getOssHttpUrl(ossKey);
|
|
|
|
|
+ } else if ("audio".equals(param.getOutputType())) {
|
|
|
|
|
+ String ossKey = "videoComposite/" + BaseUtils.getUUIDStr() + ".mp3";
|
|
|
|
|
+ AliOssFileTool.saveInPublic(EnumPublicBuckets.PUBBUCKET.getBucketName(), ossKey, Files.newInputStream(Paths.get(outputFile)), "audio/mpeg");
|
|
|
|
|
+ return CdnUtil.getOssHttpUrl(ossKey);
|
|
|
|
|
+ } else if ("image".equals(param.getOutputType())) {
|
|
|
|
|
+ String ossKey = "videoComposite/" + BaseUtils.getUUIDStr() + ".jpg";
|
|
|
|
|
+ AliOssFileTool.saveInPublic(EnumPublicBuckets.PUBBUCKET.getBucketName(), ossKey, Files.newInputStream(Paths.get(outputFile)), "image/jpeg");
|
|
|
|
|
+ return CdnUtil.getOssHttpUrl(ossKey);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ String ossKey = "videoComposite/" + BaseUtils.getUUIDStr() + ".txt";
|
|
|
|
|
+ AliOssFileTool.saveInPublic(EnumPublicBuckets.PUBBUCKET.getBucketName(), ossKey, Files.newInputStream(Paths.get(outputFile)), "text/plain");
|
|
|
|
|
+ return CdnUtil.getOssHttpUrl(ossKey);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (IOException e) {
|
|
|
|
|
+ throw new RuntimeException(e);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ for (String s : allLocalFilePathList) {
|
|
|
|
|
+ File file = new File(s);
|
|
|
|
|
+ boolean delete = file.delete();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<String> splitCommands(String ffmpegCommand) {
|
|
|
|
|
+ List<String> parts = new ArrayList<>();
|
|
|
|
|
+ boolean inQuotes = false;
|
|
|
|
|
+ StringBuilder current = new StringBuilder();
|
|
|
|
|
+
|
|
|
|
|
+ for (char c : ffmpegCommand.toCharArray()) {
|
|
|
|
|
+ if (c == '"') {
|
|
|
|
|
+ inQuotes = !inQuotes;
|
|
|
|
|
+ } else if (c == ' ' && !inQuotes) {
|
|
|
|
|
+ if (current.length() > 0) {
|
|
|
|
|
+ parts.add(current.toString());
|
|
|
|
|
+ current.setLength(0);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ current.append(c);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (current.length() > 0) {
|
|
|
|
|
+ parts.add(current.toString());
|
|
|
|
|
+ }
|
|
|
|
|
+ return parts.stream().filter(StringUtils::hasText).collect(Collectors.toList());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public String mediaSimpleMerge(MediaSimpleMergeParam param) {
|
|
|
|
|
+ if (CollectionUtil.isEmpty(param.getVideoUrls())) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 视频拼接
|
|
|
|
|
+ String videoUrl = videoTogether(param.getVideoUrls());
|
|
|
|
|
+
|
|
|
|
|
+ // 音频拼接
|
|
|
|
|
+ if (CollectionUtil.isEmpty(param.getAudioUrls())) {
|
|
|
|
|
+ return videoUrl;
|
|
|
|
|
+ }
|
|
|
|
|
+ String audioUrl = audioTogether(param.getAudioUrls());
|
|
|
|
|
+
|
|
|
|
|
+ // 音视频合并
|
|
|
|
|
+ if (TextUtils.isBlank(audioUrl)) {
|
|
|
|
|
+ return videoUrl;
|
|
|
|
|
+ }
|
|
|
|
|
+ String result = mediaMerge(videoUrl, audioUrl);
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("ffmpeg mediaSimpleMerge error", e);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public String videoTexture(VideoTextureParam param) {
|
|
|
|
|
+ if (TextUtils.isBlank(param.getImageUrl()) || TextUtils.isBlank(param.getVideoUrl())) {
|
|
|
|
|
+ log.error("videoTexture missing necessary parameters, param = {}", JSON.toJSONString(param));
|
|
|
|
|
+ throw new CommonException(ExceptionEnum.PARAM_ERROR.getCode(), "videoTexture missing necessary " +
|
|
|
|
|
+ "parameters, param:" + JSON.toJSONString(param));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<String> allLocalFilePathList = new ArrayList<>();
|
|
|
|
|
+ long beginTime = System.currentTimeMillis();
|
|
|
|
|
+ try {
|
|
|
|
|
+ String videoPath = FFmpegUtil.pathCreate("/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4");
|
|
|
|
|
+ FileUtils.saveFileContent(videoPath, imageService.imageDownloadV2(param.getVideoUrl()));
|
|
|
|
|
+ allLocalFilePathList.add(videoPath);
|
|
|
|
|
+ long time1 = System.currentTimeMillis();
|
|
|
|
|
+ log.info("videoTexture,time1:{},{}", (time1 - beginTime), param.getVideoUrl());
|
|
|
|
|
+ // 获取视频宽高
|
|
|
|
|
+ FFmpegUtil.MediaInfo videoMediaInfo = FFmpegUtil.getMediaInfo(videoPath);
|
|
|
|
|
+ long time2 = System.currentTimeMillis();
|
|
|
|
|
+ log.info("videoTexture,time2:{},{}", (time2 - time1), param.getVideoUrl());
|
|
|
|
|
+
|
|
|
|
|
+ String imagePath = FFmpegUtil.pathCreate("/datalog/videoComposite/image/" + BaseUtils.getUUIDStr() + ".png");
|
|
|
|
|
+ FileUtils.saveFileContent(imagePath, imageService.imageDownloadV2(param.getImageUrl()));
|
|
|
|
|
+ allLocalFilePathList.add(imagePath);
|
|
|
|
|
+ long time3 = System.currentTimeMillis();
|
|
|
|
|
+ log.info("videoTexture,time3:{},{}", (time3 - time2), param.getVideoUrl());
|
|
|
|
|
+
|
|
|
|
|
+ BufferedImage originImage = ImageIO.read(new File(imagePath));
|
|
|
|
|
+ boolean needCrop = false;
|
|
|
|
|
+ // 图片尺寸过大,需要裁剪
|
|
|
|
|
+ if (originImage.getWidth() > videoMediaInfo.getWidth().intValue()
|
|
|
|
|
+ || originImage.getHeight() > videoMediaInfo.getHeight().intValue()) {
|
|
|
|
|
+ needCrop = true;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ BigDecimal widthMultiply =
|
|
|
|
|
+ new BigDecimal(originImage.getWidth()).divide(new BigDecimal(videoMediaInfo.getWidth()), 2, RoundingMode.HALF_UP);
|
|
|
|
|
+ BigDecimal heightMultiply =
|
|
|
|
|
+ new BigDecimal(originImage.getHeight()).divide(new BigDecimal(videoMediaInfo.getHeight()), 2, RoundingMode.HALF_UP);
|
|
|
|
|
+ if (widthMultiply.compareTo(new BigDecimal("0.4")) > 0 || heightMultiply.compareTo(new BigDecimal("0.4")) > 0) {
|
|
|
|
|
+ needCrop = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // 图片尺寸过大,需要裁剪
|
|
|
|
|
+ if (needCrop) {
|
|
|
|
|
+ originImage = ImageUtils.cropTransparentImageByShort(originImage, videoMediaInfo.getWidth(), videoMediaInfo.getHeight());
|
|
|
|
|
+ ImageIO.write(originImage, "PNG", new File(imagePath));
|
|
|
|
|
+ long time4 = System.currentTimeMillis();
|
|
|
|
|
+ log.info("videoTexture,time4:{},{}", (time4 - time3), param.getVideoUrl());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String outputPath = FFmpegUtil.pathCreate("/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4");
|
|
|
|
|
+ allLocalFilePathList.add(outputPath);
|
|
|
|
|
+
|
|
|
|
|
+ // 贴图操作
|
|
|
|
|
+ FFmpegUtil.videoTexture(videoPath, imagePath,
|
|
|
|
|
+ videoMediaInfo.getWidth() - (originImage.getWidth() + Optional.ofNullable(param.getBorder()).orElse(0)),
|
|
|
|
|
+ videoMediaInfo.getHeight() - (originImage.getHeight() + Optional.ofNullable(param.getBorder()).orElse(0)),
|
|
|
|
|
+ outputPath);
|
|
|
|
|
+ long time5 = System.currentTimeMillis();
|
|
|
|
|
+ log.info("videoTexture,time5:{},{}", (time5 - time3), param.getVideoUrl());
|
|
|
|
|
+ // 转存
|
|
|
|
|
+ String ossKey = "videoComposite/" + BaseUtils.getUUIDStr() + ".mp4";
|
|
|
|
|
+ AliOssFileTool.saveInPublic(EnumPublicBuckets.PUBBUCKET.getBucketName(), ossKey, Files.newInputStream(Paths.get(outputPath)), "video/mp4");
|
|
|
|
|
+ long time6 = System.currentTimeMillis();
|
|
|
|
|
+ log.info("videoTexture,time6:{},{}", (time6 - time5), param.getVideoUrl());
|
|
|
|
|
+ return CdnUtil.getOssHttpUrl(ossKey);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("videoTexture error, request param = {}", JSON.toJSONString(param), e);
|
|
|
|
|
+ throw new CommonException(ExceptionEnum.SYSTEM_ERROR.getCode(), "videoTexture error," + e.getMessage());
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (CollectionUtil.isNotEmpty(allLocalFilePathList)) {
|
|
|
|
|
+ for (String filePath : allLocalFilePathList) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ File file = new File(filePath);
|
|
|
|
|
+ file.delete();
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("delete file error,", filePath);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public String videoInfo(String videoUrl) {
|
|
|
|
|
+ FFmpegUtil.MediaInfo mediaInfo = FFmpegUtil.getMediaInfo(videoUrl);
|
|
|
|
|
+ return JSON.toJSONString(mediaInfo);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String mediaMerge(String videoUrl, String audioUrl) throws IOException, DownloadImageError {
|
|
|
|
|
+ if (TextUtils.isBlank(videoUrl) || TextUtils.isBlank(audioUrl)) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ List<String> allLocalFilePathList = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ String videoPath = FFmpegUtil.pathCreate("/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4");
|
|
|
|
|
+ FileUtils.saveFileContent(videoPath, imageService.imageDownloadV2(videoUrl));
|
|
|
|
|
+ allLocalFilePathList.add(videoPath);
|
|
|
|
|
+ String audioPath = FFmpegUtil.pathCreate("/datalog/videoComposite/filelist/" + BaseUtils.getUUIDStr() + ".mp3");
|
|
|
|
|
+ FileUtils.saveFileContent(audioPath, imageService.imageDownloadV2(audioUrl));
|
|
|
|
|
+ allLocalFilePathList.add(audioPath);
|
|
|
|
|
+
|
|
|
|
|
+ String outputPath = FFmpegUtil.pathCreate("/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4");
|
|
|
|
|
+ allLocalFilePathList.add(outputPath);
|
|
|
|
|
+ FFmpegUtil.addAudioSelfAdaption(videoPath, audioPath, outputPath);
|
|
|
|
|
+ String ossKey = "videoComposite/" + BaseUtils.getUUIDStr() + ".mp4";
|
|
|
|
|
+ AliOssFileTool.saveInPublic(EnumPublicBuckets.PUBBUCKET.getBucketName(), ossKey, Files.newInputStream(Paths.get(outputPath)), "video/mp4");
|
|
|
|
|
+ for (String s : allLocalFilePathList) {
|
|
|
|
|
+ File file = new File(s);
|
|
|
|
|
+ boolean delete = file.delete();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return CdnUtil.getOssHttpUrl(ossKey);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String audioTogether(List<String> audioUrls) throws DownloadImageError, IOException {
|
|
|
|
|
+ List<String> allLocalFilePathList = new ArrayList<>();
|
|
|
|
|
+ StringBuilder concatContent = new StringBuilder();
|
|
|
|
|
+ for (String audioUrl : audioUrls) {
|
|
|
|
|
+ String audioPath = FFmpegUtil.pathCreate("/datalog/videoComposite/filelist/" + BaseUtils.getUUIDStr() + ".mp3");
|
|
|
|
|
+ FileUtils.saveFileContent(audioPath, imageService.imageDownloadV2(audioUrl));
|
|
|
|
|
+ allLocalFilePathList.add(audioPath);
|
|
|
|
|
+ concatContent.append("file '").append(audioPath).append("'").append("\n");
|
|
|
|
|
+ }
|
|
|
|
|
+ String concatFilePath = FFmpegUtil.pathCreate("/datalog/videoComposite/ace/" + BaseUtils.getUUIDStr() + ".txt");
|
|
|
|
|
+ FileUtils.saveFileContent(concatFilePath, concatContent.toString().trim().getBytes());
|
|
|
|
|
+ allLocalFilePathList.add(concatFilePath);
|
|
|
|
|
+ String concatOutputFilePath = FFmpegUtil.pathCreate("/datalog/videoComposite/ace/" + BaseUtils.getUUIDStr() + ".mp3");
|
|
|
|
|
+ FFmpegUtil.concatAudios(concatFilePath, concatOutputFilePath);
|
|
|
|
|
+ allLocalFilePathList.add(concatOutputFilePath);
|
|
|
|
|
+ String ossKey = "composeAceAudio/" + BaseUtils.getUUIDStr() + ".mp3";
|
|
|
|
|
+ AliOssFileTool.saveInPublic(EnumPublicBuckets.PUBBUCKET.getBucketName(), ossKey, Files.newInputStream(Paths.get(concatOutputFilePath)), "audio/mp3");
|
|
|
|
|
+ for (String s : allLocalFilePathList) {
|
|
|
|
|
+ File file = new File(s);
|
|
|
|
|
+ boolean delete = file.delete();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return CdnUtil.getOssHttpUrl(ossKey);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String videoTogether(List<String> videoUrls) throws DownloadImageError, IOException {
|
|
|
|
|
+ List<String> allLocalFilePathList = new ArrayList<>();
|
|
|
|
|
+ // 视频片段拼接
|
|
|
|
|
+ StringBuilder concatContent = new StringBuilder();
|
|
|
|
|
+ for (String videoUrl : videoUrls) {
|
|
|
|
|
+ String videoPath = FFmpegUtil.pathCreate("/datalog/videoComposite/filelist/" + BaseUtils.getUUIDStr() + ".mp4");
|
|
|
|
|
+ FileUtils.saveFileContent(videoPath, imageService.imageDownloadV2(videoUrl));
|
|
|
|
|
+ concatContent.append("file '").append(videoPath).append("'").append("\n");
|
|
|
|
|
+ allLocalFilePathList.add(videoPath);
|
|
|
|
|
+ }
|
|
|
|
|
+ String concatFilePath = FFmpegUtil.pathCreate("/datalog/videoComposite/filelist/" + BaseUtils.getUUIDStr() + ".txt");
|
|
|
|
|
+ FileUtils.saveFileContent(concatFilePath, concatContent.toString().trim().getBytes());
|
|
|
|
|
+ allLocalFilePathList.add(concatFilePath);
|
|
|
|
|
+ String concatVideoPath = FFmpegUtil.pathCreate("/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4");
|
|
|
|
|
+ FFmpegUtil.concatVideos(concatFilePath, concatVideoPath);
|
|
|
|
|
+ allLocalFilePathList.add(concatVideoPath);
|
|
|
|
|
+ String ossKey = "videoComposite/" + BaseUtils.getUUIDStr() + ".mp4";
|
|
|
|
|
+ AliOssFileTool.saveInPublic(EnumPublicBuckets.PUBBUCKET.getBucketName(), ossKey, Files.newInputStream(Paths.get(concatVideoPath)), "video/mp4");
|
|
|
|
|
+ for (String s : allLocalFilePathList) {
|
|
|
|
|
+ File file = new File(s);
|
|
|
|
|
+ boolean delete = file.delete();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return CdnUtil.getOssHttpUrl(ossKey);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public List<String> videoExtraFrame(String videoUrl) throws IOException {
|
|
|
|
|
+ // 1,提取音频
|
|
|
|
|
+ List<String> videoUrls = videoExtraAudio(new VideoUrlsParam().setVideoUrls(Collections.singletonList(videoUrl)));
|
|
|
|
|
+ if (CollectionUtil.isEmpty(videoUrls)) {
|
|
|
|
|
+ throw new RuntimeException("提取音频步骤失败");
|
|
|
|
|
+ }
|
|
|
|
|
+ // 2,录音文件识别
|
|
|
|
|
+ String audioUrl = videoUrls.get(0);
|
|
|
|
|
+ List<AliVoiceResultSentenceData> sentences = toolsAudioTransService.getAudioTransResults(audioUrl, false).getSentences();
|
|
|
|
|
+ if (CollectionUtil.isEmpty(sentences)) {
|
|
|
|
|
+ throw new RuntimeException("录音文件识别步骤失败");
|
|
|
|
|
+ }
|
|
|
|
|
+ // 3,提取关键帧
|
|
|
|
|
+ List<String> keyframeUrlList = new ArrayList<>();
|
|
|
|
|
+ for (AliVoiceResultSentenceData sentenceData : sentences) {
|
|
|
|
|
+ int keyTime = (sentenceData.getBeginTime() + sentenceData.getEndTime()) / 2;
|
|
|
|
|
+ String cutTime = getCutTime(keyTime);
|
|
|
|
|
+ String imageLocalPath = FFmpegUtil.getTimeImage(videoUrl, cutTime,
|
|
|
|
|
+ FFmpegUtil.pathCreate("/datalog/videoExtractKeyframe/image/" + BaseUtils.getUUIDStr() + ".jpg"), 0);
|
|
|
|
|
+ if (StringUtils.hasText(imageLocalPath)) {
|
|
|
|
|
+ String ossKey = "video/keyFrame/" + BaseUtils.getUUIDStr() + ".jpg";
|
|
|
|
|
+ AliOssFileTool.saveInPublic(EnumPublicBuckets.PUBBUCKET.getBucketName(), ossKey,
|
|
|
|
|
+ Files.newInputStream(Paths.get(imageLocalPath)), "image/jpeg");
|
|
|
|
|
+ keyframeUrlList.add(CdnUtil.getOssHttpUrl(ossKey));
|
|
|
|
|
+ File file = new File(imageLocalPath);
|
|
|
|
|
+ boolean delete = file.delete();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return keyframeUrlList;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public String videoConcat(VideoUrlsParam param) {
|
|
|
|
|
+ List<String> allLocalFilePathList = new ArrayList<>();
|
|
|
|
|
+ try {
|
|
|
|
|
+ String concatFileContent = buildConcatFileContentV2(param.getVideoUrls(), allLocalFilePathList, param.getPretreatmentSwitch());
|
|
|
|
|
+ String concatFilePath = FFmpegUtil.pathCreate("/datalog/videoComposite/filelist/" + BaseUtils.getUUIDStr() + ".txt");
|
|
|
|
|
+ FileUtils.saveFileContent(concatFilePath, concatFileContent.getBytes());
|
|
|
|
|
+ allLocalFilePathList.add(concatFilePath);
|
|
|
|
|
+
|
|
|
|
|
+ String outputFilePath = "/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4";
|
|
|
|
|
+ allLocalFilePathList.add(outputFilePath);
|
|
|
|
|
+
|
|
|
|
|
+ FFmpegUtil.concatVideos(concatFilePath, outputFilePath);
|
|
|
|
|
+
|
|
|
|
|
+ // 上传到oss中
|
|
|
|
|
+ String ossKey = "videoComposite/" + BaseUtils.getUUIDStr() + ".mp4";
|
|
|
|
|
+ InputStream inputStream = new FileInputStream(outputFilePath);
|
|
|
|
|
+ AliOssFileTool.saveInPublic(EnumPublicBuckets.PUBBUCKET.getBucketName(), ossKey, inputStream, "video/mp4");
|
|
|
|
|
+ return CdnUtil.getOssHttpUrl(ossKey);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("videoConcat error,param:{}", JSON.toJSONString(param), e);
|
|
|
|
|
+ throw new CommonException(ExceptionEnum.SYSTEM_ERROR.getCode(), e.getMessage());
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (CollectionUtil.isNotEmpty(allLocalFilePathList)) {
|
|
|
|
|
+ for (String filePath : allLocalFilePathList) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ File file = new File(filePath);
|
|
|
|
|
+ file.delete();
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("delete file error,", filePath);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Deprecated
|
|
|
|
|
+ private String buildConcatFileContent(List<String> clipVideoUrls, List<String> allLocalFilePathList) throws Exception {
|
|
|
|
|
+ List<String> scaleVideoFilePathList = new ArrayList<>();
|
|
|
|
|
+ // 以第一个视频的宽高为准,其他视频宽高不一致,缩放裁剪处理
|
|
|
|
|
+ int finalWidth = 720;
|
|
|
|
|
+ int finalHeight = 1280;
|
|
|
|
|
+ for (int i = 0; i < clipVideoUrls.size(); i++) {
|
|
|
|
|
+ String clipVideoUrl = clipVideoUrls.get(i);
|
|
|
|
|
+ String clipVideoFilePath = FFmpegUtil.pathCreate("/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4");
|
|
|
|
|
+ downloadFile(clipVideoUrl, clipVideoFilePath);
|
|
|
|
|
+ allLocalFilePathList.add(clipVideoFilePath);
|
|
|
|
|
+ FFmpegUtil.MediaInfo mediaInfo = FFmpegUtil.getMediaInfo(clipVideoFilePath);
|
|
|
|
|
+ if (i == 0) {
|
|
|
|
|
+ finalWidth = mediaInfo.getWidth();
|
|
|
|
|
+ finalHeight = mediaInfo.getHeight();
|
|
|
|
|
+ // 确保宽高可以被2整除
|
|
|
|
|
+ if (finalWidth % 2 != 0) {
|
|
|
|
|
+ finalWidth = finalWidth + 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (finalHeight % 2 != 0) {
|
|
|
|
|
+ finalHeight = finalHeight + 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (mediaInfo.getWidth() == finalWidth && mediaInfo.getHeight() == finalHeight) {
|
|
|
|
|
+ scaleVideoFilePathList.add(clipVideoFilePath);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ int width = mediaInfo.getWidth();
|
|
|
|
|
+ int height = mediaInfo.getHeight();
|
|
|
|
|
+ double wRate =
|
|
|
|
|
+ new BigDecimal(finalWidth).divide(new BigDecimal(width), 4, RoundingMode.HALF_UP).doubleValue();
|
|
|
|
|
+ double hRate =
|
|
|
|
|
+ new BigDecimal(finalHeight).divide(new BigDecimal(height), 4, RoundingMode.HALF_UP).doubleValue();
|
|
|
|
|
+ String clipParams;
|
|
|
|
|
+ if (wRate >= hRate) {
|
|
|
|
|
+ int cropX = 0;
|
|
|
|
|
+ int targetHeight = (finalWidth * height) / width;
|
|
|
|
|
+ if (targetHeight % 2 != 0) {
|
|
|
|
|
+ targetHeight = targetHeight + 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ int cropY = (targetHeight - finalHeight) / 2;
|
|
|
|
|
+ if (cropY < 0) {
|
|
|
|
|
+ cropY = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ clipParams =
|
|
|
|
|
+ "scale=" + finalWidth + ":" + targetHeight + ",crop=" + finalWidth + ":" + finalHeight + ":" + cropX +
|
|
|
|
|
+ ":" + cropY;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ int targetWidth = (finalHeight * width) / height;
|
|
|
|
|
+ if (targetWidth % 2 != 0) {
|
|
|
|
|
+ targetWidth = targetWidth + 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ int cropX = (targetWidth - finalWidth) / 2;
|
|
|
|
|
+ if (cropX < 0) {
|
|
|
|
|
+ cropX = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ int cropY = 0;
|
|
|
|
|
+ clipParams =
|
|
|
|
|
+ "scale=" + targetWidth + ":" + finalHeight + ",crop=" + finalWidth + ":" + finalHeight + ":" + cropX +
|
|
|
|
|
+ ":" + cropY;
|
|
|
|
|
+ }
|
|
|
|
|
+ String scaleClipVideoPath = FFmpegUtil.pathCreate("/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4");
|
|
|
|
|
+// System.out.println("clipVideoUrl:" + clipVideoUrl);
|
|
|
|
|
+ FFmpegUtil.clipVideo(clipVideoFilePath, clipParams, scaleClipVideoPath);
|
|
|
|
|
+ allLocalFilePathList.add(scaleClipVideoPath);
|
|
|
|
|
+
|
|
|
|
|
+ scaleVideoFilePathList.add(scaleClipVideoPath);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // 处理视频时间戳,避免拼接后时长不对问题
|
|
|
|
|
+ StringBuilder concatContent = new StringBuilder();
|
|
|
|
|
+ for (String scaleVideoFilePath : scaleVideoFilePathList) {
|
|
|
|
|
+ String timeScaleVideoPath = FFmpegUtil.pathCreate("/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4");
|
|
|
|
|
+ FFmpegUtil.fixVideoTime(scaleVideoFilePath, timeScaleVideoPath);
|
|
|
|
|
+ allLocalFilePathList.add(timeScaleVideoPath);
|
|
|
|
|
+ concatContent.append("file '" + timeScaleVideoPath + "'").append("\n");
|
|
|
|
|
+ }
|
|
|
|
|
+ return concatContent.toString().trim();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String buildConcatFileContentV2(List<String> clipVideoUrls, List<String> allLocalFilePathList, Integer pretreatmentSwitch) throws Exception {
|
|
|
|
|
+ List<String> scaleVideoFilePathList = new ArrayList<>();
|
|
|
|
|
+ // 以第一个视频的宽高为准,其他视频宽高不一致,缩放裁剪处理
|
|
|
|
|
+ int finalWidth = 720;
|
|
|
|
|
+ int finalHeight = 1280;
|
|
|
|
|
+ Integer finalAvgFrameRate = null;
|
|
|
|
|
+ Integer finalTimeBase = null;
|
|
|
|
|
+ for (int i = 0; i < clipVideoUrls.size(); i++) {
|
|
|
|
|
+ String clipVideoUrl = clipVideoUrls.get(i);
|
|
|
|
|
+ String clipVideoFilePath = FFmpegUtil.pathCreate("/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4");
|
|
|
|
|
+ downloadFile(clipVideoUrl, clipVideoFilePath);
|
|
|
|
|
+ allLocalFilePathList.add(clipVideoFilePath);
|
|
|
|
|
+ FFmpegUtil.MediaInfo mediaInfo = FFmpegUtil.getMediaInfo(clipVideoFilePath);
|
|
|
|
|
+ if (i == 0) {
|
|
|
|
|
+ finalWidth = mediaInfo.getWidth();
|
|
|
|
|
+ finalHeight = mediaInfo.getHeight();
|
|
|
|
|
+ finalAvgFrameRate = mediaInfo.getAvgFrameRate();
|
|
|
|
|
+ finalTimeBase = mediaInfo.getTimeBase();
|
|
|
|
|
+ }
|
|
|
|
|
+ String clipParams = "scale={finalWidth}:{finalHeight}:force_original_aspect_ratio=decrease,pad={finalWidth}:{finalHeight}:(ow-iw)/2:(oh-ih)/2";
|
|
|
|
|
+ clipParams = clipParams.replace("{finalWidth}", finalWidth + "").replace("{finalHeight}", finalHeight + "");
|
|
|
|
|
+
|
|
|
|
|
+ String scaleClipVideoPath = FFmpegUtil.pathCreate("/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4");
|
|
|
|
|
+
|
|
|
|
|
+ // 如果开启预处理,且第一个视频的帧率和时间基都有值,则进行预处理
|
|
|
|
|
+ if (Objects.equals(pretreatmentSwitch, 1)
|
|
|
|
|
+ && Objects.nonNull(finalAvgFrameRate) && Objects.nonNull(finalTimeBase)) {
|
|
|
|
|
+ clipParams = clipParams + ",fps=" + finalAvgFrameRate;
|
|
|
|
|
+ FFmpegUtil.clipAndTranscodeVideo(clipVideoFilePath, clipParams, scaleClipVideoPath, finalAvgFrameRate, finalTimeBase);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ FFmpegUtil.clipAndTranscodeVideo(clipVideoFilePath, clipParams, scaleClipVideoPath, null, null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ allLocalFilePathList.add(scaleClipVideoPath);
|
|
|
|
|
+
|
|
|
|
|
+ scaleVideoFilePathList.add(scaleClipVideoPath);
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+ StringBuilder concatContent = new StringBuilder();
|
|
|
|
|
+ for (String scaleVideoFilePath : scaleVideoFilePathList) {
|
|
|
|
|
+ concatContent.append("file '" + scaleVideoFilePath + "'").append("\n");
|
|
|
|
|
+ }
|
|
|
|
|
+ return concatContent.toString().trim();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public String videoAddTail(VideoAddTailParam param) {
|
|
|
|
|
+ long beginTime = System.currentTimeMillis();
|
|
|
|
|
+ List<String> allLocalFilePathList = new ArrayList<>();
|
|
|
|
|
+ try {
|
|
|
|
|
+ String originVideoFilePath = FFmpegUtil.pathCreate("/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4");
|
|
|
|
|
+ downloadFile(param.getOriginVideoUrl(), originVideoFilePath);
|
|
|
|
|
+ long time1 = System.currentTimeMillis();
|
|
|
|
|
+ log.info("videoAddTail,bizId:{},time1:{}", param.getBizId(), time1 - beginTime);
|
|
|
|
|
+ allLocalFilePathList.add(originVideoFilePath);
|
|
|
|
|
+ String volumeOriginVideoFilePath = originVideoFilePath;
|
|
|
|
|
+ if (Objects.nonNull(param.getOriginVideoVolume())) {
|
|
|
|
|
+ long timea = System.currentTimeMillis();
|
|
|
|
|
+ String lufsOriginVideoFilePath = FFmpegUtil.pathCreate("/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4");
|
|
|
|
|
+ allLocalFilePathList.add(lufsOriginVideoFilePath);
|
|
|
|
|
+ FFmpegUtil.changeVideoVolumeByLoudnorm(originVideoFilePath, "-16", lufsOriginVideoFilePath);
|
|
|
|
|
+ long time2 = System.currentTimeMillis();
|
|
|
|
|
+ log.info("videoAddTail,bizId:{},time2:{}", param.getBizId(), time2 - timea);
|
|
|
|
|
+
|
|
|
|
|
+ String volumeMultiple = new BigDecimal(param.getOriginVideoVolume()).divide(new BigDecimal("50"), 2,
|
|
|
|
|
+ RoundingMode.HALF_UP).toString();
|
|
|
|
|
+ if (new BigDecimal(volumeMultiple).subtract(new BigDecimal("1.00")).abs().compareTo(new BigDecimal("0.1")) > 0) {
|
|
|
|
|
+ volumeOriginVideoFilePath = FFmpegUtil.pathCreate("/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4");
|
|
|
|
|
+ allLocalFilePathList.add(volumeOriginVideoFilePath);
|
|
|
|
|
+ FFmpegUtil.changeVideoVolume(lufsOriginVideoFilePath, volumeMultiple, volumeOriginVideoFilePath);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ volumeOriginVideoFilePath = lufsOriginVideoFilePath;
|
|
|
|
|
+ }
|
|
|
|
|
+ long time3 = System.currentTimeMillis();
|
|
|
|
|
+ log.info("videoAddTail,bizId:{},time3:{}", param.getBizId(), time3 - time2);
|
|
|
|
|
+ }
|
|
|
|
|
+ FFmpegUtil.MediaInfo originVideoMediaInfo = FFmpegUtil.getMediaInfo(volumeOriginVideoFilePath);
|
|
|
|
|
+
|
|
|
|
|
+ List<String> concatFilePathList = new ArrayList<>();
|
|
|
|
|
+ concatFilePathList.add(volumeOriginVideoFilePath);
|
|
|
|
|
+ if ("video".equals(param.getTailType())) {
|
|
|
|
|
+ long timea = System.currentTimeMillis();
|
|
|
|
|
+ int finalWidth = originVideoMediaInfo.getWidth();
|
|
|
|
|
+ int finalHeight = originVideoMediaInfo.getHeight();
|
|
|
|
|
+
|
|
|
|
|
+ String tailVideoFilePath = FFmpegUtil.pathCreate("/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4");
|
|
|
|
|
+ allLocalFilePathList.add(tailVideoFilePath);
|
|
|
|
|
+ downloadFile(param.getTailVideoUrl(), tailVideoFilePath);
|
|
|
|
|
+ String volumeTailVideoFilePath = tailVideoFilePath;
|
|
|
|
|
+ if (Objects.nonNull(param.getTailVolume())) {
|
|
|
|
|
+ String lufsTailVideoFilePath = FFmpegUtil.pathCreate("/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4");
|
|
|
|
|
+ allLocalFilePathList.add(lufsTailVideoFilePath);
|
|
|
|
|
+ FFmpegUtil.changeVideoVolumeByLoudnorm(tailVideoFilePath, "-16", lufsTailVideoFilePath);
|
|
|
|
|
+
|
|
|
|
|
+ String volumeMultiple = new BigDecimal(param.getTailVolume()).divide(new BigDecimal("50"), 2,
|
|
|
|
|
+ RoundingMode.HALF_UP).toString();
|
|
|
|
|
+ if (new BigDecimal(volumeMultiple).subtract(new BigDecimal("1.00")).abs().compareTo(new BigDecimal("0.1")) > 0) {
|
|
|
|
|
+ volumeTailVideoFilePath = FFmpegUtil.pathCreate("/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4");
|
|
|
|
|
+ allLocalFilePathList.add(volumeTailVideoFilePath);
|
|
|
|
|
+ FFmpegUtil.changeVideoVolume(lufsTailVideoFilePath, volumeMultiple, volumeTailVideoFilePath);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ volumeTailVideoFilePath = lufsTailVideoFilePath;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ FFmpegUtil.MediaInfo tailVideoMediaInfo = FFmpegUtil.getMediaInfo(volumeTailVideoFilePath);
|
|
|
|
|
+ if (tailVideoMediaInfo.getWidth() == finalWidth && tailVideoMediaInfo.getHeight() == finalHeight) {
|
|
|
|
|
+ concatFilePathList.add(volumeTailVideoFilePath);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ String clipParams = buildScaleClipParams(finalWidth, finalHeight, tailVideoMediaInfo.getWidth(),
|
|
|
|
|
+ tailVideoMediaInfo.getHeight());
|
|
|
|
|
+ String scaleClipVideoPath = FFmpegUtil.pathCreate("/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4");
|
|
|
|
|
+ allLocalFilePathList.add(scaleClipVideoPath);
|
|
|
|
|
+ FFmpegUtil.clipVideo(volumeTailVideoFilePath, clipParams, scaleClipVideoPath);
|
|
|
|
|
+ concatFilePathList.add(scaleClipVideoPath);
|
|
|
|
|
+ }
|
|
|
|
|
+ long time4 = System.currentTimeMillis();
|
|
|
|
|
+ log.info("videoAddTail,bizId:{},time4:{}", param.getBizId(), time4 - timea);
|
|
|
|
|
+ } else if ("lastFrame".equals(param.getTailType())) {
|
|
|
|
|
+ long timea = System.currentTimeMillis();
|
|
|
|
|
+ // 最后一帧图片
|
|
|
|
|
+ String lastFrameFilePath = FFmpegUtil.pathCreate("/datalog/videoComposite/image/" + BaseUtils.getUUIDStr() + ".png");
|
|
|
|
|
+ allLocalFilePathList.add(lastFrameFilePath);
|
|
|
|
|
+ int lastTime = 1;
|
|
|
|
|
+ int durationSecond = originVideoMediaInfo.getDuration() / 1000;
|
|
|
|
|
+ for (int i = 0; i < 100; i++) {
|
|
|
|
|
+ FFmpegUtil.getVideoLastFrame(volumeOriginVideoFilePath, lastTime, lastFrameFilePath);
|
|
|
|
|
+ File lastFrameFile = new File(lastFrameFilePath);
|
|
|
|
|
+ if (lastFrameFile.exists()) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ lastTime = lastTime + 3;
|
|
|
|
|
+ if (lastTime >= durationSecond) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ long time5 = System.currentTimeMillis();
|
|
|
|
|
+ log.info("videoAddTail,bizId:{},time5:{}", param.getBizId(), time5 - timea);
|
|
|
|
|
+ File lastFrameFile = new File(lastFrameFilePath);
|
|
|
|
|
+ if (!lastFrameFile.exists()) {
|
|
|
|
|
+ throw new CommonException(ExceptionEnum.SYSTEM_ERROR.getCode(), "提取尾帧图片失败");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 确定片尾视频时长
|
|
|
|
|
+ Integer audioMillisDuration = 0;
|
|
|
|
|
+ String tailAudioFilePath = FFmpegUtil.pathCreate("/datalog/videoComposite/audio/" + BaseUtils.getUUIDStr() + ".mp3");
|
|
|
|
|
+ String volumeTailAudioFilePath = tailAudioFilePath;
|
|
|
|
|
+ FFmpegUtil.MediaInfo tailAudioMediaInfo = null;
|
|
|
|
|
+ if (StringUtils.hasText(param.getTailAudioUrl())) {
|
|
|
|
|
+ allLocalFilePathList.add(tailAudioFilePath);
|
|
|
|
|
+ downloadFile(param.getTailAudioUrl(), tailAudioFilePath);
|
|
|
|
|
+
|
|
|
|
|
+ if (Objects.nonNull(param.getTailVolume())) {
|
|
|
|
|
+ String lufsTailAudioFilePath = FFmpegUtil.pathCreate("/datalog/videoComposite/audio/" + BaseUtils.getUUIDStr() + ".mp3");
|
|
|
|
|
+ allLocalFilePathList.add(lufsTailAudioFilePath);
|
|
|
|
|
+ FFmpegUtil.changeMediaVolumeByLoudnorm(tailAudioFilePath, "-16", lufsTailAudioFilePath);
|
|
|
|
|
+
|
|
|
|
|
+ String volumeMultiple = new BigDecimal(param.getTailVolume()).divide(new BigDecimal("50"), 2,
|
|
|
|
|
+ RoundingMode.HALF_UP).toString();
|
|
|
|
|
+ if (new BigDecimal(volumeMultiple).subtract(new BigDecimal("1.00")).abs().compareTo(new BigDecimal("0.1")) > 0) {
|
|
|
|
|
+ volumeTailAudioFilePath = FFmpegUtil.pathCreate("/datalog/videoComposite/audio/" + BaseUtils.getUUIDStr() + ".mp3");
|
|
|
|
|
+ allLocalFilePathList.add(volumeTailAudioFilePath);
|
|
|
|
|
+ FFmpegUtil.changeMediaVolume(lufsTailAudioFilePath, volumeMultiple, volumeTailAudioFilePath);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ volumeTailAudioFilePath = lufsTailAudioFilePath;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ tailAudioMediaInfo = FFmpegUtil.getMediaInfo(volumeTailAudioFilePath);
|
|
|
|
|
+ audioMillisDuration = tailAudioMediaInfo.getDuration();
|
|
|
|
|
+ }
|
|
|
|
|
+ long time6 = System.currentTimeMillis();
|
|
|
|
|
+ log.info("videoAddTail,bizId:{},time6:{}", param.getBizId(), time6 - time5);
|
|
|
|
|
+ Integer srtMillisDuration = param.getSrtMillisDuration();
|
|
|
|
|
+ if (Objects.isNull(srtMillisDuration)) {
|
|
|
|
|
+ srtMillisDuration = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ Integer tailMillisDuration = Math.max(audioMillisDuration, srtMillisDuration);
|
|
|
|
|
+ if (tailMillisDuration == 0) {
|
|
|
|
|
+ tailMillisDuration = 3000;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String tailSecondDuration = new BigDecimal(tailMillisDuration).divide(new BigDecimal(1000), 3,
|
|
|
|
|
+ RoundingMode.HALF_UP).toString();
|
|
|
|
|
+ // 生成片尾视频
|
|
|
|
|
+ String tailVideoFilePath = FFmpegUtil.pathCreate("/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4");
|
|
|
|
|
+ allLocalFilePathList.add(tailVideoFilePath);
|
|
|
|
|
+ FFmpegUtil.imageToVideo(lastFrameFilePath, originVideoMediaInfo.getWidth(),
|
|
|
|
|
+ originVideoMediaInfo.getHeight(), tailSecondDuration, tailVideoFilePath);
|
|
|
|
|
+ long time7 = System.currentTimeMillis();
|
|
|
|
|
+ log.info("videoAddTail,bizId:{},time7:{}", param.getBizId(), time7 - time6);
|
|
|
|
|
+
|
|
|
|
|
+ // 片尾视频加音频
|
|
|
|
|
+ String lastTailVideoFilePath = tailVideoFilePath;
|
|
|
|
|
+ if (StringUtils.hasText(param.getTailAudioUrl())) {
|
|
|
|
|
+ String addAudioFilePath = FFmpegUtil.pathCreate("/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4");
|
|
|
|
|
+ allLocalFilePathList.add(addAudioFilePath);
|
|
|
|
|
+ FFmpegUtil.addAudio(tailVideoFilePath, volumeTailAudioFilePath, addAudioFilePath);
|
|
|
|
|
+ lastTailVideoFilePath = addAudioFilePath;
|
|
|
|
|
+ }
|
|
|
|
|
+ long time8 = System.currentTimeMillis();
|
|
|
|
|
+ log.info("videoAddTail,bizId:{},time8:{}", param.getBizId(), time8 - time7);
|
|
|
|
|
+
|
|
|
|
|
+ // 片尾视频加字幕
|
|
|
|
|
+ if (StringUtils.hasText(param.getAssSubtitle())) {
|
|
|
|
|
+ String assSubtitleFilePath = FFmpegUtil.pathCreate("/datalog/videoComposite/subtitle/" + BaseUtils.getUUIDStr() + ".ass");
|
|
|
|
|
+ FileUtils.saveFileContent(assSubtitleFilePath, param.getAssSubtitle().getBytes());
|
|
|
|
|
+ allLocalFilePathList.add(assSubtitleFilePath);
|
|
|
|
|
+ String inputSubtitleAndStyle = "subtitles=" + assSubtitleFilePath;
|
|
|
|
|
+ String addSubtitleFilePath = FFmpegUtil.pathCreate("/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4");
|
|
|
|
|
+ allLocalFilePathList.add(addSubtitleFilePath);
|
|
|
|
|
+ FFmpegUtil.addSubtitle(lastTailVideoFilePath, inputSubtitleAndStyle, addSubtitleFilePath);
|
|
|
|
|
+ lastTailVideoFilePath = addSubtitleFilePath;
|
|
|
|
|
+ }
|
|
|
|
|
+ concatFilePathList.add(lastTailVideoFilePath);
|
|
|
|
|
+ long time9 = System.currentTimeMillis();
|
|
|
|
|
+ log.info("videoAddTail,bizId:{},time9:{}", param.getBizId(), time9 - time8);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ long timea = System.currentTimeMillis();
|
|
|
|
|
+ // 处理视频时间戳,避免拼接后时长不对问题
|
|
|
|
|
+ StringBuilder concatContent = new StringBuilder();
|
|
|
|
|
+ for (String concatFilePath : concatFilePathList) {
|
|
|
|
|
+ String timeFixVideoPath = FFmpegUtil.pathCreate("/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4");
|
|
|
|
|
+ allLocalFilePathList.add(timeFixVideoPath);
|
|
|
|
|
+ FFmpegUtil.encodingEquivalence(concatFilePath, timeFixVideoPath);
|
|
|
|
|
+ concatContent.append("file '" + timeFixVideoPath + "'").append("\n");
|
|
|
|
|
+// concatContent.append("file '" + concatFilePath + "'").append("\n");
|
|
|
|
|
+ }
|
|
|
|
|
+ long time10 = System.currentTimeMillis();
|
|
|
|
|
+ log.info("videoAddTail,bizId:{},time10:{}", param.getBizId(), time10 - timea);
|
|
|
|
|
+ String concatFileListPath = FFmpegUtil.pathCreate("/datalog/videoComposite/filelist/" + BaseUtils.getUUIDStr() + ".txt");
|
|
|
|
|
+ FileUtils.saveFileContent(concatFileListPath, concatContent.toString().trim().getBytes());
|
|
|
|
|
+ allLocalFilePathList.add(concatFileListPath);
|
|
|
|
|
+
|
|
|
|
|
+ String concatVideoFilePath = FFmpegUtil.pathCreate("/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4");
|
|
|
|
|
+ allLocalFilePathList.add(concatVideoFilePath);
|
|
|
|
|
+ // 拼接视频
|
|
|
|
|
+ FFmpegUtil.concatVideos(concatFileListPath, concatVideoFilePath);
|
|
|
|
|
+ long time11 = System.currentTimeMillis();
|
|
|
|
|
+ log.info("videoAddTail,bizId:{},time11:{}", param.getBizId(), time11 - time10);
|
|
|
|
|
+
|
|
|
|
|
+ // 拼接完成后再进行一次编码
|
|
|
|
|
+// String outputFilePath = FFmpegUtil.pathCreate("/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4");
|
|
|
|
|
+// FFmpegUtil.videoTranscoding(concatVideoFilePath, "libx264", outputFilePath);
|
|
|
|
|
+// allLocalFilePathList.add(outputFilePath);
|
|
|
|
|
+
|
|
|
|
|
+ // 上传到oss中
|
|
|
|
|
+ String ossKey = "videoComposite/" + BaseUtils.getUUIDStr() + ".mp4";
|
|
|
|
|
+ InputStream inputStream = new FileInputStream(concatVideoFilePath);
|
|
|
|
|
+ AliOssFileTool.saveInPublic(EnumPublicBuckets.PUBBUCKET.getBucketName(), ossKey, inputStream, "video/mp4");
|
|
|
|
|
+ long time12 = System.currentTimeMillis();
|
|
|
|
|
+ log.info("videoAddTail,bizId:{},time12:{}", param.getBizId(), time12 - time11);
|
|
|
|
|
+ String result = CdnUtil.getOssHttpUrl(ossKey);
|
|
|
|
|
+ log.info("videoAddTail,bizId:{},result:{}", param.getBizId(), result);
|
|
|
|
|
+ return result;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("videoAddTail error,param:{}", JSON.toJSONString(param), e);
|
|
|
|
|
+ throw new CommonException(ExceptionEnum.SYSTEM_ERROR.getCode(), e.getMessage());
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (CollectionUtil.isNotEmpty(allLocalFilePathList)) {
|
|
|
|
|
+ for (String filePath : allLocalFilePathList) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ File file = new File(filePath);
|
|
|
|
|
+ file.delete();
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("delete file error,", filePath);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String buildScaleClipParams(int finalWidth, int finalHeight, int width, int height) {
|
|
|
|
|
+ double wRate =
|
|
|
|
|
+ new BigDecimal(finalWidth).divide(new BigDecimal(width), 4, RoundingMode.HALF_UP).doubleValue();
|
|
|
|
|
+ double hRate =
|
|
|
|
|
+ new BigDecimal(finalHeight).divide(new BigDecimal(height), 4, RoundingMode.HALF_UP).doubleValue();
|
|
|
|
|
+ String clipParams;
|
|
|
|
|
+ if (wRate >= hRate) {
|
|
|
|
|
+ int cropX = 0;
|
|
|
|
|
+ int targetHeight = (finalWidth * height) / width;
|
|
|
|
|
+ if (targetHeight % 2 != 0) {
|
|
|
|
|
+ targetHeight = targetHeight + 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ int cropY = (targetHeight - finalHeight) / 2;
|
|
|
|
|
+ if (cropY < 0) {
|
|
|
|
|
+ cropY = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ clipParams =
|
|
|
|
|
+ "scale=" + finalWidth + ":" + targetHeight + ",crop=" + finalWidth + ":" + finalHeight + ":" + cropX +
|
|
|
|
|
+ ":" + cropY;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ int targetWidth = (finalHeight * width) / height;
|
|
|
|
|
+ if (targetWidth % 2 != 0) {
|
|
|
|
|
+ targetWidth = targetWidth + 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ int cropX = (targetWidth - finalWidth) / 2;
|
|
|
|
|
+ if (cropX < 0) {
|
|
|
|
|
+ cropX = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ int cropY = 0;
|
|
|
|
|
+ clipParams =
|
|
|
|
|
+ "scale=" + targetWidth + ":" + finalHeight + ",crop=" + finalWidth + ":" + finalHeight + ":" + cropX +
|
|
|
|
|
+ ":" + cropY;
|
|
|
|
|
+ }
|
|
|
|
|
+ return clipParams;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String getCutTime(Integer keyFrameTime) {
|
|
|
|
|
+ Integer second = (keyFrameTime / 1000) % 60;
|
|
|
|
|
+ Integer minute = (keyFrameTime / (1000 * 60)) % 60;
|
|
|
|
|
+ Integer hour = (keyFrameTime / (1000 * 60 * 60));
|
|
|
|
|
+ return hour + ":" + (minute < 10 ? "0" + minute : minute) + ":" + (second < 10 ? "0" + second : second);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public String videoTranscoding(VideoTranscodingParam param) {
|
|
|
|
|
+ List<String> allLocalFilePathList = new ArrayList<>();
|
|
|
|
|
+ try {
|
|
|
|
|
+
|
|
|
|
|
+ String originVideoFilePath = FFmpegUtil.pathCreate("/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4");
|
|
|
|
|
+ downloadFile(param.getVideoUrl(), originVideoFilePath);
|
|
|
|
|
+ allLocalFilePathList.add(originVideoFilePath);
|
|
|
|
|
+
|
|
|
|
|
+ String outputFilePath = FFmpegUtil.pathCreate("/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4");
|
|
|
|
|
+ allLocalFilePathList.add(outputFilePath);
|
|
|
|
|
+
|
|
|
|
|
+ FFmpegUtil.videoTranscoding(originVideoFilePath, param.getCoding(), param.getBitrate(), outputFilePath);
|
|
|
|
|
+
|
|
|
|
|
+ // 上传到oss中
|
|
|
|
|
+ String ossKey = "videoComposite/" + BaseUtils.getUUIDStr() + ".mp4";
|
|
|
|
|
+ InputStream inputStream = new FileInputStream(outputFilePath);
|
|
|
|
|
+ AliOssFileTool.saveInPublic(EnumPublicBuckets.PUBBUCKET.getBucketName(), ossKey, inputStream, "video/mp4");
|
|
|
|
|
+ return CdnUtil.getOssHttpUrl(ossKey);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("videoTranscoding error,param:{}", JSON.toJSONString(param), e);
|
|
|
|
|
+ throw new CommonException(ExceptionEnum.SYSTEM_ERROR.getCode(), e.getMessage());
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (CollectionUtil.isNotEmpty(allLocalFilePathList)) {
|
|
|
|
|
+ for (String filePath : allLocalFilePathList) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ File file = new File(filePath);
|
|
|
|
|
+ file.delete();
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("delete file error,", filePath);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public String videoAddAudio(VideoAddAudioParam param) {
|
|
|
|
|
+ List<String> allLocalFilePathList = new ArrayList<>();
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 先判断下视频文件大小,文件太大可能会导致OOM
|
|
|
|
|
+ long videoSize = getVideoSize(param.getVideoUrl());
|
|
|
|
|
+ if (videoSize > 500 * 1024 * 1024) {
|
|
|
|
|
+ throw new CommonException(ExceptionEnum.SYSTEM_ERROR.getCode(), "视频文件太大,不能超过500M");
|
|
|
|
|
+ }
|
|
|
|
|
+ String videoFilePath = "/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4";
|
|
|
|
|
+ downloadFile(param.getVideoUrl(), videoFilePath);
|
|
|
|
|
+ allLocalFilePathList.add(videoFilePath);
|
|
|
|
|
+ String volumeVideoFilePath = videoFilePath;
|
|
|
|
|
+ if (Objects.nonNull(param.getVideoVolume()) && Boolean.TRUE.equals(param.getKeepVideoOriginalSound())) {
|
|
|
|
|
+ String lufsVideoFilePath = "/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4";
|
|
|
|
|
+ allLocalFilePathList.add(lufsVideoFilePath);
|
|
|
|
|
+ FFmpegUtil.changeVideoVolumeByLoudnorm(videoFilePath, "-16", lufsVideoFilePath);
|
|
|
|
|
+
|
|
|
|
|
+ String volumeMultiple = new BigDecimal(param.getVideoVolume()).divide(new BigDecimal("50"), 2,
|
|
|
|
|
+ RoundingMode.HALF_UP).toString();
|
|
|
|
|
+ if (new BigDecimal(volumeMultiple).subtract(new BigDecimal("1.00")).abs().compareTo(new BigDecimal("0.1")) > 0) {
|
|
|
|
|
+ volumeVideoFilePath = "/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4";
|
|
|
|
|
+ allLocalFilePathList.add(volumeVideoFilePath);
|
|
|
|
|
+ FFmpegUtil.changeVideoVolume(lufsVideoFilePath, volumeMultiple, volumeVideoFilePath);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ volumeVideoFilePath = lufsVideoFilePath;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String audioFilePath = "/datalog/videoComposite/audio/" + BaseUtils.getUUIDStr() + ".mp3";
|
|
|
|
|
+ downloadFile(param.getAudioUrl(), audioFilePath);
|
|
|
|
|
+ allLocalFilePathList.add(audioFilePath);
|
|
|
|
|
+ String volumeAudioFilePath = audioFilePath;
|
|
|
|
|
+ if (Objects.nonNull(param.getAudioVolume())) {
|
|
|
|
|
+ String lufsAudioFilePath = "/datalog/videoComposite/audio/" + BaseUtils.getUUIDStr() + ".mp3";
|
|
|
|
|
+ allLocalFilePathList.add(lufsAudioFilePath);
|
|
|
|
|
+ FFmpegUtil.changeMediaVolumeByLoudnorm(audioFilePath, "-16", lufsAudioFilePath);
|
|
|
|
|
+
|
|
|
|
|
+ String volumeMultiple = new BigDecimal(param.getAudioVolume()).divide(new BigDecimal("50"), 2,
|
|
|
|
|
+ RoundingMode.HALF_UP).toString();
|
|
|
|
|
+ if (new BigDecimal(volumeMultiple).subtract(new BigDecimal("1.00")).abs().compareTo(new BigDecimal("0.1")) > 0) {
|
|
|
|
|
+ volumeAudioFilePath = "/datalog/videoComposite/audio/" + BaseUtils.getUUIDStr() + ".mp3";
|
|
|
|
|
+ allLocalFilePathList.add(volumeAudioFilePath);
|
|
|
|
|
+ FFmpegUtil.changeMediaVolume(lufsAudioFilePath, volumeMultiple, volumeAudioFilePath);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ volumeAudioFilePath = lufsAudioFilePath;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String outputFilePath = "/datalog/videoComposite/video/" + BaseUtils.getUUIDStr() + ".mp4";
|
|
|
|
|
+
|
|
|
|
|
+ if (StringUtils.hasText(param.getMainTimeline()) && "audio".equals(param.getMainTimeline())) {
|
|
|
|
|
+ FFmpegUtil.MediaInfo videoMediaInfo = FFmpegUtil.getMediaInfo(volumeVideoFilePath);
|
|
|
|
|
+ FFmpegUtil.MediaInfo audioMediaInfo = FFmpegUtil.getMediaInfo(volumeAudioFilePath);
|
|
|
|
|
+ int loopCount = audioMediaInfo.getDuration() / videoMediaInfo.getDuration() - 1;
|
|
|
|
|
+ if (loopCount >= 0) {
|
|
|
|
|
+ int mod = audioMediaInfo.getDuration() % videoMediaInfo.getDuration();
|
|
|
|
|
+ if (mod > 0) {
|
|
|
|
|
+ loopCount++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (loopCount < 0) {
|
|
|
|
|
+ loopCount = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (Boolean.TRUE.equals(param.getKeepVideoOriginalSound())) {
|
|
|
|
|
+ FFmpegUtil.videoAmixAudioLoopVideoAndShortest(volumeVideoFilePath, volumeAudioFilePath, outputFilePath, loopCount);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ FFmpegUtil.addAudioLoopVideoAndShortest(volumeVideoFilePath, volumeAudioFilePath, outputFilePath, loopCount);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (StringUtils.hasText(param.getMainTimeline()) && "video".equals(param.getMainTimeline())) {
|
|
|
|
|
+ FFmpegUtil.MediaInfo videoMediaInfo = FFmpegUtil.getMediaInfo(volumeVideoFilePath);
|
|
|
|
|
+ FFmpegUtil.MediaInfo audioMediaInfo = FFmpegUtil.getMediaInfo(volumeAudioFilePath);
|
|
|
|
|
+ int loopCount = videoMediaInfo.getDuration() / audioMediaInfo.getDuration() - 1;
|
|
|
|
|
+ if (loopCount >= 0) {
|
|
|
|
|
+ int mod = videoMediaInfo.getDuration() % audioMediaInfo.getDuration();
|
|
|
|
|
+ if (mod > 0) {
|
|
|
|
|
+ loopCount++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (loopCount < 0) {
|
|
|
|
|
+ loopCount = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (Boolean.TRUE.equals(param.getKeepVideoOriginalSound())) {
|
|
|
|
|
+ FFmpegUtil.videoAmixAudioLoopAudioAndShortest(volumeVideoFilePath, volumeAudioFilePath, outputFilePath, loopCount);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ FFmpegUtil.addAudioLoopAudioAndShortest(volumeVideoFilePath, volumeAudioFilePath, outputFilePath, loopCount);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (Boolean.TRUE.equals(param.getKeepVideoOriginalSound())) {
|
|
|
|
|
+ FFmpegUtil.videoAmixAudio(volumeVideoFilePath, volumeAudioFilePath, outputFilePath);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ FFmpegUtil.addAudio(volumeVideoFilePath, volumeAudioFilePath, outputFilePath);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ allLocalFilePathList.add(outputFilePath);
|
|
|
|
|
+ // 上传到oss中
|
|
|
|
|
+ String ossKey = "videoComposite/" + BaseUtils.getUUIDStr() + ".mp4";
|
|
|
|
|
+ InputStream inputStream = new FileInputStream(outputFilePath);
|
|
|
|
|
+ AliOssFileTool.saveInPublic(EnumPublicBuckets.PUBBUCKET.getBucketName(), ossKey, inputStream, "video/mp4");
|
|
|
|
|
+ return CdnUtil.getOssHttpUrl(ossKey);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("videoAddAudio error,param:{}", JSON.toJSONString(param), e);
|
|
|
|
|
+ throw new CommonException(ExceptionEnum.SYSTEM_ERROR.getCode(), e.getMessage());
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (CollectionUtil.isNotEmpty(allLocalFilePathList)) {
|
|
|
|
|
+ for (String filePath : allLocalFilePathList) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ File file = new File(filePath);
|
|
|
|
|
+ file.delete();
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("delete file error,", filePath);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private long getVideoSize(String url) {
|
|
|
|
|
+ HttpResponseContent hrc = HttpClientUtils.head(url);
|
|
|
|
|
+ long videoSize = 0;
|
|
|
|
|
+ for (Header header : hrc.getHeaders()) {
|
|
|
|
|
+ if ("content-length".equals(header.getName().toLowerCase())) {
|
|
|
|
|
+ videoSize = Long.valueOf(header.getValue());
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return videoSize;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private int extractNumber(String filename) {
|
|
|
|
|
+ int lastUnderscore = filename.lastIndexOf('_');
|
|
|
|
|
+ if (lastUnderscore != -1 && lastUnderscore < filename.length() - 1) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ String numberStr = filename.substring(lastUnderscore + 1);
|
|
|
|
|
+ return Integer.parseInt(numberStr);
|
|
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
|
|
+ // 忽略解析错误
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return Integer.MAX_VALUE;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ public static void main(String[] args) throws Exception {
|
|
|
|
|
+ FFmpegServiceImpl impl = new FFmpegServiceImpl();
|
|
|
|
|
+ String url = "http://res.cybertogether.net/crawler/video/24d4055041c1625f31ac464428c4d324.mp4";
|
|
|
|
|
+ String mainVideoFilePath = "/datalog/videoComposite/video/111.mp4";
|
|
|
|
|
+ impl.downloadFile(url, mainVideoFilePath);
|
|
|
|
|
+ System.out.println("finish");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|