WebRTC
SkyWay
sfu

WebRTC Platform SkyWay ハンズオン SFU編

はじめに


作者紹介

なかゆうすけ(@yusuke84

  • SkyWay Team @ NTTコミュニケーションズ
    • Technical solutions & support enginner
    • Developer relations(Community manager)
    • (Unofficial) SkyWay Evangelist
  • コミュニティ
    • WebRTC Meetup Tokyo 主催
    • WebRTC Beginners Tokyo 主催

今日作るもの

本編で作成する最終的なアプリのデモをお見せします。


動作確認済みブラウザ

  • Chrome M67 Stable
  • Firefox 60 Stable

今日の流れ


STEP0

  • SkyWayの開発者登録をしてみよう

STEP1

  • ブラウザでカメラ、マイクを利用してみよう

STEP2

  • 1:1のビデオチャットを実装してみよう

STEP3

  • SkyWayのroom機能を利用して複数人によるビデオチャットを実装してみよう

STEP4

  • 応用編1
    • カメラとマイクを選択できるようにしてみよう

STEP5

  • 応用編2
    • 録音録画機能を実装してみよう

ハンズオン用ソースコード

  • https://github.com/skyway/skyway-handson-js
    • masterブランチをローカルにダウンロードして下さい
    • STEP1〜STEP5までの各フォルダに各STEPの完成形のソースコードが格納されています
    • handsonフォルダのコードをベースに開発を進めて下さい
    • githubを使ったことがない方はZIPでダウンロードして展開して下さい
  • SkyWayとは別にjQueryを利用しています

開発環境を整える

  • Webカメラ:PC内蔵のものもしくはUSB接続の外付のもの、無ければWebブラウザで認識できる仮想カメラソフト
  • エディタ: お好きなものをご利用下さい(Visual Studio Code など)
  • ブラウザ: Google Chrome(Firefoxでも動くはず)
  • ローカルWebサーバが起動できること:apache/simpleHTTPServer/php等

STEP0


SkyWayの開発者登録をしてみよう

スクリーンショット 2018-06-22 17.39.07.png


登録に必要な情報を入力

項目 内容
名前 フルネームでお願いします
メールアドレス 連絡の取れるアドレスを入れて下さい
メールアドレスがID代わりになります
パスワード パスワードを設定して下さい

登録完了後ダッシュボードへログイン

  • アプリケーションを作成する

スクリーンショット 2018-06-22 17.36.48.png


「利用可能ドメイン」について

  • SkyWayのAPIキーにはドメイン認証がかかっています

    • JavaScript SDKの場合は、SkyWayを利用したWebアプリをホスティングするWebサイトのドメイン名を設定
    • iOS / Android SDK同士の場合は、任意の文字列を設定
    • ワイルドカード文字(*)(Ex: *.xxx.co.jp)の利用もできます。
  • ハンズオン用のAPIキーには localhost を設定


「APIキー認証」について

  • 「APIキー認証を利用する」をチェックすると認証サーバと連携した認証機能利用が必須になる

スクリーンショット 2018-06-22 17.50.27.png


アプリケーションの設定内容について

詳しく知りたい方は、SkyWay公式チュートリアルをご確認ください。


STEP1


ブラウザでカメラ、マイクを利用してみよう

  • getUserMedia()を利用してカメラとマイクを取得する
    • Promiseによる非同期処理を行うAPI
navigator.mediaDevices.getUserMedia({video: true, audio: true})
    .then(function (stream) { // success
    }).catch(function (error) { // error
    return;
});


getUserMediaに必要な処理を追加する

  • script.jsに追記して下さい
let localStream = null;

navigator.mediaDevices.getUserMedia({video: true, audio: true})
    .then(function (stream) {
        $('#myStream').get(0).srcObject = stream;
        localStream = stream;
    }).catch(function (error) {
        console.error('mediaDevice.getUserMedia() error:', error);
        return;
    });

実装する際のポイント

  • VideoとAudioの選択

{video: true, audio: true}

  • 最低でも640x480以上、出来たら1280x720が良いけどだめならよしなにやってください

{ audio: true, video: { width: {min: 640, ideal: 1280}, height: {min: 480,ideal: 720} } }

  • 絶対に640x480がいいです…それ以外だったら動作しなくてもOK
    • min,maxの代わりに exact: 640 と指定することも可能

{ audio: true, video: { width: {min: 640, max: 640}, height: {min: 480, max: 480} } }

  • フレームレートの設定(一例/Chrome限定)

{ audio: true, video: { frameRate: { min: 10, max: 15 } } }


使用する上の注意点

  • 許可を求めるダイアログが出てくる
    • 複数のカメラやマイクが接続されている場合は、適切なものを選択する必要あり

Chrome

chrome_gum.png

Firefox

firefox_gum.png


使用する上の注意点

  • ホスティング先に注意
スキーマ\ブラウザ Chrome Firefox
http://localhost/
http://domain.jp x
file://index.html x
https://domain.jp

STEP2


1:1のビデオチャットを実装してみよう

SkyWayを利用して1:1のビデオチャットを実現してみます。


SkyWayのSDKを利用する

今回のハンズオンのHTMLには既に記載済みですが、以下の通りScript要素でSDKをインポートします。

<script type="text/javascript" src="https://cdn.webrtc.ecl.ntt.com/skyway-latest.js"></script>

Peerオブジェクトを作成する

  • script.jsに追記して下さい
let localStream = null;
let peer = null;
let existingCall = null;

navigator.mediaDevices.getUserMedia({video: true, audio: true})
    // 省略
});

