AQ Tech Blog

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

作成者: ko.shuko|2023年12月22日

本記事は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

ユーザーB

ユーザーA

ユーザーB

通信できたイメージ

最後

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

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