Help us understand the problem. What is going on with this article?

WebRTCにおけるメディアの取り扱い

More than 5 years have passed since last update.

WebRTC のピアコネクションにおける主要な機能のひとつに P2P メディア通信の機能があります。これは、ブラウザ上での音声映像の通話アプリケーションを実現します。ブラウザ上で動作するアプリケーションですからもちろんメディアの制御だって JavaScript の API 越しに行います。メディアの取り扱いといえば、高度な信号処理の知識が必要となる分野を想定されるかもしれませんが心配は無用です。それらを簡単に扱うためのAPIが、W3CのMedia Capture and Streamsに規定されており、これを使うことができます。

この記事では、WebRTC の APIにおけるメディアの考え方とその利用法について解説します。また、メディアをこと細やかに制御したい開発者向けに、WebRTC 以外の API からWebRTC のメディアにアクセスして利用する方法のいくつかも紹介します。なお、本文中のサンプルコードはWebRTC仕様およびMedia Capture and StreamsをはじめとしたW3C仕様に基づいたものであり、実際のブラウザで動作させるためには、たとえば webkitGetUserMedia のように適宜ベンダプレフィックスを付与する必要があることに注意して下さい。

ストリームとトラック

WebRTC におけるメディアの最小の単位はトラックであり、アプリケーションはトラックを束ねたストリームの単位でそれらを扱います。スマートフォンからのメディア取得イメージを以下に示します。

webrtc_track.png

ストリーム内にはメディアソースごとのトラックが束ねられています。更にトラックは、同一ソースのメディアが束ねられており、たとえモノラルのボイストラックであっても5.1chのオーディオトラックであっても単一のものとして扱われます。また、多くのスマートフォンはフロントとリアに2つのカメラを搭載しているでしょうから、それらも単一のトラックに束ねられています。アプリケーションはトラックから任意にメディアを取り出して利用することができます。

これらのトラックは、ピアコネクションに接続して対向のピアに送信することもできますし、ローカルのHTMLエレメントにアタッチして表示したり再生したりすることもできます。もちろん、HTMLエレメントは自由に配置して構いませんから、スクリーンキャプチャによるプレゼンテーションスライドを表示し、その片隅に発表者を撮影した映像を重畳させるといったアプリケーションも実現出来るでしょう。

JavaScript のコードにおいて、ここでのストリームは MediaStream として、トラックは MediaStreamTrack としてAPIが規定されています。新たな MediaStream の生成方法は2通りです。ひとつは getUserMedia 関数によるローカルメディアへのアクセスで、もう一方はピアコネクションによるリモートピアのメディアの取得です。ここから先はローカルメディアの取得方法と制御について解説します。

getUserMedia によるローカルメディアの取得

ローカルメディアの取得にはgetUserMedia関数を利用します。以下にオーディオのストリームとビデオのストリームを取得しmy-videoのidを持つvideoエレメントにアタッチするコード例を示します。

var didntGetUserMedia = function(stream) {
    throw new Error("cannot get local media.");
};

var gotUserMedia = function(stream){
    var video = getElementById("my-video");
    // 取得したストリーム video エレメントで再生
    video.srcObject = stream; 
    video.play();
};
// ローカルメディアを取得
navigator.getUserMedia({"audio":true, "video":true}, gotUserMedia, didntGetUserMedia); 

getUserMedia関数は3つの引数を取り、それらは取得メディアの指定、成功時コールバック、失敗時コールバックで、非同期に実行されます。まず、(カメラやマイクなどのデバイスが存在しない場合もあるため)メディアが取得可能かを確認し、それが可能であればユーザに対してそれらへのアクセスの許諾を求めます。ユーザがそれを承認すると成功時コールバックが呼び出されます。この例での成功時コールバックはgotUserMediaで、引数にストリームを与えられて呼び出されます。ここでは与えられたストリームをvideoエレメントのsrcObjectプロパティに設定しています。srcObjectはWebRTCのために新たに規定されたHTMLエレメントの属性であり、videoの他にaudioなどの全てのメディアエレメントで利用することができます。

制約を課すことによるローカルメディアの制御

WebRTC の API 越しにメディアソースを直接編集することはできませんが、制約(constraints)を課す事によって制御することは可能です。制約として課せる(constrainable)プロパティには列挙(enumerated)と範囲(range)の2つの形式があります。列挙は値のリスト、範囲は最小値と最大値を値として与えます。なお、各トラックに課すことが可能な値は getCapabilities 関数を呼び出すことによって知ることができます。

