ミルク色の記録

やったこと、やってみたこと

WebSocketとcreateObjectURLとvideoで遊んでみた

(´・ω・`)。oO(WebSocketのバイナリデータ送信って、クライアントの方も対応したのかなぁ…)

と、ふと思ったので試してみた。

環境こんな感じ。

  1. OS : ubuntu 11.04
  2. ブラウザ : Google Chrome 17.0.963.56
  3. node.js v0.6.11, connect, websocket

nodeのwebsocketモジュールで

client.sendBytes(data);

みたいなメソッドがあったのを覚えていたので、画像ファイルを送ってブラウザ側で貰えているか、
messageイベントのデータを見てみると…届いてるっぽい。

せっかくなので、前々からちょっと試してみたかったことを試してみた。

なんちゃってストリーミング配信的な

適当に用意した動画ファイルをOpenShot Video Editorを使って、
webmでエンコードして10秒毎に分割。連番のファイル名にしてひとつのディレクトリに設置。
それをクライアントに次々送りつけて、createObjectURLで変換してvideoタグのsrcにセットして再生していく。

最初videoタグ一つでやってたら、継ぎ目がプチプチしてたので、
videoタグを2つ用意してダブルバッファリング的にやってみたら、
割といい感じになったっぽい。

カメラからとかで動的にwebmファイル作れるともっと面白いんだけど。。

書いたソースは下のとおり。

サーバ側

var connect = require('connect');
var WebSocketServer = require('websocket').server;
var util = require('util');
var fs = require('fs');
var EventEmitter = require('events').EventEmitter;

/**
 * ファイルリストを順番に処理するための簡易キュー
 */
function Queue(queue) {
    EventEmitter.call(this);
    this.queue_ = queue;
    this.next();
}
util.inherits(Queue, EventEmitter);
Queue.prototype.next = function() {
    var that = this;
    process.nextTick(function() {
        var q = that.queue_.shift();
        if (q != null) {
            that.emit('data', q);
        } else {
            that.emit('end');
        }
    });
};

/**
 * 簡易HTTPサーバ
 */
var httpServer = connect.createServer(
    connect.static(__dirname + '/public_html', { maxAge : 86400000 })
    , connect.cookieParser()
    , connect.bodyParser()
    , connect.session({ secret : 'hogefugapiyo' })
    , connect.router(function(route) {

    })
).listen(3000);

/**
 * WebSocketサーバ
 */
var wsServer = new WebSocketServer({
    httpServer : httpServer,
    autoAcceptConnections : true
});
wsServer.on('connect', function(client) {
    util.log('client connect');
    client.on('message', function(msg) {
        var param = msg.utf8Data;
        switch(param) {
            case 'play':
                // クライアントからplayメッセージを受け取ったらデータ送信
                sendData(client);
                break;
        }
    });
    client.on('close', function() {
        util.log('client disconnect');
    });
});
console.log('http://localhost:3000/');

/**
 * データをクライアントへ送信
 */
function sendData(client) {
    var path = __dirname + '/public_html/video';
    // ディレクトリ一覧読み込み
    fs.readdir(path, function(err, files) {
        if (err) {
            util.log(err);
            return;
        }
        // ファイル一覧を名前順にソート
        files.sort();
        var queue = new Queue(files);
        queue.on('data', function(file) {
            // ファイル読み込み
            fs.readFile(path + '/' + file, function(err, data) {
                if (!err) {
                    client.sendBytes(data);
                }
                // データを送信完了したら次のキューへ
                queue.next();
            });
        });
        queue.on('end', function() {
            // 全部送ったら終わり
            console.log('end');
        });
    });
}

クライアント側

HTMLこんな感じ。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <link rel="stylesheet" type="text/css" href="css/index.css" />   
        <title>Video Viewer</title>
    </head>
    <body>
        <div id="viewer">
            <div id="screen_cont">
                <video id="screen1" style="display:none;"></video>
                <video id="screen2" style="display:none;"></video>
            </div>
            <div id="controller">
                <input type="button" id="play" value="play" />
            </div>
        </div>
        <script type="text/javascript" src="js/jquery-1.6.4.min.js"></script>
        <script type="text/javascript" src="js/index.js"></script>
    </body>
</html>


クライアント側のソースはこんな感じ。

!function(global) {
    
    /**
     * サーバから受信したデータキュー
     */
    var datas_ = [];
    /**
     * 再生待ちのvideoタグキュー
     */
    var readyScreens_ = [];
    /**
     * データ設定待ちのvideoタグキュー
     */
    var waitScreens_ = [];
    /**
     * 最初のデータ受信判定フラグ
     */
    var firstData_ = true;
    
    // スクリーン1
    waitScreens_.push(document.getElementById('screen1'));
    waitScreens_[0].addEventListener('ended', function() {
        // 再生終了イベント
        // スクリーンを切り替え
        changeScreen(this);
        updateQueue();
    }, false);
    // スクリーン2
    waitScreens_.push(document.getElementById('screen2'));
    waitScreens_[1].addEventListener('ended', function() {
        changeScreen(this);
        updateQueue();
    }, false);
    
    
    /**
     * スクリーンを切り替える
     * @param {Element} oldScreen videoエレメント
     */
    function changeScreen(oldScreen) {
        // 再生済みのスクリーンを非表示にして設定待ちスクリーンキューに追加
        oldScreen.style.display = 'none';
        waitScreens_.push(oldScreen);
        // 再生待ちスクリーンを取得
        var screen = readyScreens_.shift();
        if (screen) {
            // 存在したら表示して再生
            screen.style.display = '';
            screen.play();
        }
    }
    
    /**
     * キューの更新
     */
    function updateQueue() {
        if (waitScreens_.length > 0 && datas_.length > 0) {
            // 受信データとデータ設定待ちのスクリーンが存在する場合それぞれ取り出し
            var screen = waitScreens_.shift();
            var blob = datas_.shift();
            // src属性に設定
            screen.src = blob;
            if (firstData_) {
                // 最初の受信データだった場合
                firstData_ = false;
                // データを再生
                screen.style.display = '';
                screen.play();
            } else {
                // 再生待ちのスクリーンキューに追加
                readyScreens_.push(screen); 
            }
        }
    }

    // WebSocketの接続を開く  
    var socket = new WebSocket('ws://' + location.host + '/');
    // messageイベント
    socket.addEventListener('message', function(evt) {
        // メッセージを受け取ったらcreateObjectURLで変換してデータキューに入れる
        datas_.push(window.webkitURL.createObjectURL(evt.data));
        // キュー更新
        updateQueue();
    }, false); 

    $('#play').click(function() {
        // 再生ボタンクリックしたら再生メッセージをサーバへ通知
        socket.send('play');
    });

}(this);