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, 大致就是這樣,還有很多文件的參數的方法都可以在各大網站上搜索到的,這里純屬是記錄一下工作中遇到的各種問題和最終解決,感謝平台!