メディア取得成功時のコールバックであるgotUserMediaに、ひとつのビデオトラックに対して制約を課す処理を追加した例を以下に示します。

var gotUserMedia = function(stream) {
    var video = getElementById("my-video");
    // 取得したストリーム video エレメントで再生
    video.srcObject = stream; 
    video.play();
    // ビデオトラックの最初のソースを取得
    var videoTrack = stream.getVideoTracks()[0];
    // 制約を設定 
    var constraints = {
        "mandatory": {"aspectRatio": 1.3333}, 
        "optional": [{"width": {"min": 640}},
                     {"height": {"max": 400}}]
    };
    videoTrack.applyConstraints(constraints);
};

この例では、まずvideoTrackにストリームの最初のビデオトラックを取得して設定しています。このトラックは、多くのスマートフォンの場合ではフロントカメラの映像です。そして次に、制約を定義するオブジェクトであるconstraintsを定義し、トラックのapplyConstraints関数を呼ぶことによって制約を課しています。制約には強制を意味するmandatoryプロパティと、任意を意味するoptionalプロパティに値を設定することができます。この例では、アスペクト比に1.3333(すなわち4:3)を強制し、最低の幅に640と最大の幅に400を可能な限り守るようブラウザに促します。

ストリーム全体に共通の制約を課す場合は、以下の例のようにgetUserMediaを呼び出す段階で設定することも可能です。

var constraints = {
    "mandatory": {"aspectRatio": 1.3333}, 
    "optional": [{"width": {"min": 640}},
                 {"height": {"max": 400}}]
};
navigator.getUserMedia({"audio":true, "video":constraints}, gotUserMedia, didntGetUserMedia); 

この制約の仕組みは、様々なアプリケーションのユースケースから要求に対して柔軟に対応することを実現しています。たとえば輻輳が発生してビットレートを下げざるを得なくなった場合、そこでするべきことはアプリケーションによって異なるでしょう。細やかなアートワークを共有するようなアプリケーションではリフレッシュレートよりも解像度が優先されますでしょうし、スポーツ中継のようなケースであればそれは逆転するでしょう。フレームレートの低いサッカー中継を想像してみてください、それは最悪の体験でしょう。

さて、ブラウザは課せられた制約を守ることができなくなった場合には、overconstraintedイベントを発生させます。制約はメディア取得後にも変更することができます。たとえば、単純な通話アプリケーションにおいて開発者はそれがユーザにどのような用途に使われるか知り得ません。そのため、品質の設定のインターフェースを利用者に提供するかもしれません。このとき、ユーザがブラウザに与えた設定が制約過剰となる場合もあるでしょう。そのようなアプリケーションでは、制約過剰の可能性のあるトラックに対してoverconstraintedハンドラを設定しておくべきです。

現状の制約に設定可能な項目は非常に限られていますが、draft-burnett-rtcweb-constraints-registryをはじめとした様々なドラフトで議論されており、今後よりこと細やかな制御が可能となるかもしれません。

WebRTC 以外の API を用いたメディア自体のハンドリング

WebRTC の API だけでメディア自体をハンドリングすることは範囲外ですが、メディアにアクセスするための手段を他のAPIに提供することは可能です。つまり、MediaStreamを他のAPIに渡すことによって、メディアを直接ハンドリングすることが可能という訳です。ここからはその方法のいくつかを紹介します。

Recoding API

音声や映像メディアを直接ハンドリングしたいという最も原始的な欲求は、やはりそれを記録として残したいというものでしょう。特にビジネスの世界、とりわけコールセンタにおいては、顧客満足度の調査などを目的として音声を録音する装置が使われることが非常に多いです。

このような記録のためのAPIとしてMediaStream Recording が規定されています。このAPIは非常にシンプルに規定されており、以下のコード例のように MediaRecoderインスタンスを生成することによって、簡単に録画録音機能を実現することが出来ます。

var record = function(length,stream) {
    var recorder = new MediaRecorder(stream);

    recorder.ondataavailable = function(event) {
        if (recorder.state == 'recording') {
            var blob = new Blob([event.data], {
                type: 'audio/ogg'
            });
            recorder.stop();
        }
    };

    recorder.onstop = function() {
        recorder = null;
    };

    recorder.start(length);
};

