基於webrtc實現點對點桌面分享

zxdstyle發表於2021-03-08

webrtc 通話流程

  1. ClientA 和 ClientB 都連結上信令伺服器(websocket)
  2. ClientA 獲取本地影片流,並透過(websocket)傳送會話描述資訊(offer sdp)
  3. ClientB 收到信令(offer sdp)後回覆(answer sdp)
  4. 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 協議》,轉載必須註明作者和本文連結
更多文章去我的部落格 看看

相關文章