1. 程式人生 > >javaweb中ffmpeg視訊轉碼h264出現卡住不執行的解決辦法(看到最後面就是答案了)

javaweb中ffmpeg視訊轉碼h264出現卡住不執行的解決辦法(看到最後面就是答案了)

鄭文亮 專心地鑽研程式設計,每天進步一點點 隨筆-1610  文章-3  評論-570 

Java+Windows+ffmpeg實現視訊轉換

最近由於專案需要,研究了一下如何用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能解析的)格式。

2)瞭解Java如何呼叫外部程式,這會是最困難的,也會是坑最多的地方。

3)根據我們的需求設定ffmpeg的引數。(這類文章網上已經有很多了,我也不用複製黏貼了,見這裡

程式碼

上文中提到的那篇文章中的程式碼其實已經寫的很友好了,基本拿來就能用,不過仍然存在許多問題,接下來會講到,下面是文中的程式碼:

複製程式碼 複製程式碼
  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 }
複製程式碼 複製程式碼

接下來是我自己經過修改後的程式碼:

複製程式碼 複製程式碼
  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用來獲取程序的錯誤資訊流。為了保險起見,在讀出的時候,最好把子程序的輸出流和錯誤流都讀出來,這樣可以保證清空快取區。

其實,我深刻地感覺到,這些解決的問題的經歷是標準的散彈式程式設計,打到哪算哪,以後引以為戒。

分類:   JAVA 好文要頂   關注我   收藏該文     鄭文亮
關注 - 12
粉絲 - 917 +加關注 0 0 « 上一篇: 捕獲和冒泡
» 下一篇: sqlserver2014記憶體資料庫特性介紹
posted @   2015-07-23 14:49   鄭文亮  閱讀( 7149) 評論( 1)   編輯   收藏 評論列表    #1樓   2018-02-05 17:00   大漠飛魚     請問博主會實時視訊流視訊轉碼麼?將捕獲到的攝像頭視訊轉碼之後再到頁面播放。 支援(0) 反對(0) 重新整理評論 重新整理頁面 返回頂部 註冊使用者登入後才能發表評論,請   登入    註冊訪問網站首頁。 【推薦】超50萬VC++原始碼: 大型工控、組態\模擬、建模CAD原始碼2018!
【活動】杭州雲棲·2050大會-全世界年青人因科技而團聚-源點
【搶購】新註冊使用者域名搶購1元起
tencent0228 最新IT新聞:
·   《旅行青蛙》將推出官方中文版:或和國內巨頭合作
·   谷歌要在矽谷地區打造新園區,當地居民擔憂房價漲
·   支付寶300里程可兌換紅包:最高500元
·   微信上線新功能:發票查驗一鍵完成
·   中興AXON M圖賞:雙屏秒變平板
»   更多新聞... 阿里雲C2-1208 最新知識庫文章:
·   寫給自學者的入門指南
·   和程式設計師談戀愛
·   學會學習
·   優秀技術人的管理陷阱
·   作為一個程式設計師,數學對你到底有多重要
»   更多知識庫文章... 歷史上的今天:
2014-07-23   跟我一起玩轉Sencha Touch 移動 WebApp 開發(一)
2013-07-23   Hibernate的查詢 HQL查詢 查詢某幾列
2013-07-23   BoneCP學習筆記
2011-07-23   Android使用ksoap2呼叫C#webservice體會(轉)

公告

暱稱: 鄭文亮
園齡: 7年
粉絲: 917
關注: 12 +加關注
< 2018年3月 >
25 26 27 28 1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
1 2 3 4 5 6 7

搜尋

 

隨筆分類(1704)