どこで重たい動画を変換するか? 裏側でしょ!

この記事はだいぶ前に書かれたものなので情報が古いかもしれません
先日、ffmpegを使って動画の変換をやってみたんですけど……。

重たい動画を変換しようと思ったら、当然それだけ時間がかかるじゃないっすか。その間、ユーザーにずっと「現在変換しています」っていう、ローディング画面みたいなものを延々と見せ続けるわけにもいかないじゃないっすか。せめて「こちらの美女の服が段々と透けて行く様子をご覧下さい」っていう画像を見せるくらいじゃないと、クレーム来るじゃないっすか。でもみんながみんな美女に興味があるわけでもないから、結局根本的な解決にはならないじゃないっすか。

だから、アップロードが完了した時点で完了画面に飛ばしつつも、裏側では変換処理が走るっていうやり方ができれば良いですよね。シャワーを浴びて出て来たら美女が全裸でベッドに待機、戦闘準備は完了ですってなっていた方が良いですよね。それと同じこと。

いや、しかしそれだと脱がす楽しみが……?



コマンドをバックグラウンドで動かすことができる

PHPからシェルを実行する場合、exec()とかshell_exec()っていう関数があります。普段、クーロンを設定して動かしているようなバッチを、「いつ実行するか? 今でしょ!」って言われたときなんかに使うことができます。

その他にも、unixのコマンドなんかをPHPから操作する場合にも使える。

例えば、先日やった動画の変換。あれはターミナルなんかを起動してffmpegコマンドを実行していますけど、それをPHPから行なうことができる。

//ffmpegコマンドをPHPで実行
shell_exec("ffmpeg -i sample.flv -vcodec libx264 -vpre default sample.mp4");

特別難しいことはないです。普段実行しているコマンドを引数に渡してあげるだけだから。

これを適当なphpファイル(convert.phpみたいな)で実行して、終わったら完了画面(complete.phpみたいな)にリダイレクトするようなプログラムを組んだ場合。

//convert.php
shell_exec("ffmpeg -i sample.flv -vcodec libx264 -vpre default sample.mp4");
header('Location: complete.php');

//complete.php
echo 'アップロード完了! ポウ!';

これでconvert.phpにアクセスしたとき、動画の変換が完了するまでcomplete.phpにはリダイレクトされません。仮にsample.flvのファイルサイズが数百MBとかあったら、数十分か、あるいは一時間とか待たなきゃいけないかもしれない。「いつ終わるか? 今じゃないでしょ!」ってつっこまれちゃう。

さあ、どうしましょー。



しかし焦る必要はない。これを何とかするだけで良いなら、方法は至って簡単。

そんなときは、実行コマンドの最後に「&」をつける。ただそれだけで良い。

shell_exec("ffmpeg -i sample.flv -vcodec libx264 -vpre default sample.mp4 &");
header('Location: complete.php');

これで、サーバー側では一時間くらいかけて動画の変換を行っていたとしても、complete.phpへのリダイレクトは一瞬です。convert.phpにアクセスした一秒後には、あなたはcomplete.phpにリダイレクトしていることでしょう。

催眠術だとか超スピードだとか、そんなチャチなもんじゃあ断じてねえ。極めて普通のプログラム実行結果だぜ。

このコマンドの最後に&をつけるっていうのは、何もPHPで実行するときだけ有効ってわけでもないはずなので、普通にターミナルだのTeraTermだのを起動してコマンドラインから実行するときも、&をつければ処理はバックグラウンドになる……はず。



もうちょっと冒険しようず

これはこれで文句はないんですが、バックグラウンドで動画を変換した場合、いつ変換が終わったのかっていう情報が欲しいと思うんですよ。終了したときには何かしらの情報なりフラグを返してほしい、みたいな。その情報をデータベースに登録したい、的な。あるいはログファイルに書き出したい、とか。

でも上のやり方だとそれができない。たぶん。

となると、convert.phpはどこか別の場所から呼び出すようにして、しかもffmepgコマンドには&をつけずにバックグラウンドでは処理をせず、ユーザーがアクセスするPHPファイルと、終了フラグを受け取るPHPファイルを新たに二つ用意するってのはどうでやしょう?

//access.php
convert.phpを呼び出す
header('Location: complete.php');

//convert.php
shell_exec("ffmpeg -i sample.flv -vcodec libx264 -vpre default sample.mp4");
header('Location: notice.php?convert=success');

//notice.php
$flg = $_GET['convert'];
終了フラグを受け取る。
DBに書き込むなり何なりと。

//complete.php
echo 'アップロード完了! ポウ!';

この場合、ブラウザ上に表示されるページはaccess.phpとcomplete.phpの二つです。convert.phpとnotice.phpはバックグラウンドで処理することになります。



ここで問題になってくるのは、access.phpでconvert.phpを呼び出すところですよね。

