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の入門の参考となったら嬉しいです。
    最後までお読みいただき、ありがとうございました!

    アジアクエスト株式会社では一緒に働いていただける方を募集しています。
    興味のある方は以下のURLを御覧ください。