1. 程式人生 > >Java 呼叫 FFMPEG 命令時用 url 作為輸入源,Linux 下出現 “no such file or directory” 問題的解決

Java 呼叫 FFMPEG 命令時用 url 作為輸入源,Linux 下出現 “no such file or directory” 問題的解決

        Windows 下執行 ffmpeg 命令,
        D:/tools/ffmpeg/bin>ffmpeg.exe -i "某視訊檔案下載URL" -f flv D:/1.flv

        可以成功直接將下載連結輸入源轉為 1.flv。

String raw2flvCmd = "D:/tools/ffmpeg/bin/ffmpeg.exe -i \"某視訊檔案下載URL\" -f flv 1.flv";
Runtime.getRuntime().exec(raw2flvCmd);

        可以進行成功呼叫。
        Linux 下執行 ffmpeg 命令,
        /usr/local/ffmpeg/bin/ffmpeg -i "某視訊檔案下載URL" -f flv /usr/userfile/ffmpeg/tempfile/1.flv

        也可以成功直接將下載連結輸入源轉為 1.flv。

String raw2flvCmd = "/usr/local/ffmpeg/bin/ffmpeg -i \"某視訊檔案下載URL\" -f flv /usr/userfile/ffmpeg/tempfile/1.flv";
Runtime.getRuntime().exec(raw2flvCmd);

        FFmpeg 會報錯:
        No such file or directory:"某視訊檔案下載URL"。
        stackoverflow 上有人遇到了類似的問題:
        FFMPEG “no such file or directory” on Android
        I am trying to use the ffmpeg binary and call it via a native linux command in android. Most of the commands work fine but the problem is that when i need to pass an http url as an input to the -i option i get "No such file or directory" for the url. The url however is existing and running the SAME command on a mac does the job as expected.

        但最終沒人給出正確的解決方案。

        為什麼 terminal 執行正常的同一條命令列語句,Java 呼叫就掛了呢?看來 Java 並沒有將程式設計師的意圖良好地轉達給底層。

        筆者經過多次測試,終於找到解決辦法。既然 terminal 可以成功執行,啟動 shell,然後自定義命令列作為引數傳遞給 shell 直譯器。shell 知道如何將程式設計師的意圖轉達給底層。使用 sh -c,將自定義 CMD 行作為其引數,最後使用 java.lang.Runtimeexec(String[] cmdarray):

String raw2flvCmd = "/usr/local/ffmpeg/bin/ffmpeg -i \"某視訊檔案下載URL\" -f flv /usr/userfile/ffmpeg/tempfile/1.flv";
Runtime.getRuntime().exec(new String[]{"sh","-c",raw2flvCmd});

        問題迎刃而解。

        Runtime.getRuntime().exec(raw2flvCmd);會開啟一個子程序,如果當前執行緒想等待該子程序執行完畢之後再繼續往下執行,可以呼叫 java.lang.Process 的 waitFor() 方法:

Process process = null;
try {
			String raw2flvCmd = "/usr/local/ffmpeg/bin/ffmpeg -i \"某視訊檔案下載URL\" -f flv /usr/userfile/ffmpeg/tempfile/1.flv";
			process = Runtime.getRuntime().exec(new String[]{"sh","-c",raw2flvCmd});
            process.waitFor();  
		} catch (Exception e) {
			//do some thing
		}

        當前執行緒會等待子程序 process 執行結束,然後繼續往下執行。
        值得注意的一點是,ffmpeg 程序在執行時,會產生大量輸出資訊,如果我們沒有及時將流輸出的話,存放這些資訊的快取會很快填滿,之後該程序等待我們將這些資訊輸出,然而我們也在等待該程序執行結束(process.waitFor();很明顯 process 不會結束因為它也在等待我們),於是一個很經典的死鎖案例就此產生。
        這種情況表現為我們的子程序阻塞住了,而我們啟動該子程序的執行緒由於一直沒有拿到 waitFor() 的返回也就此止步於那條語句。
        所以我們需要不斷地從該子程序中的 input stream 中讀出資料以確保它不會阻塞。
        When Runtime.exec() won't
        Navigate yourself around pitfalls related to the Runtime.exec() method

        這篇文章對此進行了深入分析,並給出了推薦解決方案。我們依據該文將我們 Linux 下的 Java 呼叫 FFmpeg 最終完善為:

Process process = null;
try {
			String raw2flvCmd = "/usr/local/ffmpeg/bin/ffmpeg -i \"某視訊檔案下載URL\" -f flv /usr/userfile/ffmpeg/tempfile/1.flv";
			process = Runtime.getRuntime().exec(new String[]{"sh","-c",raw2flvCmd});
			StreamGobbler  errorGobbler  =  new  StreamGobbler(process.getErrorStream(),  "ERROR");
            errorGobbler.start();//  kick  off  stderr 
            StreamGobbler  outGobbler  =  new  StreamGobbler(process.getInputStream(),  "STDOUT");  
            outGobbler.start();//  kick  off  stdout 
            process.waitFor();  
		} catch (Exception e) {
			//do some thing
		}

        其中 StreamGobbler 原始碼為:

import  java.io.BufferedReader;  
import  java.io.IOException;  
import  java.io.InputStream;  
import  java.io.InputStreamReader;  
import  java.io.OutputStream;  
import  java.io.PrintWriter;  

public class StreamGobbler extends  Thread {
	InputStream is;
	String type;
	OutputStream os;

	public StreamGobbler(InputStream is, String type) {
		this(is, type, null);
	}

	public StreamGobbler(InputStream is, String type, OutputStream redirect) {
		this.is = is;
		this.type = type;
		this.os = redirect;
	}

	@Override
	public void run() {
		try {
			PrintWriter pw = null;
			if (os != null)
				pw = new PrintWriter(os);

			InputStreamReader isr = new InputStreamReader(is);
			BufferedReader br = new BufferedReader(isr);
			String line = null;
			while ((line = br.readLine()) != null) {
				if (pw != null)
					pw.println(line);
				System.out.println(type + ">" + line);
			}
			if (pw != null)
				pw.flush();
		} catch (IOException ioe) {
			ioe.printStackTrace();
		}
	}
}
        最後補充一點,關閉所有的 io 輸入/輸出以及錯誤流、呼叫 Process 的 destroy() 方法關閉子程序。不然程式可能會出現"java.io.IOException: error=24, Too many open files"。