RTCPeerConnection
RTCPeerConnectionクラスは、ブラウザーでWebRTCを使用するリアルタイムインタラクティブオーディオおよびビデオシステムのコアクラスであり、ローカルコンピューターからリモートエンドへのWebRTC接続を表します。このインターフェースは、接続を作成、維持、監視、および閉じるためのメソッドの実装を提供します。
RTCPeerConnection
実際、ソケット開発を行った場合、それが実際にはソケットの拡張バージョンであることを理解しやすくなります。
WebRTCの通信原理には、実際のシナリオでのメディアネゴシエーション、ネットワークネゴシエーション、およびシグナリングサーバーのセットアップが必要です。WebRTCの通信プロセスを要約するために、次のように図を描きました。
ただし、今日は 、わかりやすくするためRTCPeerConnection
に、シグナリングサーバーの開発とセットアップの問題については考慮しません。簡単にするために、同じページで両端間のオーディオとビデオの通信をシミュレートします。
その前に、使用されるAPIのいくつかと、WebRTCとの接続を確立するための手順を理解しましょう。
関連するAPI
-
RTCPeerConnection
インターフェイスは、ローカルコンピューターからリモートへのWebRTC接続を表します。このインターフェースは、接続を作成、維持、監視、および閉じるためのメソッドの実装を提供します。 -
PC.createOffer
SDPオファー情報を返すOfferメソッドを作成します。 -
PC.setLocalDescription
ローカルSDP記述情報を設定します。 -
PC.setRemoteDescription
リモートSDP記述情報、つまり相手から送信されたSDPデータを設定します。 -
PC.createAnswer
SDP回答情報を返すAnswerAnswerメソッドを作成します。 -
RTCIceCandidate
WebRTCネットワーク情報(IP、ポートなど) -
PC.addIceCandidate
相手のIceCandidate情報をPC接続に追加します。つまり、相手のネットワーク情報を追加します。
WebRTC接続手順
-
1.接続の両端にRTCPeerConnectionオブジェクトを作成し、ローカルストリームをRTCPeerConnectionオブジェクトに追加します。
-
2.ローカルメディア記述情報(SDP)を取得し、ピアと交換します。
-
3.ネットワーク情報(候補、IPアドレス、ポート)を取得し、リモートエンドと交換します。
-
( C / C ++ 、Linux 、FFmpeg 、webRTC 、rtmp 、hls 、rtsp 、ffplay 、srs )を含む最新かつ最も完全なC++オーディオおよびビデオの学習および改善資料を受け取るように私にプライベートメッセージを送ります
実際の戦闘のデモ
adpater.js
まず、各ブラウザに適応するために導入されたビデオ要素とコントロールボタンを追加します 。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo</title>
<style>
video {
width: 320px;
}
</style>
</head>
<body>
<video id="localVideo" autoplay playsinline></video>
<video id="remoteVideo" autoplay playsinline></video>
<div>
<button id="startBtn">打开本地视频</button>
<button id="callBtn">建立连接</button>
<button id="hangupBtn">断开连接</button>
</div>
<!-- 适配各浏览器 API 不统一的脚本 -->
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="./webrtc.js"></script>
</body>
</html>
次に、使用するオブジェクトを定義します。
// 本地流和远端流
let localStream;
let remoteStream;
// 本地和远端连接对象
let localPeerConnection;
let remotePeerConnection;
// 本地视频和远端视频
const localVideo = document.getElementById('localVideo');
const remoteVideo = document.getElementById('remoteVideo');
// 设置约束
const mediaStreamConstraints = {
video: true
}
// 设置仅交换视频
const offerOptions = {
offerToReceiveVideo: 1
}
次に、ボタンのイベントを登録し、関連するビジネスロジックを実装します。
function startHandle() {
startBtn.disabled = true;
// 1.获取本地音视频流
// 调用 getUserMedia API 获取音视频流
navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
.then(gotLocalMediaStream)
.catch((err) => {
console.log('getUserMedia 错误', err);
});
}
function callHandle() {
callBtn.disabled = true;
hangupBtn.disabled = false;
// 视频轨道
const videoTracks = localStream.getVideoTracks();
// 音频轨道
const audioTracks = localStream.getAudioTracks();
// 判断视频轨道是否有值
if (videoTracks.length > 0) {
console.log(`使用的设备为: ${videoTracks[0].label}.`);
}
// 判断音频轨道是否有值
if (audioTracks.length > 0) {
console.log(`使用的设备为: ${audioTracks[0].label}.`);
}
const servers = null;
// 创建 RTCPeerConnection 对象
localPeerConnection = new RTCPeerConnection(servers);
// 监听返回的 Candidate
localPeerConnection.addEventListener('icecandidate', handleConnection);
// 监听 ICE 状态变化
localPeerConnection.addEventListener('iceconnectionstatechange', handleConnectionChange)
remotePeerConnection = new RTCPeerConnection(servers);
remotePeerConnection.addEventListener('icecandidate', handleConnection);
remotePeerConnection.addEventListener('iceconnectionstatechange', handleConnectionChange);
remotePeerConnection.addEventListener('track', gotRemoteMediaStream);
// 将音视频流添加到 RTCPeerConnection 对象中
// 注意:新的协议中已经不再推荐使用 addStream 方法来添加媒体流,应使用 addTrack 方法
// localPeerConnection.addStream(localStream);
// 遍历本地流的所有轨道
localStream.getTracks().forEach((track) => {
localPeerConnection.addTrack(track, localStream)
})
// 2.交换媒体描述信息
localPeerConnection.createOffer(offerOptions)
.then(createdOffer).catch((err) => {
console.log('createdOffer 错误', err);
});
}
function hangupHandle() {
// 关闭连接并设置为空
localPeerConnection.close();
remotePeerConnection.close();
localPeerConnection = null;
remotePeerConnection = null;
hangupBtn.disabled = true;
callBtn.disabled = false;
}
// getUserMedia 获得流后,将音视频流展示并保存到 localStream
function gotLocalMediaStream(mediaStream) {
localVideo.srcObject = mediaStream;
localStream = mediaStream;
callBtn.disabled = false;
}
function createdOffer(description) {
console.log(`本地创建offer返回的sdp:\n${description.sdp}`)
// 本地设置描述并将它发送给远端
// 将 offer 保存到本地
localPeerConnection.setLocalDescription(description)
.then(() => {
console.log('local 设置本地描述信息成功');
}).catch((err) => {
console.log('local 设置本地描述信息错误', err)
});
// 远端将本地给它的描述设置为远端描述
// 远端将 offer 保存
remotePeerConnection.setRemoteDescription(description)
.then(() => {
console.log('remote 设置远端描述信息成功');
}).catch((err) => {
console.log('remote 设置远端描述信息错误', err);
});
// 远端创建应答 answer
remotePeerConnection.createAnswer()
.then(createdAnswer)
.catch((err) => {
console.log('远端创建应答 answer 错误', err);
});
}
function createdAnswer(description) {
console.log(`远端应答Answer的sdp:\n${description.sdp}`)
// 远端设置本地描述并将它发给本地
// 远端保存 answer
remotePeerConnection.setLocalDescription(description)
.then(() => {
console.log('remote 设置本地描述信息成功');
}).catch((err) => {
console.log('remote 设置本地描述信息错误', err);
});
// 本地将远端的应答描述设置为远端描述
// 本地保存 answer
localPeerConnection.setRemoteDescription(description)
.then(() => {
console.log('local 设置远端描述信息成功');
}).catch((err) => {
console.log('local 设置远端描述信息错误', err);
});
}
// 3.端与端建立连接
function handleConnection(event) {
// 获取到触发 icecandidate 事件的 RTCPeerConnection 对象
// 获取到具体的Candidate
const peerConnection = event.target;
const iceCandidate = event.candidate;
if (iceCandidate) {
// 创建 RTCIceCandidate 对象
const newIceCandidate = new RTCIceCandidate(iceCandidate);
// 得到对端的 RTCPeerConnection
const otherPeer = getOtherPeer(peerConnection);
// 将本地获得的 Candidate 添加到远端的 RTCPeerConnection 对象中
// 为了简单,这里并没有通过信令服务器来发送 Candidate,直接通过 addIceCandidate 来达到互换 Candidate 信息的目的
otherPeer.addIceCandidate(newIceCandidate)
.then(() => {
handleConnectionSuccess(peerConnection);
}).catch((error) => {
handleConnectionFailure(peerConnection, error);
});
}
}
// 4.显示远端媒体流
function gotRemoteMediaStream(event) {
if (remoteVideo.srcObject !== event.streams[0]) {
remoteVideo.srcObject = event.streams[0];
remoteStream = event.streams[0];
console.log('remote 开始接受远端流')
}
}
最後に、いくつかのログ機能とユーティリティ機能を登録する必要があります。
function handleConnectionChange(event) {
const peerConnection = event.target;
console.log('ICE state change event: ', event);
console.log(`${getPeerName(peerConnection)} ICE state: ` + `${peerConnection.iceConnectionState}.`);
}
function handleConnectionSuccess(peerConnection) {
console.log(`${getPeerName(peerConnection)} addIceCandidate 成功`);
}
function handleConnectionFailure(peerConnection, error) {
console.log(`${getPeerName(peerConnection)} addIceCandidate 错误:\n`+ `${error.toString()}.`);
}
function getPeerName(peerConnection) {
return (peerConnection === localPeerConnection) ? 'localPeerConnection' : 'remotePeerConnection';
}
function getOtherPeer(peerConnection) {
return (peerConnection === localPeerConnection) ? remotePeerConnection : localPeerConnection;
}
実際、プロセス全体に精通している場合は、すべてのLog関数を均一に抽出してカプセル化できます。コードを読み取るプロセス中にWebRTCとの接続を確立するプロセス全体を理解しやすくするために、抽出は実行されません。
ここですべてがうまくいけば、WebRTC接続が正常に確立され、効果は次のようになります。
(テーブルでネズミの年のペンギン人形をつかみます)