Streaming

 perlRTMPでRTMPサーバを立ち上げ、Webサーバー(Apache2:win32)から録画&エンコ済みのmp4を、JW FLV Media Playerを使ってストリーミング。RTMPなので、動画開始も早いし、非常に快適なシークができる。
 流れとしてはこんな感じ

  • Apacheを動かす
  • Perl(Cygwin)インストール(単接続ならActivePerlも可)
  • perlRTMPをダウンロードして適当なフォルダに解凍
  • Webサーバがアクセスできる「適当なひとつのフォルダ」に対して perlRTMP を立ち上げる
  • mp4ファイルのファイル名は大概日本語(SJIS)なので、ハードリンクによって、適当なフォルダ下に半角英数ファイル名のエイリアスを作る。
  • ハードリンクは各ドライブごとに必要だが、perlRTMPが扱えるdocument_rootはひとつしかない。よって、「適当なひとつのフォルダ」下に、ジャンクションまたはジャンクション機能を用いて上記ハードリンクを作ったフォルダをマウントする。
  • JW FLV Media Playerのプレイリストを作る。
  • Webサーバで表示。


実際の作業

構成例

Webサーバドキュメントルート
d:\webroot
Webサーバがアクセスできる適当なフォルダ
d:\webroot\videosource
mp4実体ファイルのあるドライブとフォルダ
e:\video1 f:\video2 g:\video3
ドライブごとにhardlinkを作成するフォルダ
e:\hardlink f:\hardlink g:\hardlink

junctionを張る。(てきとーなバッチファイル)

junction d:\webroot\videosource\vs1 e:\hardlink
junction d:\webroot\videosource\vs2 f:\hardlink
junction d:\webroot\videosource\vs3 g:\hardlink

perlRTMPを立ち上げる。(てきとーなvbsファイル)

コマンドプロンプトが開かないように裏で実行する。ただしデバッグ中は素直にコマンドプロンプトから実行して、標準出力されるログを見ておいたほうがいい。

CreateObject( "WScript.Shell").Run "C:\Cygwin\bin\perl.exe server.pl d:\webroot\videosource",0,False

phpでmp4ファイルのリストを取り、プレイリスト作成とハードリンク作成を行う

以下のコードはすべてutf-8を想定(画面横幅への収まりが悪いので、コメントが読みづらい位置にあるのは勘弁)