peer = new Peer({
    key: 'apikey',
    debug: 3
});

実装する際のポイント

  • ダッシュボードで払い出したAPIキーを設定する
  • デバッグレベル(console log)で表示する情報を規定する
  • peer.jsを基本的には踏襲しています
peer = new Peer({
    key: 'apikey',
    debug: 3
});

実装する際のポイント

  • new Peer()のoptions object
    peer = new Peer(peerid,options);
Name Description Required
key APIキー *
debug ログレベル(None:0,ERROR:1,WARN:2,FULL:3)
turn SkyWayで提供するTURNを利用するかどうか(true/false)
credential APIキー認証のためのクレデンシャル情報
config RTCPeerConnectionに渡されるオブジェクト
例:TURNサーバやSTUNサーバを個別に指定したりできる
  • 上記以外のオプションパラメータについてはAPI Referenceを参照

PeerオブジェクトのEventListenerを追加する

  • script.jsに追記して下さい
peer.on('open', function(){
    $('#my-id').text(peer.id);
});

peer.on('call', function(call){
    call.answer(localStream);
    setupCallEventHandlers(call);
});

peer.on('error', function(err){
    alert(err.message);
});

実装する際のポイント(1)

  • Openイベント
    • SkyWayのシグナリングサーバと接続し、利用する準備が整ったら発火します
    • 今回は、PeerIDと呼ばれるクライアント識別用のIDをシグナリングサーバで発行し、その情報をUIに表示する処理を行っています
peer.on('open', function(){
    $('#my-id').text(peer.id);
});

実装する際のポイント(2)

  • Callイベント
    • 相手から接続要求がきた場合に発火します
    • 相手との接続を管理するためのCallオブジェクトが取得できるため、それを利用して必要な処理を行います
      • Answerメソッドを実行し、接続要求に応答します。この時に、自分自身のlocalStreamをセットすると、相手に映像・音声を送信することができるようになります
      • Callオブジェクトを利用したEventListenerをセットします
        • setupCallEventHandlers()の中身については後ほど説明
peer.on('call', function(call){
    call.answer(localStream);
    setupCallEventHandlers(call);
});

実装する際のポイント(3)

  • Errorイベント
    • 何らかのエラーが発生した場合に発火します
peer.on('error', function(err){
    alert(err.message);
});

発信、切断処理の為の処理を追加する

  • script.jsに追記して下さい
$('#make-call').submit(function(e){
    e.preventDefault();
    const call = peer.call($('#peer-id').val(), localStream);
    setupCallEventHandlers(call);
});

$('#end-call').click(function(){
    existingCall.close();
});

実装する際のポイント(1)

  • 発信処理
    • peer.callで相手のPeerID、自分自身のlocalStreamを引数にセットし発信します
      • PeerIDは電話番号のようなもので、何らかの方法で入手する必要があります
    • Callオブジェクトが返ってくるため、必要なEventListenerをセットします
      • setupCallEventHandlers()の中身については後ほど説明
