io流转换为Multipart文件
个人的话是运用到了minio文件服务器保存文件,前端(vue)异步上传文件后,由于要提升用户体验效果,先上传文件到后台服务器,返回视频在文件服务器的link()参数,然后保存该文件到数据库,然后运用java 定时任务之一 @Scheduled注解(如何使用自行www.baidu.com),定时进行在后台转码相关视频文件。运用到的转码工具是FFmpeg.exe。
详细如下:
- 新建一份工具类文件,进行转码操作(亲测转码各参数都有用),个人的话是统一转码为MP4文件。
package org.springblade.common.utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springblade.common.config.loadFileConfig; import org.springblade.common.service.ResourceService; import org.springblade.core.log.exception.ServiceException; import org.springblade.core.tool.api.R; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.multipart.MultipartFile; import java.io.*; import java.util.ArrayList; import java.util.Date; import java.util.List; import static jodd.io.FileUtil.deleteFile; /** * Created by liuminghui on 2020/12/30 22:14 * * @annotate: * @Version 1.0 */ public class DisposeVideoUtils { private static final Logger logger = LoggerFactory.getLogger(DisposeVideoUtils.class); // 转码ffmpeg.exe工具路径 private static final String FFMPEG_PATH = "F:\\gugexaizai\\acutvideo\\ffmpeg.exe"; // 转码mencoder.exe工具路径 private static final String MENCODER_PATH = "F:\\gugexaizai\\acutvideo\\mencoder.exe"; // 临时的视频存储路径,转码完成后可删除 private static final String TEMPORARY_VIDEO_PATH = "F:\\Images\\"; // 转码成功mp4视频存放路径 private static final String uploadFolder="F:\\ImagesPath\\"; // 转码后台的视频访问路径 private String videoUrl; // 视频大小 private String size; private String filerealname; private String PATH; // 视频截图路径 private String videoImg; public DisposeVideoUtils() { } //重构构造方法,传入视频存放路径 public DisposeVideoUtils(String path) { videoUrl = path; } //set和get方法传递path public String getVideoPath() { return videoUrl; } public String getSize() { return size; } public void setSize(String size) { this.size = size; } public void setVideoPath(String path) { videoUrl = path; } public String getVideoImg() { return videoImg; } public void setVideoImg(String videoImg) { this.videoImg = videoImg; } /** * 转码、截图和删除源文件功能 * @param tempPath 临时源视频文件路径 */ // public String runConver(String tempPath) { // logger.info("=================转码过程开始====================="); // try { // String targetExtension = ".mp4"; // 设置转换的格式 // boolean isDelSourseFile = true; // // 转码、截图和删除源文件 // boolean beginConver = beginVideoConver(tempPath, targetExtension, isDelSourseFile); // if (beginConver) { // logger.info("=================转码过程彻底结束====================="); // return videoUrl; // } // } catch (Exception e) { // e.printStackTrace(); // } // return null; // } /** * 视频转码、截图和删除原视频处理 * @param tempPath 临时源视频路径 * @param targetExtension 转码成功的视频后缀名 * @param isDelSourseFile 转换完成后是否删除源文件 * @return */ // private boolean beginVideoConver(String tempPath, String targetExtension, boolean isDelSourseFile) { // File file = new File(tempPath); // String fileName = file.getName(); //获取文件名+后缀名 // String fileNameSuffix = fileName.substring(fileName.lastIndexOf(".") + 1); // 获取需要转码的文件后缀 // //执行转码机制 // if (process(tempPath, fileNameSuffix, targetExtension)) { // //删除临时视频或转码原视频 // if (isDelSourseFile) { // // 删除临时文件 // File fileDelete = new File(TEMPORARY_VIDEO_PATH); // String[] tempList = fileDelete.list(); // File temp = null; // for (int i = 0; i < tempList.length; i++) { // if (TEMPORARY_VIDEO_PATH.endsWith(File.separator)) { // temp = new File(TEMPORARY_VIDEO_PATH + tempList[i]); // } else { // temp = new File(TEMPORARY_VIDEO_PATH + File.separator + tempList[i]); // } // if (temp.isFile() || temp.isDirectory()) { // temp.delete(); // 删除文件夹里面的文件 // } // } // } // // return true; // } else { // return false; // } // } /** * 实际转换视频格式的方法 * @param tempPath 临时源视频文件路径 * @param fileNameSuffix 源视频后缀名 * @param targetExtension 转码成功的视频后缀名 * @return */ // private boolean process(String tempPath, String fileNameSuffix, String targetExtension) { // //先判断视频的类型-返回状态码 // int type = checkVideoSuffix(fileNameSuffix); // boolean status = false; // // //根据状态码处理 // if (type == 0) { // logger.info("ffmpeg可以转换,统一转为mp4文件"); // String status = processVideoFormat(tempPath, targetExtension);//可以指定转换为什么格式的视频 // } else if (type == 1) { // //如果type为1,将其他文件先转换为avi,然后在用ffmpeg转换为指定格式 // logger.info("ffmpeg不可以转换,先调用mencoder转码avi"); // String avifilepath = processAVI(tempPath); // // if (avifilepath == null){ // // 转码失败--avi文件没有得到 // logger.info("mencoder转码失败,未生成AVI文件"); // return false; // }else { // logger.info("生成AVI文件成功,ffmpeg开始转码:"); // String status = processVideoFormat(avifilepath, targetExtension); // } // } // return status; //执行完成返回布尔类型true // } /** * 转换为指定格式 * ffmpeg能解析的格式:(asx,asf,mpg,wmv,3gp,mp4,mov,avi,flv等) * @param oldfilepath 临时源视频文件路径 * @param targetExtension 转码成功的视频后缀名 .xxx * @return */ public String processVideoFormat(String oldfilepath, String targetExtension) { logger.info("调用了ffmpeg.exe工具"); // 先确保保存转码后的视频的mp4文件夹存在:TRANSCODE_VIDEO_MP4PATH-保存路径 String mp4Path = uploadFolder + Long.toString(new Date().getTime()) + targetExtension; File tempFile = new File(uploadFolder); if (tempFile.exists()) { if (tempFile.isDirectory()) { logger.info("该转码后的文件夹已存在"); } else { logger.info("同名的转码后的文件存在,不能创建文件夹"); } } else { // 创建目录 if (tempFile.mkdirs()) { logger.info("创建目录转码后的文件夹:" + uploadFolder + ",成功!"); } else { logger.info("创建目录转码后的文件夹:" + uploadFolder + ",失败!"); } } List<String> commend = new ArrayList<String>(); commend.add(FFMPEG_PATH); // 添加转换工具路径 commend.add("-i"); // 添加参数"-i",该参数指定要转换的文件 commend.add(oldfilepath); // 添加要转换格式的视频文件的路径 commend.add("-vcodec"); commend.add("libx264"); commend.add("-acodec"); commend.add("aac"); commend.add("-ab"); commend.add("128k"); commend.add("-ar"); commend.add("16k"); commend.add("-ac"); commend.add("2"); commend.add("-f"); // 添加参数"-y",该参数指定将覆盖已存在的文件 commend.add("mp4"); // 添加参数"-y",该参数指定将覆盖已存在的文件 commend.add("-y"); // 添加参数"-y",该参数指定将覆盖已存在的文件 commend.add(mp4Path); // 打印命令 StringBuffer test = new StringBuffer(); for (int i = 0; i < commend.size(); i++) { test.append(commend.get(i) + " "); } logger.info("ffmpeg输入的命令:" + test); try { String suffix = oldfilepath.substring(oldfilepath.lastIndexOf(".") + 1); if(checkVideoSuffix(suffix)==0){ // 多线程处理加快速度-解决rmvb数据丢失builder名称要相同 ProcessBuilder builder = new ProcessBuilder(); builder.command(commend); builder.redirectErrorStream(true); Process process = builder.start(); // 多线程处理加快速度-解决数据丢失 // 线程处理 threadSyn(process); process.waitFor(); // 进程等待机制,必须要有,否则不生成mp4!!! logger.info("生成mp4视频为:{}", mp4Path); // 生成mp4视频保存路径 setVideoPath(mp4Path); logger.info("===============视频转码结束================="); return mp4Path; } else if(checkVideoSuffix(suffix)==200){ return oldfilepath; } else{ return ("该文件暂不支持转换"); } } catch (Exception e) { e.printStackTrace(); return mp4Path; } } /** * 对ffmpeg无法解析的文件格式(wmv9,rm,rmvb等), * 可以先用(mencoder)转换为avi(ffmpeg能解析的)格式.再用ffmpeg解析为指定格式 * @param oldfilepath 临时源视频文件路径 * @return */ private String processAVI(String oldfilepath) { logger.info("调用了mencoder.exe工具"); String tempPath = TEMPORARY_VIDEO_PATH + Long.toString(new Date().getTime()); List<String> commend = new ArrayList<String>(); commend.add(MENCODER_PATH); // 指定mencoder.exe工具的位置 commend.add(oldfilepath); // 指定源视频的位置 commend.add("-oac"); commend.add("mp3lame"); // lavc 原mp3lame commend.add("-lameopts"); commend.add("preset=64"); commend.add("-ovc"); commend.add("xvid"); // mpg4(xvid),AVC(h.264/x264),只有h264才是公认的MP4标准编码,如果ck播放不了,就来调整这里 commend.add("-xvidencopts"); // xvidencopts或x264encopts commend.add("bitrate=600"); // 600或440 commend.add("-of"); commend.add("avi"); commend.add("-o"); commend.add(tempPath + ".avi"); // 存放路径+名称,生成.avi视频 // 打印出转换命令 StringBuffer test = new StringBuffer(); for (int i = 0; i < commend.size(); i++) { test.append(commend.get(i) + " "); } logger.info("mencoder输入的命令:" + test); try { // 调用线程命令启动转码 ProcessBuilder builder = new ProcessBuilder(); builder.command(commend); Process process = builder.start(); // 多线程处理加快速度-解决数据丢失 // 线程处理 threadSyn(process); // 等Mencoder进程转换结束,再调用ffmepg进程非常重要!!! process.waitFor(); logger.info("Mencoder进程结束"); return tempPath + ".avi"; // 返回转为AVI以后的视频地址 } catch (Exception e) { e.printStackTrace(); return null; } } /** * 视频截图功能 * @param sourceVideoPath 需要被截图的视频路径(包含文件名和后缀名) * @return */ public boolean processImg(String sourceVideoPath) { String imageFile = "images/videoFile/"; String fileImpPath = ResourceService.rb.getString("filePath"); // 先确保保存截图的文件夹存在 File tempFile = new File(fileImpPath + imageFile); if (tempFile.exists()) { if (tempFile.isDirectory()) { logger.info("该截图保存文件夹存在。"); } else { logger.info("同名的截图保存文件存在,不能创建文件夹。"); } } else { logger.info("截图保存文件夹不存在,创建该文件夹。"); tempFile.mkdir(); } File file = new File(sourceVideoPath); String fileName = file.getName(); // 获取视频文件的名称。 String fileRealName = fileName.substring(0, fileName.lastIndexOf(".")); // 获取不加后缀名的视频名 imageFile = imageFile + fileRealName + ".jpg"; List<String> commend = new ArrayList<String>(); // 第一帧: 00:00:01 // 截图命令:time ffmpeg -ss 00:00:01 -i test1.flv -f image2 -y test1.jpg commend.add(FFMPEG_PATH); // 指定ffmpeg工具的路径 commend.add("-ss"); commend.add("00:00:03"); // 3是代表第3秒的时候截图 commend.add("-i"); commend.add(sourceVideoPath); // 截图的视频路径 commend.add("-f"); commend.add("image2"); commend.add("-y"); commend.add(fileImpPath + imageFile); // 生成截图xxx.jpg // 打印截图命令 StringBuffer test = new StringBuffer(); for (int i = 0; i < commend.size(); i++) { test.append(commend.get(i) + " "); } logger.info("截图命令:" + test); // 转码后完成截图功能-还是得用线程来解决 try { // 调用线程处理命令 ProcessBuilder builder = new ProcessBuilder(); builder.command(commend); Process process = builder.start(); // 线程处理 threadSyn(process); // 等Mencoder进程转换结束,再调用ffmepg进程非常重要!!! process.waitFor(); setVideoImg(imageFile); // 视频大小 String videoSize = FileUploadToolUtil.getSize(file); setSize(videoSize); logger.info("截图进程结束,视频大小:{}", getSize()); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 多线程处理 * @param process */ public void threadSyn(Process process) { // 获取进程的标准输入流 final InputStream is1 = process.getInputStream(); // 获取进程的错误流 final InputStream is2 = process.getErrorStream(); // 启动两个线程,一个线程负责读标准输出流,另一个负责读标准错误流 new Thread() { public void run() { BufferedReader br = new BufferedReader(new InputStreamReader(is1)); try { String lineB = null; while ((lineB = br.readLine()) != null) { if (lineB != null) { logger.info(lineB); //必须取走线程信息避免堵塞 } } } catch (IOException e) { e.printStackTrace(); } // 关闭流 finally { try { is1.close(); } catch (IOException e) { e.printStackTrace(); } } } }.start(); new Thread() { public void run() { BufferedReader br2 = new BufferedReader(new InputStreamReader(is2)); try { String lineC = null; while ((lineC = br2.readLine()) != null) { if (lineC != null) { logger.info(lineC); //必须取走线程信息避免堵塞 } } } catch (IOException e) { e.printStackTrace(); } // 关闭流 finally { try { is2.close(); } catch (IOException e) { e.printStackTrace(); } } } }.start(); } /** * 检查文件类型,检查非MP4后缀 * asx,asf,mpg,wmv,3gp,mp4,mov,avi,flv等使用ffmpeg能解析 * wmv9,rm,rmvb等ffmpeg无法解析,先用别的工具(mencoder)转换为avi(ffmpeg能解析的)格式 * @param suffixName 后缀名 * @return */ public int checkVideoSuffix(String suffixName) { if (suffixName.equals("avi")) { return 0; } else if (suffixName.equals("mpg")) { return 0; } else if (suffixName.equals("wmv")) { return 0; } else if (suffixName.equals("3gp")) { return 0; } else if (suffixName.equals("mov")) { return 0; } else if (suffixName.equals("asf")) { return 0; } else if (suffixName.equals("asx")) { return 0; } else if (suffixName.equals("flv")) { return 0; } else if (suffixName.equals("mkv")) { return 0; } else if (suffixName.equals("wmv9")) { return 1; } else if (suffixName.equals("rm")) { return 1; } else if (suffixName.equals("rmvb")) { return 1; } else if (suffixName.equals("mp4")) { // MP4不转码 return 200; } return 9; } /** * 视频写入本地磁盘/服务器 * @param file 上传文件 * @param filePath 存储位置 * @param fileName 文件名称 * @return */ public boolean uploadVideo(MultipartFile file, String filePath, String fileName) { // 上传到本地磁盘/服务器 try { logger.info("上传的视频写入本地磁盘/服务器"); InputStream is = file.getInputStream(); OutputStream os = new FileOutputStream(new File(filePath, fileName)); int len = 0; byte[] buffer = new byte[2048]; while ((len = is.read(buffer)) != -1) { os.write(buffer, 0, len); } os.close(); os.flush(); is.close(); return true; } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return false; } // public void run() { // try { // // 转换并截图 // String filePath = "D:\\video\\old\\test.avi"; // DisposeVideoUtils cv = new DisposeVideoUtils(filePath); // cv.beginConver(); // // // 仅截图 // // ProcessFlvImg pfi = new ProcessFlvImg(); // // pfi.processImg("D:\\video\\old\\test.avi"); // // } catch (Exception e) { // e.printStackTrace(); // } // } private boolean processFLV(String oldfilepath) { if (!checkfile(PATH)) { System.out.println(oldfilepath + " is not file"); return false; } List commend = new java.util.ArrayList(); commend.add(FFMPEG_PATH); commend.add("-i"); commend.add(oldfilepath); commend.add("-vcodec"); commend.add("libx264"); commend.add("-acodec"); commend.add("aac"); commend.add("-s"); commend.add("1280x720"); commend.add("-vprofile"); commend.add("high"); // commend.add("-vlevel"); // commend.add("3.1"); // commend.add("-coder"); // commend.add("1"); // -vcodec libx264 -acodec aac -ab 128k -ar 16k -ac 2 -f mp4 -y // commend.add("-movflags"); // commend.add("faststart"); // commend.add("-force_key_frames"); // commend.add("1"); // commend.add("-strict"); // commend.add("experimental"); commend.add("-r"); commend.add("25"); commend.add("-g"); commend.add("30"); commend.add("-bf"); commend.add("2"); commend.add("-ab"); commend.add("128k"); commend.add("-ar"); commend.add("44100"); commend.add("-ac"); commend.add("2"); // commend.add("-b:v"); // commend.add("1.6M"); // commend.add("-sc_threshold"); // commend.add("0"); commend.add("-f"); // 添加参数"-y",该参数指定将覆盖已存在的文件 commend.add("mp4"); // 添加参数"-y",该参数指定将覆盖已存在的文件 commend.add("-y"); commend.add(uploadFolder + filerealname + ".mp4"); try { ProcessBuilder builder = new ProcessBuilder(); String cmd = commend.toString(); builder.command(commend); Process p = builder.start(); doWaitFor(p); p.destroy(); deleteFile(oldfilepath); return true; } catch (Exception e) { e.printStackTrace(); return false; } } public void deleteFile(String filepath) { File file = new File(filepath); if (PATH.equals(filepath)) { if (file.delete()) { System.out.println("文件" + filepath + "已删除"); } } else { if (file.delete()) { System.out.println("文件" + filepath + "已删除 "); } File filedelete2 = new File(PATH); if (filedelete2.delete()) { System.out.println("文件" + PATH + "已删除"); } } } private boolean checkfile(String path) { File file = new File(path); if (!file.isFile()) { return false; } else { return true; } } public int doWaitFor(Process p) { InputStream in = null; InputStream err = null; int exitValue = -1; // returned to caller when p is finished try { System.out.println("comeing"); in = p.getInputStream(); err = p.getErrorStream(); boolean finished = false; // Set to true when p is finished while (!finished) { try { while (in.available() > 0) { Character c = new Character((char) in.read()); System.out.print(c); } while (err.available() > 0) { Character c = new Character((char) err.read()); System.out.print(c); } exitValue = p.exitValue(); finished = true; } catch (IllegalThreadStateException e) { Thread.currentThread().sleep(500); } } } catch (Exception e) { System.err.println("doWaitFor();: unexpected exception - " + e.getMessage()); } finally { try { if (in != null) { in.close(); } } catch (IOException e) { System.out.println(e.getMessage()); } if (err != null) { try { err.close(); } catch (IOException e) { System.out.println(e.getMessage()); } } } return exitValue; } }
2. 将视频文件转码保存在本地之后,因为没有前端操作,一切都是在后台默默的进行,所以我们要读取该文件(保存在了本地磁盘),这必将是通过io流读取文件并获得,但是由于minio的上传方法要求是Multipart文件,所以将io流转换为Multipart文件就可以了。
/** * * @Description io流转换为MultipartFile---------返回MultipartFile文件 * @return org.springframework.web.multipart.MultipartFile * @date 2020/12/30 * @auther liuminghui */ public static MultipartFile getFile( String filePath) throws IOException { File file = new File(filePath); FileItem fileItem = new DiskFileItem("copyfile.txt", Files.probeContentType(file.toPath()),false,file.getName(),(int)file.length(),file.getParentFile()); byte[] buffer = new byte[4096]; int n; try (InputStream inputStream = new FileInputStream(file); OutputStream os = fileItem.getOutputStream()){ while ( (n = inputStream.read(buffer,0,4096)) != -1){ os.write(buffer,0,n); } //也可以用IOUtils.copy(inputStream,os); MultipartFile multipartFile = new CommonsMultipartFile(fileItem); System.out.println(multipartFile.getName()); return multipartFile; }catch (IOException e){ e.printStackTrace(); } return null; }
3, 大致就是这样,还有很多文件的参数的方法都可以在各大网站上搜索到的,这里纯属是记录一下工作中遇到的各种问题和最终解决,感谢平台!