Webrtc Call flow

  1. Both ClientA and ClientB are connected to a signaling server (WebSocket)
  2. ClientA gets local video stream and sends session description (Offer SDP) via (webSocket)
  3. After receiving the signaling (offer SDP)
  4. After receiving the reply signaling, ClientA starts the link, negotiates the communication key, and completes the video transmission.

It is important to note that webrTC is a client-to-client link without the involvement of the server. Signaling servers exchange data only during the link establishment phase.

Based on the environment

First of all, I set up a basic environment to facilitate development. I used the vite newly developed by The University of Utah.

yarn create @vitejs/app webrtc --template vue && cd webrtc && yarn && yarn dev
Copy the code

Install webrTC browser compatible adapter:

yarn add webrtc-adapter
Copy the code

A small demo will not introduce unnecessary extension packs and implement a simple router yourself. Adjust the app. vue file:

<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>

Copy the code

Create a new SRC /routes.js file:

import Home from "./page/Server.vue";
import Client from "./page/Client.vue";

export default {
    '/': Home,
    '/client': Client
}
Copy the code

This allows you to write client-side and server-side code separately. Since this is just a simple demo, I will use it this way, and I still recommend vue-Router for build environment development.

The body of the

First create SRC/Page/server. vue component and introduce webrTC adapter:

<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>
Copy the code

1. Obtain local video streams

This step can either record screen content or read the user’s camera.

<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>
Copy the code

Note that the execution must be done after the DOM node is loaded, otherwise the video node will not be obtained

2. Signaling server

To establish a link with clients, a signaling server must step in to help clients communicate with each other. In fact, the principle of the signaling server is very simple. It only needs to forward the data sent by the client to the target client without any processing (except for the service logic). I’m using GoLang here:

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(a)  {
	s := g.Server()

	s.BindHandler("/server".func(r *ghttp.Request) {
		ws, err := r.WebSocket()
		iferr ! =nil {
			glog.Error(err)
			r.Exit()
		}

		Server = ws

		for {
			msgType, msg, err := ws.ReadMessage()
			iferr ! =nil {
				return
			}
			iferr = Client.WriteMessage(msgType, msg); err ! =nil {
				return
			}
		}
	})

	s.BindHandler("/client".func(r *ghttp.Request) {
		ws, err := r.WebSocket()
		iferr ! =nil {
			glog.Error(err)
			r.Exit()
		}

		Client = ws

		for {
			msgType, msg, err := ws.ReadMessage()
			iferr ! =nil {
				return
			}
			iferr = Server.WriteMessage(msgType, msg); err ! =nil {
				return
			}
		}
	})

	s.SetPort(8199)
	s.Run()
}

Copy the code

3. Build a link

The signaling server is linked and the PeerConnection instance is created (the parameter is null, iceserver is ignored, and communication is only under the LAN).

let ws = new WebSocket(Ws: / / "127.0.0.1:8199 / server")
let peer = new RTCPeerConnection(null)

// Add the media track to the track set
stream.getTracks().forEach(track= > {
  peer.addTrack(track, stream)
})
Copy the code

Then send the session description (offer SDP) via websocket:

const offer = await peer.createOffer()
await peer.setLocalDescription(offer)
ws.send(JSON.stringify(offer))
Copy the code

4. The client receives the packet

The client also connects to the signaling server and creates a PeerConnection instance.

let ws = new WebSocket(Ws: / / "127.0.0.1:8199 / client")
let peer = new RTCPeerConnection(null)
Copy the code

Listen for signaling server messages:

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)
  }
}
Copy the code

After receiving a server link request, you need to reply with the following message:

const answer = async sdp => {
  await peer.setRemoteDescription(sdp)

  const answer = await peer.createAnswer()

  ws.send(JSON.stringify(answer))

  await peer.setLocalDescription(answer) 
}
Copy the code

5. The server listens for the response

The client receives the link request and replies, and the server also listens for the reply from the client:

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)
  }
}
Copy the code

6. Obtain remote video streams

When setLocalDescription is called, the RTC link collects candidates, and we just need to listen for events and pass candidate information through the signaling server.

/ / the server
peer.onicecandidate = e= > {
  if (e.candidate) {
    console.log("Gather and send candidates.")
    ws.send(
      JSON.stringify({
        type: `offer_ice`.iceCandidate: e.candidate,
      })
    )
  } else {
    console.log("Candidate collection completed!")}}/ / the client
peer.onicecandidate = e= > {
  if (e.candidate) {
    console.log("Gather and send candidates.")
    ws.send(
      JSON.stringify({
        type: `answer_ice`.iceCandidate: e.candidate,
      })
    )
  } else {
    console.log("Candidate collection completed!")}}Copy the code

Once the candidate collection is complete, the server and client are officially hooked up and start communicating and negotiating keys to establish an optimal link. At this time, only need to listen ontrack event to obtain the video stream:

peer.ontrack = e= > {
  if (e && e.streams) {
    console.log("Received audio/video stream data from the other party...")
    let localVideo = document.getElementById("localVideo")
    localVideo.srcObject = e.streams[0]}}Copy the code

conclusion

RTCPeerConnection is bidirectional. For the convenience of demonstration, I only make one-way video transmission and forcibly distinguish between the server and the client. When the client receives the link request, it can also obtain the local video stream and transfer it to the server to form two-way video transmission. At the same time, in order to facilitate the demonstration signaling server is also the simplest way to implement, the client must be linked before communication.

The full code has been uploaded at github.com/zxdstyle/we…