通常、別のPHPファイルを読み込むにはrequire_once()やinclude()を使ったり、まあ読み込むのとは違うけどリダイレクトしたりってやり方がありますが、convert.phpをバックグラウンドで動かさなきゃいけないわけだから、単純にrequire_once()とかを使って読み込むってわけにはいかない。それだと結局変換が終わるまでcomlete.phpには行けないからね。header()でリダイレクトしても同じこと。

つまり、上の二つはどちらもダメってことですね。さっきの、コマンドに&をつけるようなやり方が必要になる。

と言っても、難しいことは考えなくても大丈夫。結局のところ、ここでもshell_exec()を使っちゃえば良いって話よ。

//access.php
shell_exec('/usr/bin/php /var/www/html/convert.php &');
header('Location: complete.php');

こうすると、シェルでconvert.phpが呼び出されて、そのファイルの中にあるプログラムが走ります。

「/usr/bin/php」っていうのは、サーバーにあるPHPの実行ファイルのあるパス……っていう言い方で良いのかしら? コマンドラインからPHPを実行する場合には、こういう書き方をします。後半の「/var/www/html/convert.php」はconvert.phpのパスね。ここにconvert.phpのファイルが置いてありますよってことで。

「/usr/bin/php」は、人によっては「/usr/local/bin/php」かもしれないね。もしかしたら全然違うかもしれない。

どこにあるか分からないよーってなっても大丈夫。それを調べるコマンドがある。

# which php

//出力結果
/usr/bin/php

このコマンドを実行すればパスが出力されるので、万事解決です。僕の場合は上記の通り「/usr/bin/php」って出力された。



というわけで。

これでaccess.phpにアクセスしたときに、バックグラウンドでconvert.phpが呼び出され、動画の変換をして終了フラグをnotice.phpに返すことができるようになりました。

バッチオーケーさぁ!!

……と、言いたいところだが。



外部サーバーには通用しないっつーお話

これは自身のサーバーの中でのみ処理が完結する場合の話であって、例えばaccess.phpやcomplete.phpが置いてあるプログラム実行側のサーバーと動画ファイルが置いてあるファイルサーバーが別々だった場合。

動画変換用のconvert.phpはファイルサーバーに置かなければならないので、上の方法で処理を行なうことができない。

うむむむ……さてどうしよう。



調べてみたらいくつかやり方があるみたいなんですが、今回はwgetというコマンドを使ってみることにしました。

wgetっていうのはファイルなんかをサーバーにダウンロードするときに使うコマンドで、サーバーをいじってるとちょいちょい使うことがあるような気がします。この前の、ffmpegでの動画変換を行ったときも使いましたね。ffmpegをサーバーにインストールするために、リポジトリの追加っていう作業が必要で、そのためのファイルをサーバーにダウンロードしました。

# wget http://pkgs.repoforge.org/rpmforge-release/rpmforge-release-0.5.3-1.el6.rf.x86_64.rpm

そのときのコマンドがこれですね。このコマンドで、上記のURLにあるファイルをダウンロードしたのです。

これと同じ要領で、外部のサーバーに置いてあるconvert.phpをダウンロードすることで、プログラムを実行することができます。

//access.php
shell_exec('wget http://hogehoge.com/convert.php &');
header('Location: complete.php');

もちろん、バックグラウンドで実行するために&はつけまっせ。

これで、動画の変換用プログラムが別のサーバーにあっても、バックグラウンドで動かし、終了フラグを受け取ることができます。

良かった良かった。



GETパラメータをつける場合

一点だけ注意があるとすれば、wgetのURLにGETでパラメータをつけた場合。

shell_exec('wget http://hogehoge.com/convert.php?id=1&name=sample &');

通常、GETパラメータが複数ある場合は「&」でつなぎますよね。でもこれをこのまま実行すると、GETパラメータで渡るのはidだけで、nameの方は上手く渡せないことがあります。

何となく察しがつくかと思うんですけど、コマンド実行時において&っていうのは処理をバックグラウンドで行なうための識別子なので、どうやらこんなコマンドと解釈されちゃうっぽいんですね。

shell_exec('wget http://hogehoge.com/convert.php?id=1 &');

最初の&以降は無視されちゃうみたいな感じ。

なのでGETパラメータをつける場合、パラメータが一つだけなら大丈夫ですけど、&でいくつかつなげる場合は、URLをクォーテーションで囲った方が良いみたいです。

shell_exec("wget 'http://hogehoge.com/convert.php?id=1&name=sample' &");

これで&がただの文字列と判断されて全部のパラメータがちゃんとconvert.phpに渡る。






ふう……これで何とか重い動画をアップロードする場合でも、ユーザーさんにストレスを与えなくて済みそうですね。

もっとも、変換自体に時間がかかるっていうところはどうしようもないので、アップロード完了画面に行った後も、しばらくは動画が閲覧できない状態にはなりますけどね。

それはもう、しようがないね。
 もしかしたら何か関連しているかも? 
 質問や感想などお気軽にコメントしてください