WebRTC入門-ビデオ通話機能の実現(ハンズオン)

    WebRTC入門-ビデオ通話機能の実現(ハンズオン)

    本記事はAsiaQuest Advent Calendarの20日目です。

    目次

      記事の目的(または狙い)

      WebRTCを用いたビデオ通話機能の実装を通じて、通信確立のプロセスを理解することを目的とします。

      はじめに

      こんにちは!Web3課のコウです。
      最近の技術勉強で、WebRTCについて勉強していました。
      今回の記事は、WebRTCの基礎概念や知識を紹介せず、直でハンズオンを行い、WebRTCの通信を実践することを考えています。 WebRTCの通信プロセスのイメージを持った上で、改めてわからなかった単語などを調べてもらえれば効率的にWebRTCについて学習できると思います!
      ※記事中に出たSDPやICE Candidateなどの説明について、下記の記事をお勧めします。
       手動でWebRTCの通信をつなげよう ーWebRTC入門2016 
      ※ コードはReact JSXで書いています。

      実装

      1.PCのカメラとマイクをオンにします

      function Main() {
      const localVideoRef = useRef(null)
      const localStreamRef = useRef(null)

      const getMediaDevices = () => {
      // video:true, audio:true を指定すると、カメラとマイクをオンにします
      navigator.mediaDevices.getUserMedia({ video: true, audio: true })
      .then(stream => {
      console.log('stream', stream)
      localVideoRef.current.srcObject = stream // カメラ映像を画面で表示します。
      localStreamRef.current = stream
      })
      }

      return (
      <div>
      <button onClick={getMediaDevices}>カメラとマイクをオンにする</button><br />
      <video ref={localVideoRef} style={{ width: '400px'}} autoPlay></video>
      </div>
      )
      }

      export default Main

      2.RTCコネクションを作成します

      function Main() {
      const localVideoRef = useRef(null)
      const localStreamRef = useRef(null)
      /* 追加 */
      const pc = useRef(null)

      const getMediaDevices = () => {...}
      /* 追加 */
      const createRtcConnection = () => {
      const _pc = new RTCPeerConnection({
      // 二つのクライアント直接通信できない場合、STUNサーバーを介して中継通信
      iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
      })
      pc.current = _pc
      console.log('RTCコネクションを作成しました');
      }

      return (
      <div>
      <button onClick={getMediaDevices}>カメラとマイクをオンにする</button><br />
      <video ref={localVideoRef} style={{ width: '400px'}} autoPlay></video>
      /* 追加 */
      <button onClick={createRtcConnection}>RTCコネクションを作成する</button><br />
      </div>
      )
      }

      export default Main

      3.ローカルストリームをRTCコネクションに追加します

      function Main() {
      const localVideoRef = useRef(null)
      const localStreamRef = useRef(null)
      const pc = useRef(null)

      const getMediaDevices = () => {...}

      const createRtcConnection = () => {...}
      /* 追加 */
      const addLocalStreamToRtcConnection = () => {
      const localStream = localStreamRef.current
      localStream.getTracks().forEach(track => {
      pc.current?.addTrack(track, localStream)
      })
      console.log('ローカルストリームをRTCコネクションに追加しました');
      }

      return (
      <div>
      <button onClick={getMediaDevices}>カメラとマイクをオンにする</button><br />
      <video ref={localVideoRef} style={{ width: '400px'}} autoPlay></video>
      <button onClick={createRtcConnection}>RTCコネクションを作成する</button><br />
      /* 追加 */
      <button onClick={addLocalStreamToRtcConnection}>ローカルストリームをRTCコネクションに追加</button><br />
      </div>
      )
      }

      export default Main

      4.OfferとAnswerを作成します

      • OfferとAnswerは通信間のクライアント各自のSDPです。
      • お互い通信を確立するために、SDPを交換することが必要です。
      function Main() {
      const localVideoRef = useRef(null)
      const localStreamRef = useRef(null)
      const pc = useRef(null)
      /* 追加 */
      const textRef = useRef(null)

      const getMediaDevices = () => {...}

      const createRtcConnection = () => {...}

      const addLocalStreamToRtcConnection = () => {...}
      /* 追加 */
      const createOffer = () => {
      pc.current?.createOffer({
      offerToReceiveAudio: true,
      offerToReceiveVideo: true
      })
      .then(sdp => {
      console.log('offer', JSON.stringify(sdp));
      // 作成したSDPをローカルデスクリプションにセット
      pc.current?.setLocalDescription(sdp)
      })
      }

      /* 追加 */
      const createAnswer = () => {
      pc.current?.createAnswer({
      offerToReceiveAudio: true,
      offerToReceiveVideo: true
      })
      .then(sdp => {
      console.log('answer', JSON.stringify(sdp));
      pc.current?.setLocalDescription(sdp)
      })
      }

      return (
      <div>
      <button onClick={getMediaDevices}>カメラとマイクをオンにする</button><br />
      <video ref={localVideoRef} style={{ width: '400px'}} autoPlay></video>
      <button onClick={createRtcConnection}>RTCコネクションを作成する</button><br />
      <button onClick={addLocalStreamToRtcConnection}>ローカルストリームをRTCコネクションに追加</button><br />
      /* 追加 */
      <button onClick={createOffer}>Offerを作成する</button>
      <button onClick={createAnswer}>Answerを作成する</button><br />
      <textarea ref={textRef}></textarea><br />
      </div>
      )
      }

      export default Main

      5.交換したSDPをクライアント各自のリモートデスクリプションとしてセットします

      • 例えば、ユーザーAとユーザーBが通話を行います。
      • ユーザーAがSDP(Offer)を作成し、ユーザーBのリモートデスクリプションにセットします。
      • ユーザーBがSDP(Ansewer)を作成し、ユーザーAのリモートデスクリプションにセットします。
        要注意 ユーザーBのリモートデスクリプション(ユーザーAのOffer)は先にセットしないと、ユーザーBがSDP(Answer)作成できないです。
      function Main() {
      const localVideoRef = useRef(null)
      const localStreamRef = useRef(null)
      const pc = useRef(null)
      const textRef = useRef(null)

      const getMediaDevices = () => {...}

      const createRtcConnection = () => {...}

      const addLocalStreamToRtcConnection = () => {...}

      const createOffer = () => {...}

      const createAnswer = () => {...}

      /* 追加 */
      const setRemoteDescription = () => {
      const remoteSdp = JSON.parse(textRef.current.value)
      pc.current?.setRemoteDescription(new RTCSessionDescription(remoteSdp))
      console.log('リモートデスクリプション', remoteSdp);
      }

      return (
      <div>
      <button onClick={getMediaDevices}>カメラとマイクをオンにする</button><br />
      <video ref={localVideoRef} style={{ width: '400px'}} autoPlay></video>
      <button onClick={createRtcConnection}>RTCコネクションを作成する</button><br />
      <button onClick={addLocalStreamToRtcConnection}>ローカルストリームをRTCコネクションに追加</button><br />
      <button onClick={createOffer}>Offerを作成する</button>
      <button onClick={createAnswer}>Answerを作成する</button><br />
      <textarea ref={textRef}></textarea><br />
      /* 追加 */
      <button onClick={setRemoteDescription}>リモートデスクリプションを設定する</button><br />
      </div>
      )
      }

      export default Main

      6.ICE Candidateを交換し、RTCコネクションに追加します

      function Main() {
      const localVideoRef = useRef(null)
      const localStreamRef = useRef(null)
      const pc = useRef(null)
      const textRef = useRef(null)

      const getMediaDevices = () => {...}

      const createRtcConnection = () => {
      const _pc = new RTCPeerConnection({
      // 二つのクライアント直接通信できない場合、STUNサーバーを介して中継通信
      iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
      })
      /* 追加 */
      _pc.onicecandidate = event => {
      if (event.candidate) {
      console.log('candidate', JSON.stringify(event.candidate));
      }
      }
      pc.current = _pc
      console.log('RTCコネクションを作成しました');
      }

      const addLocalStreamToRtcConnection = () => {...}

      const createOffer = () => {...}

      const createAnswer = () => {...}

      const setRemoteDescription = () => {...}
      /* 追加 */
      const addCandidate = () => {
      const candidate = JSON.parse(textRef.current.value)
      pc.current?.addIceCandidate(new RTCIceCandidate(candidate))
      console.log('candiate', candidate);
      }

      return (
      <div>
      <button onClick={getMediaDevices}>カメラとマイクをオンにする</button><br />
      <video ref={localVideoRef} style={{ width: '400px'}} autoPlay></video>
      <button onClick={createRtcConnection}>RTCコネクションを作成する</button><br />
      <button onClick={addLocalStreamToRtcConnection}>ローカルストリームをRTCコネクションに追加</button><br />
      <button onClick={createOffer}>Offerを作成する</button>
      <button onClick={createAnswer}>Answerを作成する</button><br />
      <textarea ref={textRef}></textarea><br />
      <button onClick={setRemoteDescription}>リモートデスクリプションを設定する</button>
      /* 追加 */
      <button onClick={addCandidate}>AddCandidate</button><br />
      </div>
      )
      }

      export default Main

      7.RTCコネクションにあるローカルストリームを画面に表示します

      • 通信相手の画面を表示します
      function Main() {
      const localVideoRef = useRef(null)
      const localStreamRef = useRef(null)
      const pc = useRef(null)
      const textRef = useRef(null)
      /* 追加 */
      const remoteVideoRef = useRef(null)

      const getMediaDevices = () => {...}

      const createRtcConnection = () => {
      const _pc = new RTCPeerConnection({...})
      _pc.onicecandidate = event => {...}
      /* 追加 */
      _pc.ontrack = event => {
      remoteVideoRef.current.srcObject = event.streams[0]
      }
      pc.current = _pc
      console.log('RTCコネクションを作成しました');
      }

      const addLocalStreamToRtcConnection = () => {...}

      const createOffer = () => {...}

      const createAnswer = () => {...}

      const setRemoteDescription = () => {...}

      const addCandidate = () => {...}

      return (
      <div>
      <button onClick={getMediaDevices}>カメラとマイクをオンにする</button><br />
      <video ref={localVideoRef} style={{ width: '400px'}} autoPlay></video>
      /* 追加 */
      <video ref={remoteVideoRef} style={{ width: '400px'}} autoPlay></video><br />
      <button onClick={createRtcConnection}>RTCコネクションを作成する</button><br />
      <button onClick={addLocalStreamToRtcConnection}>ローカルストリームをRTCコネクションに追加</button><br />
      <button onClick={createOffer}>Offerを作成する</button>
      <button onClick={createAnswer}>Answerを作成する</button><br />
      <textarea ref={textRef}></textarea><br />
      <button onClick={setRemoteDescription}>リモートデスクリプションを設定する</button>
      <button onClick={addCandidate}>AddCandidate</button><br />
      </div>
      )
      }

      export default Main

      画面操作

      ブラウザで二つのタブを開き、ユーザーAとユーザーBのビデオ通話を行います

      ユーザーA

      webrtc_video_handson_01

      ユーザーB

      webrtc_video_handson_02

      ユーザーA

      webrtc_video_handson_03

      ユーザーB

      webrtc_video_handson_04

      通信できたイメージ

      webrtc_video_handson_05

      最後

      今回はWebRTCの通信確立プロセスを理解するために、SDPとICE Candidateの交換は手動で行いましたが、より実践的な方法としてはWebSocketを使ってシグナリングサーバーを作って、遠く離れたユーザーとも通信の確立ができます。

      以上、多少WebRTCの入門の参考となったら嬉しいです。
      最後までお読みいただき、ありがとうございました!