觀感度:?????
口味:新疆炒米粉
烹飪時間:10min
本文已收錄在前端食堂同名倉庫Github github.com/Geekhyt,歡迎光臨食堂,如果覺得酒菜還算可口,賞個 Star 對食堂老闆來說是莫大的鼓勵。
通過上兩個系列專欄的學習,我們對前端音視訊及 WebRTC 有了初步的瞭解,是時候敲程式碼實現一個 Demo 來真實感受下 WebRTC 實時通訊的魅力了。還沒有看過的同學請移步:
RTCPeerConnection
RTCPeerConnection
類是在瀏覽器下使用 WebRTC 實現實時互動音視訊系統中最核心的類,它代表一個由本地計算機到遠端的 WebRTC 連線。該介面提供了建立、保持、監控及關閉連線的方法的實現。
想要對這個類瞭解更多可以移步這個連結, https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection
其實,如果你有做過 socket 開發的話,你會更容易理解 RTCPeerConnection
,它其實就是一個加強版本的 socket。
在上個系列專欄 前端音視訊之WebRTC初探 中,我們瞭解了 WebRTC 的通訊原理,在真實場景下需要進行媒體協商、網路協商、架設信令伺服器等操作,我畫了一張圖,將 WebRTC 的通訊過程總結如下:
不過今天我們為了單純的搞清楚 RTCPeerConnection
,先不考慮開發架設信令伺服器的問題,簡單點,我們這次嘗試在同一個頁面中模擬兩端進行音視訊的互通。
在此之前,我們先了解一些將要用到的 API 以及 WebRTC 建立連線的步驟。
相關 API
RTCPeerConnection
介面代表一個由本地計算機到遠端的 WebRTC 連線。該介面提供了建立、保持、監控、關閉連線的方法的實現。PC.createOffer
建立提議 Offer 方法,此方法會返回 SDP Offer 資訊。PC.setLocalDescription
設定本地 SDP 描述資訊。PC.setRemoteDescription
設定遠端 SDP 描述資訊,即對方發過來的 SDP 資料。PC.createAnswer
建立應答 Answer 方法,此方法會返回 SDP Answer 資訊。RTCIceCandidate
WebRTC 網路資訊(IP、埠等)PC.addIceCandidate
PC 連線新增對方的 IceCandidate 資訊,即新增對方的網路資訊。
WebRTC 建立連線步驟
- 1.為連線的兩端建立一個 RTCPeerConnection 物件,並且給 RTCPeerConnection 物件新增本地流。
- 2.獲取本地媒體描述資訊(SDP),並與對端進行交換。
- 3.獲取網路資訊(Candidate,IP 地址和埠),並與遠端進行交換。
Demo 實戰
首先,我們新增視訊元素及控制按鈕,引入 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 = mediaStream;
console.log('remote 開始接受遠端流')
}
}
最後,還需要註冊一些 Log 函式及工具函式。
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 連線,效果如下:
(隨手抓起桌邊的鼠年企鵝公仔)
參考
- 《從 0 打造音視訊直播系統》 李超
- 《WebRTC 音視訊開發 React+Flutter+Go 實戰》 亢少軍
- https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection
❤️愛心三連擊
1.如果你覺得食堂酒菜還合胃口,就點個贊支援下吧,你的贊是我最大的動力。
2.關注公眾號前端食堂,吃好每一頓飯!
3.點贊、評論、轉發 === 催更!