webrtc 通話流程
- ClientA 和 ClientB 都連結上信令伺服器(websocket)
- ClientA 獲取本地影片流,並透過(websocket)傳送會話描述資訊(offer sdp)
- ClientB 收到信令(offer sdp)後回覆(answer sdp)
- ClientA 接收到回覆信令後開始連結,協商通訊金鑰,完成影片傳輸。
值得注意的是,webrtc 是客戶端與客戶端直接連結,不需要服務端參與。信令伺服器僅在建立連結階段交換資料。
基礎環境
首先搭建一個基礎環境方便開發,我使用的是尤大最新開發的vite。
yarn create @vitejs/app webrtc --template vue && cd webrtc && yarn && yarn dev
安裝webrtc瀏覽器相容介面卡:
yarn add webrtc-adapter
一個小的demo就不引入不必要的擴充套件包了,自己動手實現一個簡單的路由器。調整App.vue
檔案:
<script>
import { h, computed } from "vue"
import routes from "./routes"
export default {
setup() {
const currentRoute = computed(() => window.location.pathname)
const currentComponent = computed(() => routes[currentRoute.value] || "")
return () => h(currentComponent.value)
}
}
</script>
新建 src/routes.js
檔案:
import Home from "./page/Server.vue";
import Client from "./page/Client.vue";
export default {
'/': Home,
'/client': Client
}
這樣就可以分開編寫客戶端和服務端程式碼了。由於這只是個簡單的 demo ,所以我這樣使用,生成環境開發還是建議大家使用 vue-router
。
正文
首先建立 src/page/Server.vue
元件,引入 webrtc
介面卡:
<template>
<div class="server">
<video id="localVideo" autoplay playsinline muted></video>
</div>
</template>
<script>
import "webrtc-adapter"
export default {
name: "Server"
}
</script>
<style scoped>
video {
width: 80%;
height: 80%;
background: #000;
position: absolute;
left: 0;right: 0;
top: 0;bottom: 0;
margin: auto;
}
</style>
1. 獲取本地影片流
這一步既可以錄製螢幕內容也可以讀取使用者攝像頭。
<template>
<div class="server">
<video id="localVideo" autoplay playsinline muted></video>
</div>
</template>
<script>
import "webrtc-adapter"
import { onMounted } from "vue"
export default {
name: "Home",
setup() {
const startLive = async () => {
const localVideo = document.getElementById("localVideo")
let stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true })
localVideo.srcObject = stream
}
onMounted(() => startLive())
return { }
}
}
</script>
這裡需要注意的是一定要在dom節點載入完畢之後再執行,否則會獲取不到video節點
2. 信令伺服器
想要與客戶端建立連結,必須有信令伺服器端介入,幫助客戶端之間進行通訊。其實信令伺服器原理很簡單,不用做任何處理(業務邏輯除外),只需要把客戶端傳送的資料原樣轉發給目標客戶端即可。我這裡採用 GoLang 實現:
package main
import (
"github.com/gogf/gf/frame/g"
"github.com/gogf/gf/net/ghttp"
"github.com/gogf/gf/os/glog"
)
var (
Server *ghttp.WebSocket
Client *ghttp.WebSocket
)
func main() {
s := g.Server()
s.BindHandler("/server", func(r *ghttp.Request) {
ws, err := r.WebSocket()
if err != nil {
glog.Error(err)
r.Exit()
}
Server = ws
for {
msgType, msg, err := ws.ReadMessage()
if err != nil {
return
}
if err = Client.WriteMessage(msgType, msg); err != nil {
return
}
}
})
s.BindHandler("/client", func(r *ghttp.Request) {
ws, err := r.WebSocket()
if err != nil {
glog.Error(err)
r.Exit()
}
Client = ws
for {
msgType, msg, err := ws.ReadMessage()
if err != nil {
return
}
if err = Server.WriteMessage(msgType, msg); err != nil {
return
}
}
})
s.SetPort(8199)
s.Run()
}
3. 建立連結
連結信令伺服器並且建立 PeerConnection 例項 (引數為null,忽略 iceserver,僅區域網下通訊);
let ws = new WebSocket("ws://127.0.0.1:8199/server")
let peer = new RTCPeerConnection(null)
// 將媒體軌道新增到軌道集
stream.getTracks().forEach(track => {
peer.addTrack(track, stream)
})
然後透過websocket傳送會話描述資訊(offer sdp):
const offer = await peer.createOffer()
await peer.setLocalDescription(offer)
ws.send(JSON.stringify(offer))
4. 客戶端接收
客戶端同樣連結信令伺服器並建立PeerConnection例項。
let ws = new WebSocket("ws://127.0.0.1:8199/client")
let peer = new RTCPeerConnection(null)
監聽信令伺服器訊息:
ws.onmessage = event => {
const { type, sdp, iceCandidate } = JSON.parse(e.data)
if (type === "offer") {
answer(new RTCSessionDescription({ type, sdp }))
} else if (type === "offer_ice") {
peer.addIceCandidate(iceCandidate)
}
}
收到伺服器連結請求後,需要回復訊息:
const answer = async sdp => {
await peer.setRemoteDescription(sdp)
const answer = await peer.createAnswer()
ws.send(JSON.stringify(answer))
await peer.setLocalDescription(answer)
}
5. 服務端監聽響應
客戶端收到連結請求並回復,服務端同時也要監聽客戶端的回覆:
ws.onmessage = e => {
const { type, sdp, iceCandidate } = JSON.parse(e.data)
if (type === "answer") {
peer.setRemoteDescription(new RTCSessionDescription({ type, sdp }))
} else if (type === "answer_ice") {
peer.addIceCandidate(iceCandidate)
}
}
6. 獲取遠端影片流
當呼叫 setLocalDescription
之後,RTC 連結就開始蒐集候選人,而我們只需要監聽事件,透過信令伺服器傳遞候選人資訊即可。
// 服務端
peer.onicecandidate = e => {
if (e.candidate) {
console.log("蒐集併傳送候選人")
ws.send(
JSON.stringify({
type: `offer_ice`,
iceCandidate: e.candidate,
})
)
} else {
console.log("候選人收集完成!")
}
}
// 客戶端
peer.onicecandidate = e => {
if (e.candidate) {
console.log("蒐集併傳送候選人")
ws.send(
JSON.stringify({
type: `answer_ice`,
iceCandidate: e.candidate,
})
)
} else {
console.log("候選人收集完成!")
}
}
候選人蒐集完成之後,服務端和客戶端就正式勾搭上了,開始通訊協商金鑰,建立一條最優的連結方式。這個時候只需要監聽 ontrack
事件獲取影片流即可:
peer.ontrack = e => {
if (e && e.streams) {
console.log("收到對方音訊/影片流資料...")
let localVideo = document.getElementById("localVideo")
localVideo.srcObject = e.streams[0]
}
}
結語
RTCPeerConnection 連結是雙向的,這裡我為了演示方便,僅做了單向影片傳輸,強行區分了服務端與客戶端。客戶端收到連結請求的同時也可以獲取本地影片流傳遞到服務端,形成雙向影片傳輸。同時為了方便演示信令伺服器也是最簡單的方式實現,客戶端比需先連結才能進行通訊。
完整程式碼已上傳 github.com/zxdstyle/webrtc-demo
本作品採用《CC 協議》,轉載必須註明作者和本文連結