$('#make-call').submit(function(e){
    e.preventDefault();
    const call = peer.call($('#peer-id').val(), localStream);
    setupCallEventHandlers(call);
});

実装する際のポイント(2)

  • 切断処理
    • Callオブジェクトのclose()メソッドを実行します
    • 先程生成したCallオブジェクトはexistingCallとして保持しておきます
      • オブジェクト保持はsetupCallEventHandlers()の中で実行します
$('#end-call').click(function(){
    existingCall.close();
});

CallオブジェクトのEventListenerを追加する

  • script.jsに追記して下さい
function setupCallEventHandlers(call){
    if (existingCall) {
        existingCall.close();
    };

    existingCall = call;

    call.on('stream', function(stream){
        addVideo(call,stream);
        setupEndCallUI();
        $('#connected-peer-id').text(call.remoteId);
    });

    call.on('close', function(){
        removeVideo(call.remoteId);
        setupMakeCallUI();
    });
}

実装する際のポイント(1)

  • 今回作るアプリでは既に接続中の場合は一旦既存の接続を切断し、後からきた接続要求を優先する
    • アプリの仕様次第
if (existingCall) {
    existingCall.close();
};

existingCall = call;

実装する際のポイント(2)

  • Streamイベント
    • 相手の映像・音声を受信した際に発火します
    • 取得したStreamオブジェクトをVIDEO要素にセットします
      • addVideo()の中身については後ほど説明
    • UI関連の処理を実施します
      • 切断用のボタンを表示
call.on('stream', function(stream){
    addVideo(call,stream);
    setupEndCallUI();
    $('#connected-peer-id').text(call.remoteId);
});

実装する際のポイント(3)

  • Closeイベント
    • Callオブジェクトのclose()メソッドが実行され切断処理が完了したら発火します
    • close()メソッドを実行した側、実行された側それぞれで発火します
    • 切断時にVIDEO要素を削除します
      • call.remoteIdで切断先のPeerIDを取得できます
      • removeVideo()の中身については後ほど説明
    • UI関連の処理を実施します
      • 接続用のボタン、PeerIDを入力するInputボックスを用意
call.on('close', function(){
    removeVideo(call.remoteId);
    setupMakeCallUI();
});

必要な関数を準備する

  • script.jsに追記して下さい
function addVideo(call,stream){
    const videoDom = $('<video autoplay>');
    videoDom.attr('id',call.remoteId);
    videoDom.get(0).srcObject = stream;
    $('.videosContainer').append(videoDom);
}

function removeVideo(peerId){
    $('#'+peerId).remove();
}

function setupMakeCallUI(){
    $('#make-call').show();
    $('#end-call').hide();
}

function setupEndCallUI() {
    $('#make-call').hide();
    $('#end-call').show();
}

実装する際のポイント(1)

  • VIDEO要素のsrcObjectプロパティにStreamオブジェクトをセットすることで再生できます
    • 削除する処理のことを考えて、idプロパティにcall.remoteId(PeerID)をセットします
function addVideo(call,stream){
    const videoDom = $('<video autoplay>');
    videoDom.attr('id',call.remoteId);
    videoDom.get(0).srcObject = stream;
    $('.videosContainer').append(videoDom);
}

実装する際のポイント(2)

  • 切断された(した)相手のVIDEO要素をPeerIDを元に削除します
function removeVideo(peerId){
    $('#'+peerId).remove();
}

実装する際のポイント(3)

  • UI関連処理を実装します
function setupMakeCallUI(){
    $('#make-call').show();
    $('#end-call').hide();
}

function setupEndCallUI() {
    $('#make-call').hide();
    $('#end-call').show();
}

1:1のビデオチャットを試してみる

  1. 2つのブラウザタブでアプリを開く
  2. 片方のYour idを片方のInputボックスにコピペしてCallボックスをクリックする

STEP3