Recording API は Mozilla Firefox では実装が始まっていますが、残念ながら Google Chrome では対応していないようです。

Web Audio

ブラウザ上でオーディオをこと細やかに制御するAPIとしてWebAudioが規定されており、これを用いてWebRTCのストリームやトラックをデータ化したり、エフェクトをかける等の手を加えることができます。WebAudio の根幹となるインターフェースは AudioContext であり、WebRTCのメディアストリームからの生成は以下のように createMediaStreamSource 関数を用います。

var audioContext = new AudioContext().createMediaStreamSource(stream);

WebAudioは、オーディオの全てを制御したい開発者のために整備されており、その特性から信号処理の専門的な知識が必要となります。たとえ、ただ録音機能を実現しようとするだけであっても、取得した生のBlobからPCMファイルを生成するために以下のようなコードが必要となります。(ここで必要なのはPCMファイルの仕様の知識だけであり、コードが煩雑となることを示すことを目的としています。)

var encodeWAV = function(samples) {
    var buffer = new ArrayBuffer(44 + samples.length * 2);
    var view = new DataView(buffer);

    // RIFF 識別子
    writeString(view, 0, 'RIFF');
    // ファイル長
    view.setUint32(4, 32 + samples.length * 2, true);
    // RIFF タイプ
    writeString(view, 8, 'WAVE');
    // フォーマットチャンク識別子
    writeString(view, 12, 'fmt ');
    // フォーマットチャンク長
    view.setUint32(16, 16, true);
    // サンプルフォーマット(raw)
    view.setUint16(20, 1, true);
    // チャネルカウント
    view.setUint16(22, 2, true);
    // サンプリングレート
    view.setUint32(24, sampleRate, true);
    // バイトレート (サンプリングレート * ブロックアライン)
    view.setUint32(28, sampleRate * 4, true);
    // ブロックアライン (チャネルカウント * バイト/サンプル) 
    view.setUint16(32, 4, true);
    // ビット/サンプル 
    view.setUint16(34, 16, true);
    // データチャンク識別子 
    writeString(view, 36, 'data');
    // データチャンク長
    view.setUint32(40, samples.length * 2, true);

    floatTo16BitPCM(view, 44, samples);

    return view;
};

なお、WebRTC のストリームからの AudioContext の生成は現在、Google Chrome だけで対応しており、しかもローカルメディアだけで動作し、リモートピアのストリームではうまく動作しないようです。

Canvasを用いた映像の取得

WebRTC の映像ストリームからデータを取得する仕組みは現状では存在しませんが、動画から静止画を切り出すことは原始的なvideoエレメントの機能を利用することによって実現可能です。厳密に言えば、videoエレメントというよりはHTML Canvasの機能です。以下のようなコードによって、新規にCanvasを作成し、video変数に取得したvideoエレメントのフレームをキャプチャし、Data URIとして返すことが可能です。

var captureFrame = function(){
    var canvas = document.createElement('canvas');
    var ctx = canvas.getContext('2d')
    canvas.width = video.videoWidth;
    canvas.height = video.videoHeight;
    ctx.drawImage(video, Math.floor((videoHeight-videoWidth)/2), 0);
    var dataURL = canvas.toDataURL('image/jpeg');
    return dataURL;
};

このコードが、既存のスチルカメラの機能を置き換えるかというと少々力不足です。何故ならば WebRTC の映像ストリームは動画で用いることを想定しており、スチルカメラとはその考え方が全く異なるからです。これは、スチルと動画でのシャッタースピードの考え方が全く異なることに由来しています。

逆に、高頻度に映像を取得し、それに加工して再び動画のように見せるようなケースではこのコードは役に立つでしょう。そのような例としてneave/face-detectionのように画像を解析して顔の位置を推定してそこになんらかの画像を重畳するようなデモが存在しています。

まとめ

以上、WebRTC におけるメディアの考え方や使い方、そしてWebRTCの垣根を超えた活用方法について解説しました。スマートフォンやタブレット端末の登場により、複数のカメラやマイクを搭載した端末が一般的になってきています。また、動画配信向けに複数の映像や音声を同時に取得可能なデバイスも普及し始めています。WebRTCではそれらの複数のメディアを同時に活用したアプリケーションの実現が可能です。また、そのメディアに対して WebRTC 以外の種々の API を用いて手を加えることも可能です。今後これらの機能を活用したアプリケーションの登場が期待されます。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした