1. 概要

2つのブラウザが通信する必要がある場合、通常、通信を調整し、ブラウザ間でメッセージを渡すために、間にサーバーが必要です。 ただし、サーバーを中央に配置すると、ブラウザー間の通信が遅延します。

このチュートリアルでは、 WebRTC について学習します。これは、ブラウザとモバイルアプリケーションがリアルタイムで直接通信できるようにするオープンソースプロジェクトです。次に、 2つのHTMLクライアント間でデータを共有するためのピアツーピア接続を作成する単純なアプリケーションを作成することにより、実際に動作します。

HTML、JavaScript、WebSocketライブラリを使用し、Webブラウザに組み込まれているWebRTCサポートを使用してクライアントを構築します。 そして、通信プロトコルとしてWebSocketを使用して、Spring Bootでシグナリングサーバーを構築します。 最後に、この接続にビデオストリームとオーディオストリームを追加する方法を説明します。

2. WebRTCの基礎と概念

WebRTCを使用しない一般的なシナリオで2つのブラウザーがどのように通信するかを見てみましょう。

2つのブラウザがあり、ブラウザ1ブラウザ2にメッセージを送信する必要があるとします。 ブラウザ1は最初にそれをサーバーに送信します。

 

サーバーはメッセージを受信すると、メッセージを処理し、ブラウザ2 を見つけて、メッセージを送信します。

 

サーバーはメッセージをブラウザ2に送信する前に処理する必要があるため、通信はほぼでリアルタイムに行われます。 もちろん、リアルタイムでatにしたいと思います。

WebRTCは、2つのブラウザー間にダイレクトチャネルを作成することでこの問題を解決し、サーバーの必要性を排除します。

 

その結果、メッセージが送信者から受信者に直接ルーティングされるようになったため、あるブラウザから別のブラウザにメッセージを渡すのにかかる時間が大幅に短縮されました。 また、サーバーから発生する重労働と帯域幅を取り除き、関係するクライアント間で共有できるようにします。

3. WebRTCと組み込み機能のサポート

WebRTCは、Chrome、Firefox、Opera、Microsoft Edgeなどの主要なブラウザ、およびAndroidやiOSなどのプラットフォームでサポートされています。

ソリューションはブラウザにすぐにバンドルされているため、WebRTCではブラウザに外部プラグインをインストールする必要はありません。

さらに、ビデオとオーディオの送信を含む一般的なリアルタイムアプリケーションでは、C ++ライブラリに大きく依存する必要があり、次のような多くの問題を処理する必要があります。

  • パケット損失の隠蔽
  • エコー・キャンセリング
  • 帯域幅の適応性
  • 動的ジッタバッファリング
  • 自動利得制御
  • ノイズリダクションと抑制
  • 画像「クリーニング」

しかし、 WebRTCはこれらすべての懸念を内部で処理し、クライアント間のリアルタイム通信をより簡単にします。

4. ピアツーピア接続

サーバーの既知のアドレスがあり、クライアントが通信するサーバーのアドレスをすでに知っているクライアント/サーバー通信とは異なり、P2P(ピアツーピア)接続では、どのピアも持っていません別のピアへの直接アドレス

ピアツーピア接続を確立するには、クライアントが次のことを行えるようにするためのいくつかの手順が必要です。

  • コミュニケーションに利用できるようにする
  • お互いを識別し、ネットワーク関連の情報を共有する
  • 関連するデータ、モード、およびプロトコルの形式を共有し、合意する
  • データを共有する

WebRTCは、これらのステップを実行するための一連のAPIと方法論を定義します。

クライアントがお互いを発見し、ネットワークの詳細を共有し、データの形式を共有するために、WebRTCはシグナリングと呼ばれるメカニズムを使用します。

5. シグナリング

シグナリングとは、ネットワークの検出、セッションの作成、セッションの管理、およびメディア機能のメタデータの交換に関連するプロセスを指します。

クライアントは通信を開始するために事前にお互いを知る必要があるため、これは不可欠です。

これらすべてを実現するために、 WebRTCはシグナリングの標準を指定せず、開発者の実装に任せます。したがって、これにより、あらゆるテクノロジーとサポートプロトコルを備えたさまざまなデバイスでWebRTCを使用できる柔軟性が得られます。

5.1. シグナリングサーバーの構築

シグナリングサーバーについては、Spring Bootを使用してWebSocketサーバーを構築します。 SpringInitializrから生成された空のSpringBootプロジェクトから始めることができます。