SkyWayのRoom機能を利用して複数人によるビデオチャットを実装してみよう

  • WebRTCは1:1の通信をP2Pでやることを前提に作られていますが、それを応用することで複数人で通信することが可能になります
  • SkyWayではRoom機能を提供し、より直感的に複数人によるビデオチャット等を実現できるようになっています
  • STEP2で作成したコードを修正します

getUserMediaのキャプチャサイズを変更する

  • script.jsを修正して下さい
let constraints = {
    video: {},
    audio: true
};
constraints.video.width = {
    min: 320,
    max: 320
};
constraints.video.height = {
    min: 240,
    max: 240        
};

navigator.mediaDevices.getUserMedia(constraints)
    .then(function (stream) { // success
        $('#myStream').get(0).srcObject = stream;
        localStream = stream;
    }).catch(function (error) { // error
    console.error('mediaDevice.getUserMedia() error:', error);
    return;
});

実装する際のポイント(1)

  • 320x240に制限します
    • 複数人のビデオチャットの場合、ブラウザに掛かる負荷が増えるため、メディアのキャプチャサイズを制限し負荷を減らすことが出来ます
    • minとmaxでその解像度を矯正します
      • サイズだけを指定するとブラウザの制御に委ねられる
      • ただし、制御した解像度で映像が取得できなければエラーになります
let constraints = {
    video: {},
    audio: true
};
constraints.video.width = {
    min: 320,
    max: 320
};
constraints.video.height = {
    min: 240,
    max: 240 
};

navigator.mediaDevices.getUserMedia(constraints)
    .then(function (stream) {
        $('#myStream').get(0).srcObject = stream;
        localStream = stream;
    }).catch(function (error) {
        console.error('mediaDevice.getUserMedia() error:', error);
        return;
    });

発信処理をRoomへの参加処理に変更する

  • script.jsを修正して下さい
$('#make-call').submit(function(e){
    e.preventDefault();
    let roomName = $('#join-room').val();
    if (!roomName) {
        return;
    }
    const call = peer.joinRoom(roomName, {mode: 'sfu', stream: localStream});
    setupCallEventHandlers(call);
});

実装する際のポイント(1)

  • 接続先のPeerIDの代わりに参加するRoom名を入力してもらい、そのRoom名を引数に指定し、peer.joinRoomメソッドを実行する
    • modeはsfumeshから選択可能
let roomName = $('#join-room').val();
if (!roomName) {
  return;
}
const call = peer.joinRoom(roomName, {mode: 'sfu', stream: localStream});

実装する際のポイント(2)

  • SFUとMeshの違い
    • SFUはサーバに対して自分自身のメディアストリームを送信する
    • Meshは参加者全員に対してメディアストリームを送信する
    • 受信はどちらとも参加者分だけ行う
    • SFUの方が端末の負荷が軽い

sfu_mesh.png


CallオブジェクトのEventListenerにRoom機能を実現するためのイベントを追加する

  • script.jsを修正して下さい
function setupCallEventHandlers(call){
    if (existingCall) {
        existingCall.close();
    };

    existingCall = call;
    setupEndCallUI();
    $('#room-id').text(call.name);

    call.on('stream', function(stream){
        addVideo(stream);
    });

    call.on('peerLeave', function(peerId){
        removeVideo(peerId);
    });

    call.on('close', function(){
        removeAllRemoteVideos();
        setupMakeCallUI();
    });

}

実装する際のポイント(1)

  • 1:1の時はStreamイベント内部にあった処理を外に出す
    • Room機能を利用する場合はRoomに参加した時点で通信開始となるため
setupEndCallUI();
$('#room-id').text(call.name);

実装する際のポイント(2)

  • Room機能を利用するとStreamオブジェクトにPeerIDが格納されるため、addVideoの第一引数に指定したCallオブジェクトは省略できます
    • addVideo()の中身については後ほど
call.on('stream', function(stream){
    addVideo(stream);
});

実装する際のポイント(3)

  • peerLeaveイベント
    • Roomから参加者が抜けたら発火します
    • 抜けた参加者のPeerIDを取得できるため、そのIDを利用して対応するVIDEO要素を削除します
call.on('peerLeave', function(peerId){
    removeVideo(peerId);
});