// フォルダごとの設定部分
// mp4実体ファイルのあるフォルダ
$DirectoryToScan = mb_convert_encoding("g:/video3/所さん~1","SJIS","UTF-8"); 
$ScanCommand1 = mb_convert_encoding("g:\\video3\\所さん~1\\*.mp4","SJIS","UTF-8");
// ハードリンクを作成するフォルダ
$HardlinkFolder = mb_convert_encoding("g:/hardlink/tokoro","SJIS","UTF-8");
$ScanCommand2 = mb_convert_encoding("g:\\hardlink\\tokoro\\*.mp4","SJIS","UTF-8");
// perlRTMPで指定したドキュメントルートからの相対アドレス
$dirHeader = "vs3/tokoro/";
// プレイリストの出力先
$listpath = "d:/webroot/stream/tokoro/";
$title = "所さんの世田谷ベース";
// 以下共通処理
exec("dir " . $ScanCommand1 ,$count1);
exec("dir " . $ScanCommand2 ,$count2);
if(count($count1) > count($count2)){    // エイリアスより実体ファイルの数が多いとき
    exec("del " . $ScanCommand2);
    $fp = fopen($listpath . "list.xml","w");
    // RSSプレイリスト
    fputs($fp, "<rss version=\"2.0\" xmlns:jwplayer=\"http://developer.longtailvideo.com/trac/wiki/FlashFormats\">\n");
    fputs($fp, "<channel>\n<title>" . $title . "</title>\n");
    $dir = opendir($DirectoryToScan);
    while (($file = readdir($dir)) !== false) {
        $FullFileName = realpath($DirectoryToScan.'/'.$file);
        if (is_file($FullFileName) && mb_eregi("\.mp4",$FullFileName)) {
            set_time_limit(10);
            $file8 = mb_convert_encoding($file,"UTF-8","SJIS");
            fputs($fp, " <item>\n");
            // ダミーのファイル名を与えておく
            fputs($fp, "  <enclosure url=\"dummy.flv\" />\n");
           // ファイル名をタイトルに
            fputs($fp, "  <jwplayer:title>" . str_replace(".mp4", "", $file8) . "</jwplayer:title>\n");
           // RTMP指定
            fputs($fp, "  <jwplayer:type>rtmp</jwplayer:type>");
           // 適当に半角英数ファイル名を作成する
            $file8 = urlencode($file8);
            $file8 = str_replace("+","%20",$file8);
           // あまり長いファイル名はperlRTMPに嫌われる
            if(strlen($file8) > 120){
                $pos = strpos($file8,"%",100);
                // 同名(になってしまった)ファイルの連番処理
                if(file_exists($hlFolder . "/" . substr($file8,0,$pos) . ".mp4")){
                    for($i=0;;$i++){
                        if(!file_exists($hlFolder . "/" . substr($file8,0,$pos) . "_" . $i . ".mp4")){
                            $file8 = substr($file8,0,$pos) . "_" . $i . ".mp4";
                            break;
                        }
                    }
                } else {
                    $file8 = substr($file8,0,$pos) . ".mp4";
                }
            }
            // URL Encodeしたままだと、処理系に依っては日本語に戻される可能性があるので%を削除
            $file8 = str_replace("%","",$file8);
            // ここで実際のファイル名を指定
            fputs($fp, "  <jwplayer:file>" . $dirHeader . $file8 . "</jwplayer:file>\n");
            // 自鯖のアドレス(プロトコルはrtmp)
            fputs($fp, "  <jwplayer:streamer>rtmp://www.your-server.jp/</jwplayer:streamer>\n");
            fputs($fp, "</item>\n");
            // ハードリンク作成
            $cmd_exec = "fsutil.exe hardlink create " . $hlFolder . "/" . $file8 . " \"" . $DirectoryToScan . "/" . $file . "\"";
        }
    }
    fputs($fp, "</channel>\n</rss>\n");
    fclose($fp);
}
ってなことをすると、
<rss version="2.0" xmlns:jwplayer="http://developer.longtailvideo.com/trac/wiki/FlashFormats">
<channel>
<title>所さんの世田谷ベース</title>
 <item>
  <enclosure url="dummy.flv" />
  <jwplayer:title>001 世田谷ベースの秘密</jwplayer:title>
  <jwplayer:type>rtmp</jwplayer:type>
  <jwplayer:file>vs3/tokoro/00120E4B896E794B0E8B0B7E38399E383BCE382B9E381AEE7A798E5AF86.mp4</jwplayer:file>
  <jwplayer:streamer>rtmp://www.your-server.jp/</jwplayer:streamer>
 </item>
 <item>
  <enclosure url="dummy.flv" />
  <jwplayer:title>007 レースカー&バイク かなり見せます語ります</jwplayer:title>
  <jwplayer:type>rtmp</jwplayer:type>
  <jwplayer:file>vs3/tokoro/00720E383ACE383BCE382B9E382ABE383BCEFBC86E38390E382A4E382AF20E3818BE3.mp4</jwplayer:file>
  <jwplayer:streamer>rtmp://www.your-server.jp/</jwplayer:streamer>
 </item>
</channel>
</rss>
って感じで d:/webroot/stream/tokoro/list.xml が出力されるので、d:/webroot/stream/tokoro/index.php などで、
<p id='preview'>Flash Player が必要です</p>
<script type='text/javascript' src='/script/swfobject.js'></script>
<script type='text/javascript'>
var s1 = new SWFObject('/swf/player.swf','player','1240','558','9');
// 960:540 = 16:9  width = 960 + 280, height = 540 + 18
s1.addParam('allowfullscreen',   'true');
s1.addParam('allowscriptaccess', 'always');
s1.addParam('wmode',             'opaque');
s1.addVariable('file',           'list.xml');
s1.addVariable('playlist',       'right');
s1.addVariable('playlistsize',   '280');
s1.addVariable('displaytitle',   'true');
s1.write('preview');
</script>
<h2 id='title' style='display:none'></h2>
ってな記述をして、ブラウザで http://www.your-server.jp/stream/tokoro/ にアクセス。 ひとつ上の http://www.your-server.jp/stream/ に番組リストとかを作るのと、.htaccessで適宜直リンなどを防ぐ。

おまけ

JW FLV Media PlayerにListenerを追加して、ブラウザ上に title と description を表示する

var player;
function ChangeItem(obj){
    if(document.getElementById("title")){
        if(player.getPlaylist()[obj.index].description){
            document.getElementById("title").innerHTML = player.getPlaylist()[obj.index].title + 
            "<br /><span style='font-weight:normal;font-size:60%'>" +  
            player.getPlaylist()[obj.index].description + "</desc>";
        } else {
            document.getElementById("title").innerHTML = player.getPlaylist()[obj.index].title;
        }
        document.getElementById("title").style.display = "block";
    }
}
function playerReady(obj){     // playerが準備完了になったら呼ばれる関数
    player = document.getElementById('player');
    player.addControllerListener("ITEM", "ChangeItem");
}