最近因爲項目須要,研究了一下如何用Java實現視頻轉換,「着實」廢了點心思,整理整理,寫出給本身備忘下。html
思路java
因爲以前沒有無法過相關功能的經驗,一開始來真不知道從哪裏入手。固然,這個解決,google一下立馬就發現了ffmpeg,網上講解用Java+ffmpeg來進行視頻轉換的文章也不在少數,我主要參考的這篇文章。編程
上文提到的這篇文章,基本已經把開發流程什麼的講的很清楚了,這裏總結下:緩存
1)核心是利用ffmpeg進行視頻轉換,咱們本身並不寫轉換視頻的代碼,只是調用ffmpeg,它會幫咱們完成視頻的轉換。ffmpeg支持的類型有:asx,asf,mpg,wmv,3gp,mp4,mov,avi,flv等,這些類型,能夠利用ffmpeg進行直接轉換。ffmpeg不支持的類型有:wmv9,rm,rmvb等,這些類型須要先用別的工具(mencoder)轉換爲avi(ffmpeg能解析的)格式。dom
2)瞭解Java如何調用外部程序,這會是最困難的,也會是坑最多的地方。ide
3)根據咱們的需求設置ffmpeg的參數。(這類文章網上已經有不少了,我也不用複製黏貼了,見這裏)函數
代碼工具
上文中提到的那篇文章中的代碼其實已經寫的很友好了,基本拿來就能用,不過仍然存在許多問題,接下來會講到,下面是文中的代碼:post
1 import java.io.File; 2 import java.util.ArrayList; 3 import java.util.Calendar; 4 import java.util.List; 5 6 public class ConvertVideo { 7 8 private final static String PATH = "c:\\ffmpeg\\input\\c.mp4"; 9 10 public static void main(String[] args) { 11 if (!checkfile(PATH)) { 12 System.out.println(PATH + " is not file"); 13 return; 14 } 15 if (process()) { 16 System.out.println("ok"); 17 } 18 } 19 20 private static boolean process() { 21 int type = checkContentType(); 22 boolean status = false; 23 if (type == 0) { 24 System.out.println("直接將文件轉爲flv文件"); 25 status = processFLV(PATH);// 直接將文件轉爲flv文件 26 } else if (type == 1) { 27 String avifilepath = processAVI(type); 28 if (avifilepath == null) 29 return false;// avi文件沒有獲得 30 status = processFLV(avifilepath);// 將avi轉爲flv 31 } 32 return status; 33 } 34 35 private static int checkContentType() { 36 String type = PATH.substring(PATH.lastIndexOf(".") + 1, PATH.length()) 37 .toLowerCase(); 38 // ffmpeg能解析的格式:(asx,asf,mpg,wmv,3gp,mp4,mov,avi,flv等) 39 if (type.equals("avi")) { 40 return 0; 41 } else if (type.equals("mpg")) { 42 return 0; 43 } else if (type.equals("wmv")) { 44 return 0; 45 } else if (type.equals("3gp")) { 46 return 0; 47 } else if (type.equals("mov")) { 48 return 0; 49 } else if (type.equals("mp4")) { 50 return 0; 51 } else if (type.equals("asf")) { 52 return 0; 53 } else if (type.equals("asx")) { 54 return 0; 55 } else if (type.equals("flv")) { 56 return 0; 57 } 58 // 對ffmpeg沒法解析的文件格式(wmv9,rm,rmvb等), 59 // 能夠先用別的工具(mencoder)轉換爲avi(ffmpeg能解析的)格式. 60 else if (type.equals("wmv9")) { 61 return 1; 62 } else if (type.equals("rm")) { 63 return 1; 64 } else if (type.equals("rmvb")) { 65 return 1; 66 } 67 return 9; 68 } 69 70 private static boolean checkfile(String path) { 71 File file = new File(path); 72 if (!file.isFile()) { 73 return false; 74 } 75 return true; 76 } 77 78 // 對ffmpeg沒法解析的文件格式(wmv9,rm,rmvb等), 能夠先用別的工具(mencoder)轉換爲avi(ffmpeg能解析的)格式. 79 private static String processAVI(int type) { 80 List<String> commend = new ArrayList<String>(); 81 commend.add("c:\\ffmpeg\\mencoder"); 82 commend.add(PATH); 83 commend.add("-oac"); 84 commend.add("lavc"); 85 commend.add("-lavcopts"); 86 commend.add("acodec=mp3:abitrate=64"); 87 commend.add("-ovc"); 88 commend.add("xvid"); 89 commend.add("-xvidencopts"); 90 commend.add("bitrate=600"); 91 commend.add("-of"); 92 commend.add("avi"); 93 commend.add("-o"); 94 commend.add("c:\\ffmpeg\\output\\a.avi"); 95 try { 96 ProcessBuilder builder = new ProcessBuilder(); 97 builder.command(commend); 98 builder.start(); 99 return "c:\\ffmpeg\\output\\a.avi"; 100 } catch (Exception e) { 101 e.printStackTrace(); 102 return null; 103 } 104 } 105 106 // ffmpeg能解析的格式:(asx,asf,mpg,wmv,3gp,mp4,mov,avi,flv等) 107 private static boolean processFLV(String oldfilepath) { 108 109 if (!checkfile(PATH)) { 110 System.out.println(oldfilepath + " is not file"); 111 return false; 112 } 113 114 // 文件命名 115 Calendar c = Calendar.getInstance(); 116 String savename = String.valueOf(c.getTimeInMillis())+ Math.round(Math.random() * 100000); 117 List<String> commend = new ArrayList<String>(); 118 commend.add("c:\\ffmpeg\\ffmpeg"); 119 commend.add("-i"); 120 commend.add(oldfilepath); 121 commend.add("-ab"); 122 commend.add("56"); 123 commend.add("-ar"); 124 commend.add("22050"); 125 commend.add("-qscale"); 126 commend.add("8"); 127 commend.add("-r"); 128 commend.add("15"); 129 commend.add("-s"); 130 commend.add("600x500"); 131 commend.add("c:\\ffmpeg\\output\\a.flv"); 132 133 try { 134 Runtime runtime = Runtime.getRuntime(); 135 Process proce = null; 136 String cmd = ""; 137 String cut = " c:\\ffmpeg\\ffmpeg.exe -i " 138 + oldfilepath 139 + " -y -f image2 -ss 8 -t 0.001 -s 600x500 c:\\ffmpeg\\output\\" 140 + "a.jpg"; 141 String cutCmd = cmd + cut; 142 proce = runtime.exec(cutCmd); 143 ProcessBuilder builder = new ProcessBuilder(commend); 144 builder.command(commend); 145 builder.start(); 146 147 return true; 148 } catch (Exception e) { 149 e.printStackTrace(); 150 return false; 151 } 152 } 153 }
接下來是我本身通過修改後的代碼:ui
1 import java.io.File; 2 import java.io.IOException; 3 import java.util.ArrayList; 4 import java.util.Calendar; 5 import java.util.List; 6 public class ConvertVideo { 7 8 private static String inputPath = ""; 9 10 private static String outputPath = ""; 11 12 private static String ffmpegPath = ""; 13 14 public static void main(String args[]) throws IOException { 15 16 getPath(); 17 18 if (!checkfile(inputPath)) { 19 System.out.println(inputPath + " is not file"); 20 return; 21 } 22 if (process()) { 23 System.out.println("ok"); 24 } 25 } 26 27 private static void getPath() { // 先獲取當前項目路徑,在得到源文件、目標文件、轉換器的路徑 28 File diretory = new File(""); 29 try { 30 String currPath = diretory.getAbsolutePath(); 31 inputPath = currPath + "\\input\\test.wmv"; 32 outputPath = currPath + "\\output\\"; 33 ffmpegPath = currPath + "\\ffmpeg\\"; 34 System.out.println(currPath); 35 } 36 catch (Exception e) { 37 System.out.println("getPath出錯"); 38 } 39 } 40 41 private static boolean process() { 42 int type = checkContentType(); 43 boolean status = false; 44 if (type == 0) { 45 System.out.println("直接轉成flv格式"); 46 status = processFLV(inputPath);// 直接轉成flv格式 47 } else if (type == 1) { 48 String avifilepath = processAVI(type); 49 if (avifilepath == null) 50 return false;// 沒有獲得avi格式 51 status = processFLV(avifilepath);// 將avi轉成flv格式 52 } 53 return status; 54 } 55 56 private static int checkContentType() { 57 String type = inputPath.substring(inputPath.lastIndexOf(".") + 1, inputPath.length()) 58 .toLowerCase(); 59 // ffmpeg能解析的格式:(asx,asf,mpg,wmv,3gp,mp4,mov,avi,flv等) 60 if (type.equals("avi")) { 61 return 0; 62 } else if (type.equals("mpg")) { 63 return 0; 64 } else if (type.equals("wmv")) { 65 return 0; 66 } else if (type.equals("3gp")) { 67 return 0; 68 } else if (type.equals("mov")) { 69 return 0; 70 } else if (type.equals("mp4")) { 71 return 0; 72 } else if (type.equals("asf")) { 73 return 0; 74 } else if (type.equals("asx")) { 75 return 0; 76 } else if (type.equals("flv")) { 77 return 0; 78 } 79 // 對ffmpeg沒法解析的文件格式(wmv9,rm,rmvb等), 80 // 能夠先用別的工具(mencoder)轉換爲avi(ffmpeg能解析的)格式. 81 else if (type.equals("wmv9")) { 82 return 1; 83 } else if (type.equals("rm")) { 84 return 1; 85 } else if (type.equals("rmvb")) { 86 return 1; 87 } 88 return 9; 89 } 90 91 private static boolean checkfile(String path) { 92 File file = new File(path); 93 if (!file.isFile()) { 94 return false; 95 } 96 return true; 97 } 98 99 // 對ffmpeg沒法解析的文件格式(wmv9,rm,rmvb等), 能夠先用別的工具(mencoder)轉換爲avi(ffmpeg能解析的)格式. 100 private static String processAVI(int type) { 101 List<String> commend = new ArrayList<String>(); 102 commend.add(ffmpegPath + "mencoder"); 103 commend.add(inputPath); 104 commend.add("-oac"); 105 commend.add("lavc"); 106 commend.add("-lavcopts"); 107 commend.add("acodec=mp3:abitrate=64"); 108 commend.add("-ovc"); 109 commend.add("xvid"); 110 commend.add("-xvidencopts"); 111 commend.add("bitrate=600"); 112 commend.add("-of"); 113 commend.add("avi"); 114 commend.add("-o"); 115 commend.add(outputPath + "a.avi"); 116 try { 117 ProcessBuilder builder = new ProcessBuilder(); 118 Process process = builder.command(commend).redirectErrorStream(true).start(); 119 new PrintStream(process.getInputStream()); 120 new PrintStream(process.getErrorStream()); 121 process.waitFor(); 122 return outputPath + "a.avi"; 123 } catch (Exception e) { 124 e.printStackTrace(); 125 return null; 126 } 127 } 128 129 // ffmpeg能解析的格式:(asx,asf,mpg,wmv,3gp,mp4,mov,avi,flv等) 130 private static boolean processFLV(String oldfilepath) { 131 132 if (!checkfile(inputPath)) { 133 System.out.println(oldfilepath + " is not file"); 134 return false; 135 } 136 137 List<String> command = new ArrayList<String>(); 138 command.add(ffmpegPath + "ffmpeg"); 139 command.add("-i"); 140 command.add(oldfilepath); 141 command.add("-ab"); 142 command.add("56"); 143 command.add("-ar"); 144 command.add("22050"); 145 command.add("-qscale"); 146 command.add("8"); 147 command.add("-r"); 148 command.add("15"); 149 command.add("-s"); 150 command.add("600x500"); 151 command.add(outputPath + "a.flv"); 152 153 try { 154 155 // 方案1 156 // Process videoProcess = Runtime.getRuntime().exec(ffmpegPath + "ffmpeg -i " + oldfilepath 157 // + " -ab 56 -ar 22050 -qscale 8 -r 15 -s 600x500 " 158 // + outputPath + "a.flv"); 159 160 // 方案2 161 Process videoProcess = new ProcessBuilder(command).redirectErrorStream(true).start(); 162 163 new PrintStream(videoProcess.getErrorStream()).start(); 164 165 new PrintStream(videoProcess.getInputStream()).start(); 166 167 videoProcess.waitFor(); 168 169 return true; 170 } catch (Exception e) { 171 e.printStackTrace(); 172 return false; 173 } 174 } 175 } 176 177 class PrintStream extends Thread 178 { 179 java.io.InputStream __is = null; 180 public PrintStream(java.io.InputStream is) 181 { 182 __is = is; 183 } 184 185 public void run() 186 { 187 try 188 { 189 while(this != null) 190 { 191 int _ch = __is.read(); 192 if(_ch != -1) 193 System.out.print((char)_ch); 194 else break; 195 } 196 } 197 catch (Exception e) 198 { 199 e.printStackTrace(); 200 } 201 } 202 }
問題
原文的代碼中有一個很大的問題,即是不知道視頻轉換到底何時結束。看原文中的這兩處代碼:
98行處
1 builder.command(commend); 2 builder.start(); 3 return "c:\\ffmpeg\\output\\a.avi";
145行處
1 builder.start(); 2 3 return true;
在進程開始以後,直接就返回結果了。要知道,這樣的寫法,是不會阻塞當前進程的,也就是說,固然程序返回的時候,轉碼程序(ffmpeg和mencoder)還在執行。若是須要mencoder進行中間轉碼,那原文中的寫法會形成在avi文件還未轉換完成時,程序就調用了ffmpeg進行轉換。而對於最終的flv文件,咱們也沒法知道究竟是何時轉換好的,這顯然是沒法知足咱們的業務需求的 。
解決方案
最早想到的辦法天然就是阻塞當前進程(主進程),實例代碼:
1 Process process = new ProcessBuilder(command).start(); 2 process.waitFor(); 3 return true;
採用這種的方案運行程序,發現視頻轉到十幾秒的時候就不轉了,可是程序還沒返回,打開進程管理器一開,ffmpeg進程還在,內存還佔着,可是CPU爲0,如圖:
當時不知道什麼緣由,在網上查了半天,才明白這是死鎖了,可是不知道是什麼緣由形成的。當時就一直以爲死鎖是waitFor()函數形成了,看來用它來判斷子進程是否結果是不行了,因此又在網上查了半天其餘判斷子進程結束的辦法(這裏其實就已經走彎路了)。有人說能夠用exitValue(),因而就有了下面的代碼:
1 Process process = new ProcessBuilder(command).start(); 2 while (true) { 3 try { 4 if (process.exitValue() == 0) 5 break; 6 } 7 catch (IllegalThreadStateException e) { 8 continue; 9 } 10 } 11 return true;
當子進程沒有結束的時候,若是執行exitValue()就會拋出異常,我採用的辦法是捕獲這個異常而後不去理他,直到程序結束exitValue()返回0爲止。可是,仍是失敗了,出現的狀況和用waitFor()方式時的如出一轍,我才以爲多是另外的緣由,在去google,發現多是是因爲JVM只提供有限緩存空間,當外部程序(子進程)的輸出流超出了這個有限空間而父進程又不讀出這些數據,子進程會被阻塞waitFor()永遠都不會返回,就會形成死鎖。
官方解釋:
Because some native platforms only provide limited buffer size for standard input and output streams, failure to promptly write the input stream or read the output stream of the subprocess may cause the subprocess to block, and even deadlock.
知道問題了就要對症下藥(其實當時我也不知道這是否是就是我遇到的問題,只能各類打散彈了,打中了算)。關於如何讀出子進程的輸出流,如何解決這個死鎖,網上的辦法都大同小異,寫的比較好的能夠看這個地址。
因而程序被改爲這樣:
1 Process process = new ProcessBuilder(command).start(); 2 3 new PrintStream(process.getInputStream()).start(); 4 5 process.waitFor();
PrintStream類以下:
1 class PrintStream extends Thread 2 { 3 java.io.InputStream __is = null; 4 public PrintStream(java.io.InputStream is) 5 { 6 __is = is; 7 } 8 9 public void run() 10 { 11 try 12 { 13 while(this != null) 14 { 15 int _ch = __is.read(); 16 if(_ch != -1) 17 System.out.print((char)_ch); 18 else break; 19 } 20 } 21 catch (Exception e) 22 { 23 e.printStackTrace(); 24 } 25 } 26 }
運行,發現仍是不對,症狀和以前的如出一轍,我還覺得是否是輸出流太多了,一個線程讀的不夠快(好吧,真的很傻很天真,人被逼急了真的什麼想法都有),因而我就再開了幾個如出一轍的線程,結果仍是同樣。
就在我快要放棄的時候,在百度知道上,看了個無關痛癢的例子,因而作了個小修改,在進程啓動以前,重定向了下錯誤輸出流,以下:
1 Process videoProcess = new ProcessBuilder(command).redirectErrorStream(true).start(); 2 3 new PrintStream(videoProcess.getInputStream()).start(); 4 5 videoProcess.waitFor(); 6 7 return true;
而後,而後,而後就能夠了,凌亂。。。
結論
其實有兩種寫法能夠解決這個問題,這種事像我上面那樣寫,還有一種以下:
1 Process videoProcess = new ProcessBuilder(command).start(); 2 3 new PrintStream(videoProcess.getErrorStream()).start(); 4 5 new PrintStream(videoProcess.getInputStream()).start(); 6 7 videoProcess.waitFor(); 8 9 return true;
其實道理仍是同樣的,就是讀出ffmpeg的輸出流,避免ffmpeg的輸出流塞滿緩存形成死鎖。可是不知道爲何,ffmpeg的輸出信息是在錯誤輸出流裏面的,我看了下控制檯打印結果,發現只是一些當前轉換狀態的信息,並無錯誤,使人費解。
在Process類中,getInputStream用來獲取進程的輸出流,getOutputStream用來獲取進程的輸入流,getErrorStream用來獲取進程的錯誤信息流。爲了保險起見,在讀出的時候,最好把子進程的輸出流和錯誤流都讀出來,這樣能夠保證清空緩存區。
其實,我深入地感受到,這些解決的問題的經歷是標準的散彈式編程,打到哪算哪,之後引覺得戒。