実装する際のポイント(4)

  • Closeイベント
    • close()メソッドを実行し、自分自身がRoomから抜けた場合に発火します
    • 複数参加者がいる場合があるため、全てのVIDEO要素を削除します
      • removeAllRemoteVideos()の中身については後ほど
call.on('close', function(){
    removeAllRemoteVideos();
    setupMakeCallUI();
});

addVideoを修正しremoveAllRemoteVideosを追加する

  • script.jsを修正して下さい
function addVideo(stream){
    const videoDom = $('<video autoplay>');
    videoDom.attr('id',stream.peerId);
    videoDom.get(0).srcObject = stream;
    $('.videosContainer').append(videoDom);
}

// 省略

function removeAllRemoteVideos(){
    $('.videosContainer').empty();
}

実装する際のポイント(1)

  • stream.peerIdでPeerIDが取得できます
function addVideo(stream){
    const videoDom = $('<video autoplay>');
    videoDom.attr('id',stream.peerId);
    videoDom.get(0).srcObject = stream;
    $('.videosContainer').append(videoDom);
}

実装する際のポイント(2)

  • jQueryの機能を使いVIDEO要素全てを削除します
function removeAllRemoteVideos(){
    $('.videosContainer').empty();
}

UI上の文言を修正する

  • index.htmlを修正して下さい
<div class="myControllerContainer">
    <p>Your id: <span id="my-id">...</span></p>
    <form id="make-call">
        <input type="text" placeholder="Join room..." id="join-room">
        <button type="submit">Join</button>
    </form>
    <div id="end-call">
        <p>Currently in room <span id="room-id">...</span></p>
        <button>Leave</button>
    </div>
</div>

実装する際のポイント(1)

  • CallからJoin roomへ変更します
<input type="text" placeholder="Join room..." id="join-room">
<button type="submit">Join</button>
<p>Currently in room <span id="room-id">...</span></p>
<button>Leave</button>

複数人によるビデオチャットを試してみる

  1. 3つのブラウザタブでアプリを開く
  2. 任意のRoom名(半角英数)を決めて、全アプリでそのRoomに参加する

STEP4(応用編)


カメラとマイクを選択できるようにしてみよう

getUserMediaを利用するとブラウザが提供するダイアログウィンドウにてマイク、カメラを選択することが出来ます。

chrome_gum.png


カメラとマイクを選択できるようにしてみよう

その機能をアプリ内に組込むことも出来ます。そうすることで、より利用者に優しいUI設計が可能となります。

STEP4ではSTEP3までで開発したアプリにその機能を組み込みます。また、ビデオチャット中にカメラ切り替えを可能にします。

動作確認には複数のカメラ、マイクが必要です。


カメラとマイクを選択できるようにしてみよう

  • enumerateDevicesというAPIを利用
navigator.mediaDevices.enumerateDevices()
    .then(function (devices) { // success
    }).catch(function (error) { // error
    return;
});

カメラ、マイクの一覧を取得し切り替えられるようにする

  • script.jsに追記します
let audioSelect = $('#audioSource');
let videoSelect = $('#videoSource');

navigator.mediaDevices.enumerateDevices()
    .then(function(deviceInfos) {
        for (let i = 0; i !== deviceInfos.length; ++i) {
            let deviceInfo = deviceInfos[i];
            let option = $('<option>');
            option.val(deviceInfo.deviceId);
            if (deviceInfo.kind === 'audioinput') {
                option.text(deviceInfo.label);
                audioSelect.append(option);
            } else if (deviceInfo.kind === 'videoinput') {
                option.text(deviceInfo.label);
                videoSelect.append(option);
            }
        }
        videoSelect.on('change', setupGetUserMedia);
        audioSelect.on('change', setupGetUserMedia);
        setupGetUserMedia();
    }).catch(function (error) {
        console.error('mediaDevices.enumerateDevices() error:', error);
        return;
    });

実装する際のポイント(1)

  • UI上にマイクとカメラ選択用のリストボックスを用意し、そのリストに選択可能なマイクとカメラを追加します
    • HTMLの修正は後ほど
let audioSelect = $('#audioSource');
let videoSelect = $('#videoSource');

実装する際のポイント(2)

  • enumerateDevices()の実行に成功すると、deviceInfosというマイクとカメラ情報が格納された配列が返ってくる
    • その配列の中にある、deviceIdとlabelを取り出し、Option要素を生成しリストボックスに設定する