実装にWebSocketを使用するには、pom.xmlに依存関係を追加しましょう。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
    <version>2.4.0</version>
</dependency>

使用する最新バージョンは、 MavenCentralからいつでも見つけることができます。

シグナリングサーバーの実装は単純です。クライアントアプリケーションがWebSocket接続として登録するために使用できるエンドポイントを作成します。

Spring Bootでこれを行うには、 WebSocketConfigurer を拡張し、registerWebSocketHandlersメソッドをオーバーライドする@Configurationクラスを記述します。

@Configuration
@EnableWebSocket
public class WebSocketConfiguration implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new SocketHandler(), "/socket")
          .setAllowedOrigins("*");
    }
}

次のステップで構築するクライアントから登録するURLとして/socketを特定したことに注意してください。 また、SocketHandleraddHandlerメソッドの引数として渡しました。これは、実際には次に作成するメッセージハンドラーです。

5.2. SignalingServerでのメッセージハンドラの作成

次のステップは、複数のクライアントから受信するWebSocketメッセージを処理するためのメッセージハンドラーを作成することです。

これは、異なるクライアント間でメタデータを交換して直接WebRTC接続を確立するために不可欠です

ここでは、簡単にするために、クライアントからメッセージを受信すると、それ自体を除く他のすべてのクライアントにメッセージを送信します。

これを行うには、SpringWebSocketライブラリからextend TextWebSocketHandlerを実行し、handleTextMessageメソッドとafterConnectionEstablishedメソッドの両方をオーバーライドします。

@Component
public class SocketHandler extends TextWebSocketHandler {

    List<WebSocketSession>sessions = new CopyOnWriteArrayList<>();

    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message)
      throws InterruptedException, IOException {
        for (WebSocketSession webSocketSession : sessions) {
            if (webSocketSession.isOpen() && !session.getId().equals(webSocketSession.getId())) {
                webSocketSession.sendMessage(message);
            }
        }
    }

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        sessions.add(session);
    }
}

afterConnectionEstablished メソッドでわかるように、受信したセッションをセッションのリストに追加して、すべてのクライアントを追跡できるようにします。

また、 handleTextMessageに示されているように、いずれかのクライアントからメッセージを受信すると、はリスト内のすべてのクライアントセッションを繰り返し処理し、送信者を除く他のすべてのクライアントにメッセージを送信します。送信者のセッションIDとリスト内のセッション。

6. メタデータの交換

P2P接続では、クライアントは互いに大きく異なる可能性があります。 たとえば、Android上のChromeはMac上のMozillaに接続できます。

したがって、これらのデバイスのメディア機能は大きく異なる可能性があります。 したがって、ピア間のハンドシェイクでは、通信に使用されるメディアタイプとコーデックについて合意することが不可欠です。

このフェーズでは、 WebRTCは、SDP(Session Description Protocol)を使用して、クライアント間のメタデータについて合意します。 

これを実現するために、開始ピアは、他のピアによってリモート記述子として設定する必要があるオファーを作成します。 さらに、もう一方のピアは、開始ピアによってリモート記述子として受け入れられる応答を生成します。

このプロセスが完了すると、接続が確立されます。

7. クライアントの設定

開始ピアとリモートピアの両方として機能できるように、WebRTCクライアントを作成しましょう。

まず、 index.html という名前のHTMLファイルと、index.htmlが使用するclient.jsという名前のJavaScriptファイルを作成します。

シグナリングサーバーに接続するために、WebSocket接続を作成します。 構築したSpringBootシグナリングサーバーがhttp:// localhost:8080 で実行されていると仮定すると、接続を作成できます。

var conn = new WebSocket('ws://localhost:8080/socket');

シグナリングサーバーにメッセージを送信するために、次の手順でメッセージを渡すために使用されるsendメソッドを作成します。

function send(message) {
    conn.send(JSON.stringify(message));
}

8. SimpleRTCDataChannelのセットアップ

client.js でクライアントを設定した後、RTCPeerConnectionクラスのオブジェクトを作成する必要があります。

configuration = null;
var peerConnection = new RTCPeerConnection(configuration);

この例では、構成オブジェクトの目的は、STUN(NATのセッショントラバーサルユーティリティ)サーバーとTURN(NAT周辺のリレーを使用したトラバーサル)サーバー、およびこのチュートリアルの後半で説明するその他の構成を渡すことです。 この例では、nullを渡すだけで十分です。

