WebRTC 雙向視頻通話
一、項目概述
WebRTC(Web Real - Time Communication)是一種支持瀏覽器之間進行實時通信的技術,它使得在網頁上實現音視頻通話、文件共享等功能變得更加容易。為了體驗這個技術,所以我實現了webrtc - local
二、項目結構
項目主要分為兩個主要部分:webrtc - server
(服務端)和 webrtc - client
(客戶端)。
1. 服務端(webrtc - server)
服務端使用 Node.js 搭建,借助 socket.io
(也可以是其他)實現信令的一個交換,其實就是一個信息中轉的地方。
2. 客戶端(webrtc - client)
客戶端基于 Vue 3 和 TypeScript 構建,使用 socket.io - client
與服務端進行通信。
三、項目啟動步驟
詳看 README
四、代碼實現分析
1. 服務端(index.js)
服務端使用 https
協議創建服務器,并使用 socket.io
處理實時通信。以下是主要邏輯:
const socket = require('socket.io')
const https = require('https')
const fs = require('fs')
const path = require('path')const server = https.createServer({key: fs.readFileSync(path.join(__dirname, '../cert/key.pem')),cert: fs.readFileSync(path.join(__dirname, '../cert/cert.pem')),
})const io = socket(server, {cors: {origin: '*', // 配置跨域},
})io.on('connection', (sock) => {console.log('連接成功...')sock.emit('connectionSuccess')// 監聽客戶端進入房間的事件sock.on('joinRoom', (roomId) => {sock.join(roomId)})// 處理各種視頻通話相關事件sock.on('callRemote', (roomId) => {io.to(roomId).emit('receiveCall')})sock.on('acceptCall', (roomId) => {io.to(roomId).emit('acceptCall')})// 處理 offer、answer 和 candidate 信息sock.on('sendOffer', ({ roomId, offer }) => {io.to(roomId).emit('sendOffer', offer)})sock.on('sendAnswer', ({ roomId, answer }) => {io.to(roomId).emit('receiveAnswer', answer)})sock.on('sendCandidate', ({ roomId, candidate }) => {io.to(roomId).emit('receiveCandidate', candidate)})sock.on('hangUp', (roomId) => {io.to(roomId).emit('hangUp')})
})server.listen(3001, () => {console.log('服務器啟動成功')
})
服務端主要負責監聽客戶端的連接和各種事件,用來交換不同客戶端的信令等數據。
2. 客戶端(App.vue)
客戶端使用 Vue 3,結合 socket.io - client
和 RTCPeerConnection
實現視頻通話。以下是主要邏輯及相關知識解釋:
4.2.1 獲取本地音視頻流
// 獲取本地音視頻流
const getLocalStream = async () => {// 獲取音視頻流const stream = await navigator.mediaDevices.getUserMedia({video: true,audio: true,})// 將媒體流設置到 video 標簽上播放localVideo.value!.srcObject = stream// 播放音視頻流localVideo.value!.play()// 存儲本地流localStream.value = streamreturn stream
}
知識解釋:
navigator.mediaDevices.getUserMedia
是 WebRTC 提供的一個 API,用于請求訪問用戶的攝像頭和麥克風。它接受一個約束對象作為參數,該對象指定了需要獲取的媒體類型(如視頻、音頻)以及其他可選的配置。當用戶允許訪問后,該方法會返回一個 Promise
,該 Promise
會解析為一個 MediaStream
對象,該對象包含了用戶的音視頻流。
4.2.2 處理視頻請求
// 發起視頻請求(發起方)
const callRemote = async () => {if (calling.value || communicating.value) {return}calling.value = true// 獲取本地音視頻流await getLocalStream()// 向服務器發送發起視頻請求的事件caller.value = truesocket.value.emit('callRemote', roomId)
}// 接收視頻請求(接受方)
const acceptCall = () => {// 向服務器發送接受視頻請求的事件socket.value.emit('acceptCall', roomId)
}
知識解釋:
在 WebRTC 視頻通話中,發起方首先需要獲取本地音視頻流,然后通過 socket.io
向服務端發送視頻請求。服務端接收到請求后,將其廣播給房間內的其他客戶端。接收方接收到請求后,可以選擇接受或拒絕。如果接受,接收方會向服務端發送接受請求的事件,服務端再將該事件廣播給發起方。
4.2.3 交換 offer/answer
// 發送方收到同意視頻事件
sock.on('acceptCall', async () => {if (caller.value) {// 發送方// 創建RTCPeerConnection對象peer.value = new RTCPeerConnection()// 添加本地音視頻流peer.value.addStream(localStream.value)// 生成offerconst offer = await peer.value.createOffer({offerToReceiveAudio: true,offerToReceiveVideo: true,})// 設置本地描述的offerawait peer.value.setLocalDescription(offer)// 發送offersock.emit('sendOffer', { roomId, offer })}
})// 接收方收到offer
sock.on('sendOffer', async (offer: any) => {if (called.value) {const stream = await getLocalStream()// 接收方創建自己的RTCPeerConnection對象peer.value = new RTCPeerConnection()// 添加本地音視頻流peer.value.addStream(stream)// 設置遠端描述信息await peer.value.setRemoteDescription(offer)// 生成answerconst answer = await peer.value.createAnswer()// 設置本地描述信息await peer.value.setLocalDescription(answer)// 發送answersock.emit('sendAnswer', { roomId, answer })}
})// 發送方收到接收方的answer
sock.on('receiveAnswer', (answer: any) => {if (caller.value) {// 設置遠端描述信息peer.value.setRemoteDescription(answer)}
})
知識解釋:
- offer:發起方通過
RTCPeerConnection.createOffer
方法創建一個offer
,該offer
包含了發起方的會話描述信息,如支持的編解碼器、媒體類型等。然后使用RTCPeerConnection.setLocalDescription
方法將該offer
設置為本地描述,并通過socket.io
發送給接收方。 - answer:接收方收到
offer
后,使用RTCPeerConnection.setRemoteDescription
方法設置遠端描述,然后通過RTCPeerConnection.createAnswer
方法創建一個answer
,該answer
包含了接收方的會話描述信息。同樣,使用RTCPeerConnection.setLocalDescription
方法將該answer
設置為本地描述,并通過socket.io
發送給發起方。 - 會話描述協議(SDP):
offer
和answer
都是基于會話描述協議(SDP)的,SDP 是一種用于描述多媒體會話的格式,它包含了會話的各種信息,如媒體類型、編解碼器、傳輸地址等。通過交換offer
和answer
,雙方可以協商出一個共同支持的會話配置。
4.2.4 交換 candidate 信息
// 獲取candidate信息
peer.value.onicecandidate = (event: any) => {if (event.candidate) {// 向服務器發送candidate信息sock.emit('sendCandidate', { roomId, candidate: event.candidate })}
}// 接收candidate信息
sock.on('receiveCandidate', async (candidate: any) => {await peer.value.addIceCandidate(candidate)
})
知識解釋:
- ICE(交互式連接建立):由于雙方可能位于不同的網絡環境中,需要通過 ICE 機制來找到雙方之間的最佳通信路徑。ICE 會收集雙方的網絡地址信息,這些信息被稱為
candidate
。 - candidate:
candidate
包含了設備的網絡地址和端口信息,可能是本地地址、反射地址(通過 NAT 獲得)或中繼地址(通過 TURN 服務器獲得)。當RTCPeerConnection
對象收集到一個candidate
時,會觸發onicecandidate
事件,此時可以將該candidate
通過socket.io
發送給對方。 - addIceCandidate:對方接收到
candidate
后,使用RTCPeerConnection.addIceCandidate
方法將其添加到自己的RTCPeerConnection
對象中,這樣雙方就可以嘗試通過該candidate
建立連接。
五、總結
webrtc - local
項目通過結合 WebRTC 技術和 socket.io
實現了簡單的局域
雙向視頻通話功能,客戶端和服務端在同一網絡環境下運行。
在實際應用中,可以考慮使用 STUN/TURN 服務器來解決跨網絡通信的問題。
倉庫
https://github.com/PL-FE/webrtc-local/tree/main