navigator.mediaDevices.enumerateDevices()
        .then(function(deviceInfos) {
            for (var i = 0; i !== deviceInfos.length; ++i) {
                let deviceInfo = deviceInfos[i];
                let option = $('<option>');
                option.val(deviceInfo.deviceId);
                if (deviceInfo.kind === 'audioinput') {
                    option.text(deviceInfo.label);
                    audioSelect.append(option);
                } else if (deviceInfo.kind === 'videoinput') {
                    option.text(deviceInfo.label);
                    videoSelect.append(option);
                }
            }

実装する際のポイント(3)

  • deviceInfosの中身

deviceinfos.png

項目名 用途
deviceId 識別IDでgetUserMediaでマイク、カメラを指定する際に利用する
groupId 同じ物理デバイスで複数認識されている場合に同一グループになる。例えば、PC等でサウンド出力を内蔵マイクとしている場合、既定と内蔵マイクは同一グループ
kind audioinput/videoinputの2値でAudioデバイスか、Videoデバイスかを識別可能
label PC等で付与されるラベルでユーザには一番わかり易い

実装する際のポイント(4)

  • 先程の処理で生成したOption要素の例
<option value="c81339aea15028711063a8f861df00e5e2c61cd9415200d2ebda016bf7d3374e">FaceTime HD カメラ</option>

実装する際のポイント(5)

  • セレクトボックスで選択状態が変更されたら都度getUserMedia()を実行する
videoSelect.on('change', setupGetUserMedia);
audioSelect.on('change', setupGetUserMedia);
setupGetUserMedia();

getUserMediaの処理を修正しdeviceIdを指定できるようにする

  • script.jsを修正します
function setupGetUserMedia() {
    let audioSource = $('#audioSource').val();
    let videoSource = $('#videoSource').val();
    let constraints = {
        audio: {deviceId: {exact: audioSource}},
        video: {deviceId: {exact: videoSource}}
    };

    // 省略

    if(localStream){
        localStream = null;
    }

    navigator.mediaDevices.getUserMedia(constraints)
        .then(function (stream) {
            $('#myStream').get(0).srcObject = stream;
            localStream = stream;

            if(existingCall){
                existingCall.replaceStream(stream);
            }

        }).catch(function (error) {
            console.error('mediaDevice.getUserMedia() error:', error);
            return;
        });
}

実装する際のポイント(1)

  • セレクトボックスで選択されたデバイスのdeviceIdを取得してgetUserMediaに渡すパラメーターを生成します
let audioSource = $('#audioSource').val();
let videoSource = $('#videoSource').val();
let constraints = {
    audio: {deviceId: {exact: audioSource}},
    video: {deviceId: {exact: videoSource}}
};

// 省略

実装する際のポイント(2)

  • getUserMediaが複数回実行する事を考慮して毎回ガベージコレクションします
if(localStream){
    localStream = null;
}

実装する際のポイント(3)

  • ビデオチャット中にマイクやカメラを切り替え場合、チャットを中断すること無くそれを反映させることが出来ます
    • replaceStreamというメソッドに新しいStreamオブジェクトを設定し実行します
if(existingCall){
    existingCall.replaceStream(stream);
}

通信中にマイクやカメラが変更された場合に検知するためのイベントを追加する

  • script.jsを修正します
function setupCallEventHandlers(call){

    // 省略

    call.on('stream', function(stream){
        addVideo(stream);
    });

    call.on('removeStream', function(stream){
        removeVideo(stream.peerId);
    });

    call.on('peerLeave', function(peerId){
        removeVideo(peerId);
    });

    // 省略

}

実装する際のポイント(1)

  • removeSrtreamイベントを追加します。相手がreplaceStreamメソッドを実行すると発火します
    • 発火したら該当のVIDEO要素を削除します
    • ほぼ同時にstreamイベントも発火し新しいStreamオブジェクトをVIDEO要素にセットします
call.on('removeStream', function(stream){
    removeVideo(stream.peerId);
});

セレクトボックスを設置する

  • index.htmlを修正して下さい
<!-- 省略 -->
<p>Your id: <span id="my-id">...</span></p>
<div class="select">
    <label for="audioSource">Audio input source: </label><select id="audioSource"></select>
</div>
<div class="select">
    <label for="videoSource">Video source: </label><select id="videoSource"></select>
</div>
<form id="make-call">
<!-- 省略 -->

実装する際のポイント(1)

  • マイクとカメラを選択するためのセレクトボックスを設置します
<div class="select">
    <label for="audioSource">Audio input source: </label><select id="audioSource"></select>
</div>
<div class="select">
    <label for="videoSource">Video source: </label><select id="videoSource"></select>
</div>

マイクカメラ切り替えを試してみる

  1. PCに複数のカメラを接続する(マイクよりわかりやすい)
  2. 3つのブラウザタブでアプリを開く
  3. 任意のRoom名(半角英数)を決めて、全アプリでそのRoomに参加する
  4. セレクタで切り替えられることを確認する

STEP5(応用編)


録音録画機能使ってみよう

ChromeやFirefoxではビデオチャットの映像音声を録音録画するAPIが備わっています。APIの全容は以下の通りです。

// getUserMedia()で取得したstreamをセットしておく
let localStream = null;
let recorder =  null;
function startRecording() {
  // MediaRecorderオブジェクトを利用できるようにする
 recorder = new MediaRecorder(localStream);
 recorder.ondataavailable = function(evt) {
  // レコーディングしたデータを非同期で取得できる
 }

 // レコーディング開始
 recorder.start();
}

// レコーディング停止
function stopRecording() {
 recorder.stop();
}

出典:WebRTCで録画する!MediaRecoderを使ってみよう@html5experts.jp


録音録画機能を実装してみよう

  • STEP5ではSTEP4までで開発したアプリにその機能を組み込みます。簡易版なのでRoom機能が備わっていますが、1:1のビデオチャットを想定します

Recordingボタンを追加する

  • script.jsに追記して下さい
$('#recording button').click(function(){
    if(recorder){
        recorder.stop();
        $('#recording button').text('Recording');
        $('#downloadlink').hide();
    }else if(remoteStream){
        let chunks = [];
        let options = {
            mimeType : 'video/webm; codecs=vp9'
        };

        recorder = new MediaRecorder(remoteStream,options);

        recorder.ondataavailable = function(evt) {
            console.log("data available: evt.data.type=" + evt.data.type + " size=" + evt.data.size);
            chunks.push(evt.data);
        };

        recorder.onstop = function(evt) {
            console.log('recorder.onstop(), so playback');
            recorder = null;
            const videoBlob = new Blob(chunks, { type: "video/webm" });
            blobUrl = window.URL.createObjectURL(videoBlob);
            $('#downloadlink').attr("download", 'recorded.webm');
            $('#downloadlink').attr("href", blobUrl);
            $('#downloadlink').show();
        };
        recorder.start(1000);
        console.log('start recording');
        $('#recording button').text('Stop');
        $('#downloadlink').hide();
    }
});

実装する際のポイント(1)

  • ボタン1つでRecordingとStopに対応させるため。既にRecordingしている場合はStopするようにします
  • UI関連の処理を実施します
    • ボタンのテキストをRecordingに変更
    • ダウンロードリンクを非表示
$('#recording button').click(function(){
    if(recorder){
        recorder.stop();
        $('#recording button').text('Recording');
        $('#downloadlink').hide();
    }else if(remoteStream){
        // 省略

実装する際のポイント(2)

  • Recordingデータを書くのするための配列の初期化
let chunks = [];

実装する際のポイント(3)

  • 生成する映像ファイルのmimetypeを指定します。これを指定しないとダウンロードしたファイルが再生できません
let options = {
    mimeType : 'video/webm; codecs=vp9'
};

実装する際のポイント(4)

  • MediaRecorderオブジェクトを作成します。引数でRecordingするStreamオブジェクトを渡します
    • 今回のハンズオンのソースコードでは、remoteStreamを渡し、相手の映像音声をRecordingします
    • optionsでmimeTypeを指定します
recorder = new MediaRecorder(remoteStream,options);

実装する際のポイント(4)

  • Recordingに関するイベントを2つセットします
    • ondataavailableはデータが到着したら発火します
      • このイベント契機で配列chunksにデータを格納していきます
    • onstopはstopメソッドが実行されてRecordingが停止したら発火します
      • このイベント契機で配列chunksからデータを取り出しダウンロード可能な形式に変換し、ダウンロードリンクのURLとして設定します
  • UI関連の処理を実施します
    • ダウンロードリンクを表示にする
recorder.ondataavailable = function(evt) {
    console.log("data available: evt.data.type=" + evt.data.type + " size=" + evt.data.size);
    chunks.push(evt.data);
};

recorder.onstop = function(evt) {
    console.log('recorder.onstop(), so playback');
    recorder = null;
    const videoBlob = new Blob(chunks, { type: "video/webm" });
    blobUrl = window.URL.createObjectURL(videoBlob);
    $('#downloadlink').attr("download", 'recorded.webm');
    $('#downloadlink').attr("href", blobUrl);
    $('#downloadlink').show();
};

実装する際のポイント(5)

  • startメソッドでRecordingを開始します
    • 引数に設定した時間間隔(単位:ms)でデータを区切り、ondataavailableイベントを発火させます
  • UI関連の処理を実施します
    • ボタンのテキストをStopに変更
    • ダウンロードリンクを非表示にする
recorder.start(1000);
console.log('start recording');
$('#recording button').text('Stop');
$('#downloadlink').hide();

Recordingで利用するオブジェクトを初期化し、remoteStreamを格納する

  • script.jsを修正して下さい
let remoteStream = null;
let recorder = null;

     // 省略

function setupCallEventHandlers(call){

     // 省略

    call.on('stream', function(stream){
        addVideo(stream);
        remoteStream = stream;
    });

     // 省略

実装する際のポイント(1)

  • 今回のハンズオンのソースコードではグローバルオブジェクトとして入れ物を用意します
let remoteStream = null;
let recorder = null;

実装する際のポイント(2)

  • 相手のStreamオブジェクトを受信したタイミングでremoteStreamに格納します
    • 複数人分の録画は想定していないため、最後に入室した人のStreamオブジェクトが録画されます
call.on('stream', function(stream){
    addVideo(stream);
    remoteStream = stream;
});

Recordingボタンを設置する

  • index.htmlを修正して下さい
<div id="end-call">
   <!-- 省略 -->
</div>
<div id="recording">
    <button>Recording</button>
    <a href="#" id="downloadlink">Download</a>
    <p>※ 1:1の場合にリモートを録画可能</p>
</div>

実装する際のポイント(1)

  • Leaveボタンの下に設置し、Roomに接続している最中のみ表示されるようにする
    • ダウンロードリンクはデファクトでCSSで非表示にしています
<div id="recording">
    <button>Recording</button>
    <a href="#" id="downloadlink">Download</a>
    <p>※ 1:1の場合にリモートを録画可能</p>
</div>

Recordingボタンの表示・非表示を切り替える

  • script.jsを修正して下さい
    function setupMakeCallUI(){
        $('#make-call').show();
        $('#end-call').hide();
        $('#recording').hide();
    }

    function setupEndCallUI() {
        $('#make-call').hide();
        $('#end-call').show();
        $('#recording').show();
    }

実装する際のポイント(1)

  • RecordingボタンはデフォルトはCSSで非表示にしている
  • Roomに入室したらボタンを表示し、退室したら非表示にする
    function setupMakeCallUI(){
        $('#recording').hide();
    }

    function setupEndCallUI() {
        $('#recording').show();
    }

録音録画機能を試してみる

  1. 2つのブラウザタブでアプリを開く
  2. 任意のRoom名(半角英数)を決めて、全アプリでそのRoomに参加する
  3. Recordingボタンをクリックししばらく録音する
  4. Stopボタンクリック後にダウンロードリンクをクリックしダウンロードする
  5. Chromeにドラッグ・アンド・ドロップして再生できることを確認する

これでハンズオンを終わります

  • ハンズオン用ソースコード
  • 開発環境を整える
  • STEP0
  • STEP1
  • STEP2
  • STEP3
  • STEP4(応用編)
  • STEP5(応用編)
  • これでハンズオンを終わります