これで、メッセージパッシングに使用するdataChannelを作成できます。

var dataChannel = peerConnection.createDataChannel("dataChannel", { reliable: true });

その後、データチャネル上のさまざまなイベントのリスナーを作成できます。

dataChannel.onerror = function(error) {
    console.log("Error:", error);
};
dataChannel.onclose = function() {
    console.log("Data channel is closed");
};

9. ICEとの接続の確立

WebRTC接続を確立する次のステップには、ICE(Interactive Connection Establishment)およびSDPプロトコルが含まれます。ここでは、ピアのセッション記述が交換され、両方のピアで受け入れられます。

シグナリングサーバーは、ピア間でこの情報を送信するために使用されます。 これには、クライアントがシグナリングサーバーを介して接続メタデータを交換する一連の手順が含まれます。

9.1. オファーの作成

まず、オファーを作成し、それをpeerConnectionのローカル記述として設定します。 次に、オファーを他のピアに送信します。

peerConnection.createOffer(function(offer) {
    send({
        event : "offer",
        data : offer
    });
    peerConnection.setLocalDescription(offer);
}, function(error) {
    // Handle error here
});

ここで、 send メソッドは、シグナリングサーバーを呼び出してoffer情報を渡します。

サーバー側のテクノロジーを使用して、sendメソッドのロジックを自由に実装できることに注意してください。

9.2. ICE候補の処理

次に、ICE候補を処理する必要があります。  WebRTCは、ICE(Interactive Connection Establishment)プロトコルを使用してピアを検出し、接続を確立します。

peerConnection にローカルの説明を設定すると、icecandidateイベントがトリガーされます。

このイベントは、候補をリモートピアに送信して、リモートピアが候補をリモート候補のセットに追加できるようにする必要があります。

これを行うには、onicecandidateイベントのリスナーを作成します。

peerConnection.onicecandidate = function(event) {
    if (event.candidate) {
        send({
            event : "candidate",
            data : event.candidate
        });
    }
};

icecandidate イベントは、すべての候補が収集されると、空の候補文字列で再度トリガーされます。

この候補オブジェクトもリモートピアに渡す必要があります。 この空の候補文字列を渡して、リモートピアがすべてのicecandidateオブジェクトが収集されたことを確実に認識できるようにします。

また、同じイベントが再度トリガーされ、イベントでcandidateオブジェクトの値がnullに設定された状態でICE候補の収集が完了したことを示します。 これをリモートピアに渡す必要はありません。

9.3. ICE候補の受け取り

第三に、他のピアから送信されたICE候補を処理する必要があります。

リモートピアは、この候補を受信すると、それを候補プールに追加する必要があります。

peerConnection.addIceCandidate(new RTCIceCandidate(candidate));

9.4. オファーを受け取る

その後、他のピアがオファーを受信すると、それをリモート記述として設定する必要があります。さらに、 answer を生成する必要があります。これは、開始ピアに送信されます。

peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
peerConnection.createAnswer(function(answer) {
    peerConnection.setLocalDescription(answer);
        send({
            event : "answer",
            data : answer
        });
}, function(error) {
    // Handle error here
});

9.5. 答えを受け取る

最後に、開始ピアは回答を受け取り、それをリモート記述として設定します。

handleAnswer(answer){
    peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
}

これにより、WebRTCは正常な接続を確立します。

これで、シグナリングサーバーなしで、2つのピア間で直接データを送受信できます。

10. メッセージの送信

接続が確立されたので、 dataChannelのsendメソッドを使用してピア間でメッセージを送信できます。

dataChannel.send(“message”);

同様に、他のピアでメッセージを受信するために、onmessageイベントのリスナーを作成しましょう。

dataChannel.onmessage = function(event) {
    console.log("Message:", event.data);
};

データチャネルでメッセージを受信するには、peerConnectionオブジェクトにコールバックを追加する必要もあります。

peerConnection.ondatachannel = function (event) {
    dataChannel = event.channel;
};

このステップで、完全に機能するWebRTCデータチャネルを作成しました。 これで、クライアント間でデータを送受信できるようになりました。 さらに、これにビデオおよびオーディオチャネルを追加できます。

11. ビデオおよびオーディオチャネルの追加

WebRTCがP2P接続を確立すると、オーディオおよびビデオストリームを直接簡単に転送できます。

11.1. メディアストリームの取得

まず、ブラウザからメディアストリームを取得する必要があります。 WebRTCは、このためのAPIを提供します。

const constraints = {
    video: true,audio : true
};
navigator.mediaDevices.getUserMedia(constraints).
  then(function(stream) { /* use the stream */ })
    .catch(function(err) { /* handle the error */ });

制約オブジェクトを使用して、ビデオのフレームレート、幅、および高さを指定できます。

制約オブジェクトでは、モバイルデバイスの場合に使用するカメラを指定することもできます。

var constraints = {
    video : {
        frameRate : {
            ideal : 10,
            max : 15
        },
        width : 1280,
        height : 720,
        facingMode : "user"
    }
};

また、バックカメラを有効にする場合は、 FacingMode の値を「user」ではなく、「environment」に設定できます。

11.2. ストリームの送信

次に、WebRTCピア接続オブジェクトにストリームを追加する必要があります。

peerConnection.addStream(stream);

ピア接続にストリームを追加すると、接続されているピアでaddstreamイベントがトリガーされます。

11.3. ストリームの受信

第三に、リモートピアでストリームを受信するために、リスナーを作成できます。

このストリームをHTMLビデオ要素に設定しましょう。

peerConnection.onaddstream = function(event) {
    videoElement.srcObject = event.stream;
};

12. NATの問題

現実の世界では、ファイアウォールとNAT(ネットワークアドレストラバーサル)デバイスがデバイスをパブリックインターネットに接続します。

NATは、ローカルネットワーク内で使用するためのIPアドレスをデバイスに提供します。 したがって、このアドレスはローカルネットワークの外部からはアクセスできません。 パブリックアドレスがないと、ピアは私たちと通信できません。

この問題に対処するために、WebRTCは2つのメカニズムを使用します。

  1. 気絶
  2. 順番

13. 使用する 気絶

STUNは、この問題に対する最も簡単なアプローチです。 ネットワーク情報をピアと共有する前に、クライアントはSTUNサーバーに要求を行います。 STUNサーバーの責任は、リクエストの受信元のIPアドレスを返すことです。

したがって、STUNサーバーにクエリを実行することで、独自の公開IPアドレスを取得します。 次に、このIPとポートの情報を接続するピアと共有します。 他のピアも同じことを行って、公開されているIPを共有できます。

STUNサーバーを使用するには、RTCPeerConnectionオブジェクトを作成するための構成オブジェクトにURLを渡すだけです。

var configuration = {
    "iceServers" : [ {
        "url" : "stun:stun2.1.google.com:19302"
    } ]
};

14. 使用する 順番

対照的に、TURNは、WebRTCがP2P接続を確立できない場合に使用されるフォールバックメカニズムです。 TURNサーバーの役割は、ピア間でデータを直接中継することです。この場合、データの実際のストリームはTURNサーバーを通過します。 デフォルトの実装を使用すると、TURNサーバーはSTUNサーバーとしても機能します。

TURNサーバーは公開されており、クライアントはファイアウォールやプロキシの背後にある場合でもサーバーにアクセスできます。

ただし、中間サーバーが存在するため、TURNサーバーの使用は実際にはP2P接続ではありません。

注:TURNは、P2P接続を確立できない場合の最後の手段です。データはTURNサーバーを通過するため、多くの帯域幅が必要であり、この場合はP2Pを使用していません。

STUNと同様に、同じ構成オブジェクトでTURNサーバーのURLを提供できます。

{
  'iceServers': [
    {
      'urls': 'stun:stun.l.google.com:19302'
    },
    {
      'urls': 'turn:10.158.29.39:3478?transport=udp',
      'credential': 'XXXXXXXXXXXXX',
      'username': 'XXXXXXXXXXXXXXX'
    },
    {
      'urls': 'turn:10.158.29.39:3478?transport=tcp',
      'credential': 'XXXXXXXXXXXXX',
      'username': 'XXXXXXXXXXXXXXX'
    }
  ]
}

15. 結論

このチュートリアルでは、WebRTCプロジェクトとは何かについて説明し、その基本的な概念を紹介しました。 次に、2つのHTMLクライアント間でデータを共有するための簡単なアプリケーションを構築しました。

また、WebRTC接続の作成と確立に関連する手順についても説明しました。

さらに、WebRTCが失敗した場合のフォールバックメカニズムとして、STUNサーバーとTURNサーバーの使用を検討しました。

この記事で提供されている例をGitHubで確認できます。