最近更新: 2013-07-11

ffmpeg 用於網路線上轉檔

假設我要做一個視訊網站服務,讓使用者上傳視訊檔供其他人觀賞。 但是顧慮到視訊有著像 rmvb, avi, 3gp, mod, mkv, mp4 等等格式的多樣性,為了讓其他人可以順利地觀賞視訊,故打算將上傳的視訊統一轉成目前使用率最普遍的 mp4 格式。

欲達此目的,最簡單的作法就是上傳完成後,再執行轉檔工具。缺點是轉檔要花點時間,故使用者上傳後不能馬上觀賞轉檔後的 mp4 。不過只要對 CGI 規格有所了解的人,就會知道要實現一邊上傳一邊轉檔的功能,其實是一件簡單的事。

基本概念

基於普遍性,選擇 ffmpeg 為轉檔工具。ffmpeg 的輸入檔名若為 - 號,就會從 stdin 讀取資料。 CGI 這一端只要一邊接收上傳資料,一邊將接收到的資料透過 pipe 寫到 ffmpeg 的 stdin 端即可。概念類似下列的命令列操作:

$ cat uploaded_file | ffmpeg -i  -  -codec copy output.mp4

只是目前多數的 CGI 實作品(所有的 Web 開發框架或工具,都屬於 CGI 實作品)不會讓使用者接觸到如此「低階」、「原始」的處理步驟。以 PHP 為例, Apache + mod-php 內定的檔案上傳策略就是先把上傳資料儲存為檔案後,才開始執行 PHP script 。在這個過程中,使用者不會接觸到上傳中的資料串流,只會摸到上傳完成後的成品。 所以對透過 mod-php 執行的 PHP script 來說,它只適合這樣做:

$uploadfile = '/uploaded/files_' . $rnd;
move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadfile)
system("ffmpeg -i $uploadfile -codec copy output.mp4");

若是用 CGI 模式執行 PHP script ,要實現一邊上傳一邊轉檔的線上轉檔功能就簡單了。你只需要拋開 Web 開發框架帶給你的思想制約,就會發現這件事。

ffmpeg

安裝 ffmpeg 套件。在 Debian 或 Ubuntu 下,需下列套件:

  • ffmpeg
  • libavcodec-extra-52 或 libavcodec52 或 libavcodec 或 libavcodec-extra-53 。 不同的套件名稱提供不同的 codec 。

ffmpeg 有新舊版本不同的參數語法,請執行 ffmpeg -h 自行查看。

此外,ffmpeg 在編譯時就會決定它可用的 codec 內容。 不同的 Linux 散佈套件所啟用的 codec 也會不同,也要自己查看。 有兩種參數語法可以查看你安裝的 ffmpeg 支援的 codec 。 第一種是: ffmpeg -formats ;有些版本則是用第二種: ffmpeg -codecs

查詢結果第一欄的 D 表示此 codec 可用於解碼(Decode)。 第二欄的 E 表示此 codec 可用於編碼(Encode)。 第三欄的 V 或 A 表示此 codec 用於處理視訊(video)或音訊(audio)。 以 MP4 為例,建議的 codec 組合是: -vcodec libx264 -acodec libvo_aacenc 。 不過並非每一種 ffmpeg 編成品都提供 libx264 和 libvo_aacenc 這兩種編碼器。

可參考「ffmpeg 轉檔與影片連接範例」了解其他 ffmpeg 的使用範例。

PHP 實作

ffmpeg.cgi 是用於線上轉檔的 PHP script 。這不一定要用 PHP 實作,你有興趣的話,會發現用 C 語言來寫也不難。

ffmpeg.cgi 預設的輸出目錄是 /tmp ,轉檔後的視訊檔名的主檔名同上傳檔的主檔名。 例如上傳 123.avi ,那麼轉檔後的視訊檔會是 /tmp/123.mp4 。 當然實務上不會這麼直接地儲存,這就看實作者的設計了。

#!/usr/bin/php
Content-Type: text/plain

<?php

$boundary = fgets(STDIN);
$end_boundary = rtrim($boundary) . "--\r\n";

$headers = array();
while ($s = fgets(STDIN)) {
    if ($s == "\r\n") // head end line.
        break;
    if (preg_match('/(^[\w\-]+): (.*)/', $s, $m))
        $headers[$m[1]] = trim($m[2]);
}

$filename = 'test.mp4';

// Get filename of upload file.
if (isset($headers['Content-Disposition'])) {
    $disposition = $headers['Content-Disposition'];
    echo 'Content-Disposition: ', $disposition, "\n";

    if (preg_match('/filename="(.*)\.[\w]+"$/', $disposition, $m))
        $filename = $m[1] . '.mp4';
}

// You may save this filename.
echo $filename, "\n";

$out_stream = popen('ffmpeg -i - -vcodec libx264 -b:v 500000 -s 640x360
    -acodec libvo_aacenc -b:a 128000 -ar 48000 -y /tmp/' . $filename, 'w');
#$out_stream = popen('ffmpeg -i - -vcodec mpeg4 -vb 1000k -r 25 -s 640x360 \
#    -acodec mp2 -ab 96k -ar 22050 -y /tmp/' . $filename, 'w');

if ($out_stream === false) {
    echo "Could not invoke ffmpeg.\n";
    exit(1);
}

$data_len = 0;
$out_buffer = false;
while ($s = fgets(STDIN)) {
    #if (strpos($s, $boundary) === 0)
    if ($s == $end_boundary) {
        #echo "BREAK\n";
        break;
    }
    fwrite($out_stream, $out_buffer);
    $data_len += strlen($out_buffer);
    $out_buffer = $s;
}

fwrite($out_stream, substr($out_buffer, 0, -2)); // strip end mark (\r\n).
pclose($out_stream);

$data_len += strlen($out_buffer) - 2;

// Output read bytes to browser.
echo $data_len;

?>

查看 Apache 的組態文件,啟用 CGI 設定。 將 ffmpeg.cgi 放在 CGI 目錄下。 ffmpeg.cgi 要設置可執行屬性,以便執行。(因為要用 CGI 模式執行,所以 Debian/Ubuntu 必須安裝 php5-cli 套件) 根據你安裝的 ffmpeg 的參數語法與可用的 codec 內容,修改 ffmpeg.cgi 中呼叫 ffmpeg 的參數內容。

ffmpeg.html 則是示範用的檔案上傳頁面。

<html>
<form action='/cgi-bin/ffmpeg.cgi' method='POST'
	enctype='multipart/form-data'>
<input type="file" name="data" />
<br/>
<button type="submit">Submit</button>
</form>
</html>

選擇一個檔案上傳, ffmpeg.cgi 就會一邊讀取上傳的內容,一邊將已讀取的資料內容交給 ffmpeg 轉檔。 轉換後的新檔會放在 /tmp 目錄下 (可以修改參數的儲存路徑)。

基本上,除非你是區域網路上傳,否則在一般 Internet 環境下,轉檔速度通常快過上傳速度。所以上傳完成時,同時就完成傳換格式的工作了。

樂多舊網址: http://blog.roodo.com/rocksaying/archives/25362202.html