ブラウザで端末のWebカメラを使ってみる : WebRTC のStreamAPIについて

f:id:slowquery:20130130001157p:plain

Web RTCとは

公式サイトでは、「シンプルなJavascript APIHTML5を用いて、ブラウザ内でリッチで高品質なリアルタイムコミュニケーション(Real Time Communication)アプリを開発できるようにする」ことを目標としたプロジェクトと書かれています。

Our mission: To enable rich, high quality, RTC applications to be developed in the browser via simple Javascript APIs and HTML5.

対応ブラウザ

公式サイトでは以下のブラウザで利用可能と記載されています。(2013年1月29日現在)

非公式ではありますが、Can I use getUserMedia/Stream API?では、OperaOpera Mobileでも利用可能と書かれています。

WebRTCのJava Script API

APIJavascriptのものが用意されています。大きく以下の2つに分類されています。

  1. Stream APIs : 映像や音声を扱うAPI。getUserMedia()関数で端末のカメラとマイクを利用可能。
  2. PeerConnection APIs : ブラウザ間でP2P通信をするためのAPI

公式サイトWebRTC Native APIsではこれらのAPIの詳細についての記載があります。

RTC(リアルタイムコミュニケーション)の方法

Web RTC(Real Time Communication)の目的であるRTCの実現方法は以下のような物です。

  1. 端末の映像/音声ストリームデータを取得

    • WebRTCのStream APIを利用
  2. ストリームデータを相手に届ける。

    • WebRTCのPeerConnection APIを利用

      • 通信相手とアドレスやポート情報を共有するために、STUNサーバを用意する必要がああります。公開サーバもありますがややハードルが高いです。
      • こてさきAjax - WebRTC事始めでは、Googleが公開しているSTUNサーバを使って通信を行った事例が掲載されています。
    • WebSocketを利用

      • PeerConnection APIはSTUNサーバが必要で大変なので、馴染みやすい方法としてNode.jsなどでWebsocketサーバを立てて、Webアプリを使ってリアルタイムコミュニケーションを行う方法もあります。

以降では、1.の「映像/音声の取得」のうち映像の取得を行います。また取得した映像にエフェクトをかけてみます。

Web RTC - Stream API の使い方

概要

次の2つの使い方を説明します。

  • 端末のWebカメラから取得した映像をそのまま表示する方法。
  • 端末のWebカメラから取得した映像に(白黒化などの)エフェクトをかけて表示する方法。

手順

端末のカメラの映像をそのまま表示

  1. 端末のWebカメラから映像ストリームを取得してVideoタグで表示します。

    • WebRTCのStream APIで定義されているgetUserMedia()関数を使ってストリームを取得します。
    • videoタグのsrcにストリームを割り当てます。

端末のカメラの映像にエフェクトをかけて表示

上の項目の続きです。動画から画像を等間隔で抜き出しパラパラアニメのように表示させます。画像にするのでエフェクトの適用が容易に行えます。

  1. videoタグのデータを、短い間隔でCanvasタグに貼付けます。

    • 2,3,4はsetInterval()関数内で行います。
  2. canvasタグのデータにエフェクトを適用します。

    • Javascriptでエフェクト用の関数を作成します。
  3. canvasタグのデータをDataURL形式に変換してimgタグに貼付けます。

    • このDataURL形式のデータは扱いやすくwebsocket等で簡単にサーバに送れます。

サンプルコード

node.js + expressの例ですが、node.js固有の機能は使っていませんので、jadeをhtmlに書き換えて静的なhtmlとjavascriptの構成にしても動作します。

Javascript - webrtc.js

// WebRTC Demo
// 
// ■処理の流れ
// 端末カメラ->Videoタグ->Canvasタグ->エフェクト->DataURL化->imgタグ

$(function(){
    //端末の差異を吸収
    navigator.getMedia = (  navigator.getUserMedia ||
                            navigator.webkitGetUserMedia ||
                            navigator.mozGetUserMedia ||
                            navigator.msGetUserMedia );
    
    //端末のCameraのstreamデータを流すVideoタグ
    var video = document.getElementById('camera');
    
    //StreamデータをDataURL化するために一時的に貼付けるCanvasタグ
    var canvas = document.getElementById('camera_canvas');
    var ctx = canvas.getContext('2d');
    
    //DataURLを貼付けるimgタグ
    var image1 = document.getElementById('camera_image1');
    var image2 = document.getElementById('camera_image2');
    var image3 = document.getElementById('camera_image3');
    
    //Videoタグ
    navigator.getMedia ({ video:true, audio:true }, function(stream) {
        video.src = window.URL.createObjectURL(stream);
    }, function(err){console.log(err);});
    
    function cameraToImage(modifyFunc,img){
        var canvas_image = ctx.drawImage(video,0,0,400,300);
        canvas_image = ctx.getImageData(0, 0, 400, 300);
        modifyFunc(canvas_image.data);
        ctx.putImageData(canvas_image, 0, 0);

        var dataURL = canvas.toDataURL("image/octet-stream");
        img.src = dataURL;
    }

    //動画から画像を生成
    setInterval(function(){
        cameraToImage(toGlayScale,image1);
        cameraToImage(toBlackWhite,image2);
        cameraToImage(toRedNoise,image3);
    }, 200);

    //Effect function : glayscale
    function toGlayScale(pixel){
        for (var i = 0, n = pixel.length; i < n; i += 4) {
            var gr = pixel[i  ] * 0.2 + pixel[i+1] * 0.2 + pixel[i+2] * 0.2;
            pixel[i  ] = gr; //R
            pixel[i+1] = gr; //G
            pixel[i+2] = gr; //B
        }
    }
    
    //Effect function : red noise
    function toRedNoise(pixel){
        for (var i = 0, n = pixel.length; i < n; i += 4) {
            pixel[i  ] *= Math.random()*2; //R
            pixel[i+1] *= Math.random()*0.3; //G
            pixel[i+2] *= Math.random()*0.3; //B
        }
    }
    
    //Effect function : black & white
    function toBlackWhite(pixel){
        for (var i = 0, n = pixel.length; i < n; i += 4) {
            var bw = pixel[i  ] * 4 + pixel[i+1] * 4 + pixel[i+2] * 4;
            pixel[i  ] = bw; //R
            pixel[i+1] = bw; //G
            pixel[i+2] = bw; //B
        }
    }
    
    $('img').click(function(){
        var dataURL = $(this).attr('src');
        var $img = $('<img>').attr('src', dataURL);
        $($img).prependTo('#snapshot');
    });
  
});

HTML(jade) - layout.jade

doctype 5
html
  head
    title= title
    link(rel='stylesheet', href='/stylesheets/style.css')
    script(type='text/javascript', src='/javascripts/jquery.js')
    script(type='text/javascript', src='/javascripts/webrtc.js')
  body
    block content

HTML(jade) - index.jade

extends layout

block content
    h1 WebRTC Stream API Demo

    h2 Video
    video#camera(width='400', height='300', autoplay)
    img#camera_image1(src='')
    img#camera_image2(src='')
    img#camera_image3(src='')
    canvas#camera_canvas(style='display:none;', width='400', height='300')
    
    h2 Capture 
    #snapshot