Day23 [实作] 一对一视讯通话(3): Client

昨天我们把 Signaling server 完成了,今天我们要继续完成 Client 端:

  1. 细部分解
  2. 完整程序码
  3. 测试

https://ithelp.ithome.com.tw/upload/images/20211007/201300625LHZ59vYez.png

index.html

上一篇为了测试我们在 index.html 中写入 hello,今天我们要把他替换掉,我们要在画面中呈现两个 video ,一个显示自己的画面,另一个显示通话对方的画面

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>1on1 webrtc</title>
  </head>
  <body>

    <div>
      <video muted="false" width="320" autoplay playsinline id="localVideo" ></video>
      <video width="320" autoplay playsinline id="remoteVideo"></video>
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
    <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
    <script src="./js/main.js"></script>
    
  </body>
</html>

建立 js 档

在 public 资料夹内建立一个 js 的资料夹,并在内部建立 main.js

// video 标签
const localVideo = document.querySelector('video#localVideo')
const remoteVideo = document.querySelector('video#remoteVideo')

let localStream
let peerConn
const room = 'room1'

// socket
const socket = io('ws://0.0.0.0:8080')

socket.on('ready', async (msg) => {
	// TODO:- 收到 ready 代表对方已经连线,可以建立offer 发过去
})

socket.on('offer', async (desc) => {
  // TODO:- 收到 offer 後,设定对方的配置,并建立 answer 发送到对端
})

socket.on('answer', async (desc) => {
  // TODO:- 收到 answer 後,设定对方的配置
})

socket.on('ice_candidate', async (data) => {
  // TODO:- 加入新取得的 ICE candidate
})

function init() {
	// 加入房间
  socket.emit('join', room)
}

window.onload = init()

收到 ready 代表对方已经连线,可以建立offer 发过去

  1. 建立一个用来处理信令的 function

    /**
     * 处理信令
     * @param {Boolean} isOffer 是 offer 还是 answer
     */
    async function sendSDP(isOffer) {
      try {
        if (!peerConn) {
          return
        }
    
        // 建立 SDP 
        const localSDP = await peerConn[isOffer ? 'createOffer' : 'createAnswer']({
          offerToReceiveAudio: true,
          offerToReceiveVideo: true,
        })
    
        // 设定本地 SDP 信令
        await peerConn.setLocalDescription(localSDP)
    
        // 寄出SDP信令
        let e = isOffer ? 'offer' : 'answer'
        socket.emit(e, room, peerConn.localDescription)
      } catch (err) {
        throw err
      }
    }
    
  2. 在ready中加入

    socket.on('ready', async (msg) => {
      // 发送 offer
      await sendSDP(true)
    })
    

收到 offer 後,设定对方的配置,并建立 answer 发送到对端

socket.on('offer', async (desc) => {

  // 设定对方的配置
  await peerConn.setRemoteDescription(desc)

  // 发送 answer
  await sendSDP(false)
})

收到 answer 後,设定对方的配置

socket.on('answer', async (desc) => {

  // 设定对方的配置
  await peerConn.setRemoteDescription(desc)
})

加入新取得的 ICE candidate

socket.on('ice_candidate', async (data) => {

  const candidate = new RTCIceCandidate({
    sdpMLineIndex: data.label,
    candidate: data.candidate,
  })
  await peerConn.addIceCandidate(candidate)
})

取得自己的视讯并建立连接

/**
 * 取得本地串流
 */
async function createStream() {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({
      audio: true,
      video: true,
    })

    localStream = stream

    localVideo.srcObject = stream
  } catch (err) {
    throw err
  }
}

/**
 * 初始化Peer连结
 */
function initPeerConnection() {
  const configuration = {
    iceServers: [
      {
        urls: 'stun:stun.l.google.com:19302',
      },
    ],
  }
  peerConn = new RTCPeerConnection(configuration)

  // 增加本地串流
  localStream.getTracks().forEach((track) => {
    peerConn.addTrack(track, localStream)
  })

  // 找寻到 ICE 候选位置後,送去 Server 与另一位配对
  peerConn.onicecandidate = (e) => {
    if (e.candidate) {
      console.log('发送 ICE')
      // 发送 ICE
      socket.emit('ice_candidate', room, {
        label: e.candidate.sdpMLineIndex,
        id: e.candidate.sdpMid,
        candidate: e.candidate.candidate,
      })
    }
  }

  // 监听 ICE 连接状态
  peerConn.oniceconnectionstatechange = (e) => {
    if (e.target.iceConnectionState === 'disconnected') {
      remoteVideo.srcObject = null
    }
  }

  // 监听是否有流传入,如果有的话就显示影像
  peerConn.onaddstream = ({ stream }) => {
    // 接收流并显示远端视讯
    remoteVideo.srcObject = stream
  }
}
async function init() {
  await createStream()
  initPeerConnection()
  socket.emit('join', room)
}

完整程序码如下:

  1. public/index.html

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>1on1 webrtc</title>
      </head>
      <body>
    
        <div>
          <video muted width="320" autoplay playsinline id="localVideo" ></video>
          <video width="320" autoplay playsinline id="remoteVideo"></video>
        </div>
    
        <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
        <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
        <script src="./js/main.js"></script>
    
      </body>
    </html>
    
  2. pubilc/js/main.js

    // video 标签
    const localVideo = document.querySelector('video#localVideo')
    const remoteVideo = document.querySelector('video#remoteVideo')
    
    let localStream
    let peerConn
    const room = 'room1'
    
    // socket
    const socket = io('ws://0.0.0.0:8080')
    
    socket.on('ready', async (msg) => {
      console.log(msg)
      // 发送 offer
      console.log('发送 offer ')
      await sendSDP(true)
    })
    
    socket.on('ice_candidate', async (data) => {
      console.log('收到 ice_candidate')
      const candidate = new RTCIceCandidate({
        sdpMLineIndex: data.label,
        candidate: data.candidate,
      })
      await peerConn.addIceCandidate(candidate)
    })
    
    socket.on('offer', async (desc) => {
      console.log('收到 offer')
    
      // 设定对方的配置
      await peerConn.setRemoteDescription(desc)
    
      // 发送 answer
      await sendSDP(false)
    })
    
    socket.on('answer', async (desc) => {
      console.log('收到 answer')
    
      // 设定对方的配置
      await peerConn.setRemoteDescription(desc)
    })
    
    /**
     * 取得本地串流
     */
    async function createStream() {
      try {
        // 取得影音的Stream
        const stream = await navigator.mediaDevices.getUserMedia({
          audio: true,
          video: true,
        })
    
        // 提升作用域
        localStream = stream
    
        // 导入<video>
        localVideo.srcObject = stream
      } catch (err) {
        throw err
      }
    }
    
    /**
     * 初始化Peer连结
     */
    function initPeerConnection() {
      const configuration = {
        iceServers: [
          {
            urls: 'stun:stun.l.google.com:19302',
          },
        ],
      }
      peerConn = new RTCPeerConnection(configuration)
    
      // 增加本地串流
      localStream.getTracks().forEach((track) => {
        peerConn.addTrack(track, localStream)
      })
    
      // 找寻到 ICE 候选位置後,送去 Server 与另一位配对
      peerConn.onicecandidate = (e) => {
        if (e.candidate) {
          console.log('发送 ICE')
          // 发送 ICE
          socket.emit('ice_candidate', room, {
            label: e.candidate.sdpMLineIndex,
            id: e.candidate.sdpMid,
            candidate: e.candidate.candidate,
          })
        }
      }
    
      // 监听 ICE 连接状态
      peerConn.oniceconnectionstatechange = (e) => {
        if (e.target.iceConnectionState === 'disconnected') {
          remoteVideo.srcObject = null
        }
      }
    
      // 监听是否有流传入,如果有的话就显示影像
      peerConn.onaddstream = ({ stream }) => {
        // 接收流并显示远端视讯
        remoteVideo.srcObject = stream
      }
    }
    
    /**
     * 处理信令
     * @param {Boolean} isOffer 是 offer 还是 answer
     */
    async function sendSDP(isOffer) {
      try {
        if (!peerConn) {
          console.log('尚未开启视讯')
          return
        }
    
        // 创建SDP信令
        const localSDP = await peerConn[isOffer ? 'createOffer' : 'createAnswer']({
          offerToReceiveAudio: true, // 是否传送声音流给对方
          offerToReceiveVideo: true, // 是否传送影像流给对方
        })
    
        // 设定本地SDP信令
        await peerConn.setLocalDescription(localSDP)
    
        // 寄出SDP信令
        let e = isOffer ? 'offer' : 'answer'
        socket.emit(e, room, peerConn.localDescription)
      } catch (err) {
        throw err
      }
    }
    
    /**
     * 初始化
     */
    async function init() {
      await createStream()
      initPeerConnection()
      socket.emit('join', room)
    }
    
    window.onload = init()
    

测试

  1. 确认结构

    ❯ tree -I 'node_modules'
    .
    ├── package-lock.json
    ├── package.json
    ├── public
    │   ├── index.html
    │   ├── index.js
    │   └── js
    │       └── main.js
    └── server.js
    
    2 directories, 6 files
    
  2. 进入 1-on-1-webrtc 资料夹内部

    node server.js
    
  3. 连线开启两个视窗 PC_A 及 PC_B

    http://localhost:8080/
    

    https://ithelp.ithome.com.tw/upload/images/20211007/20130062e38Wjrughv.png


<<:  Day 22. slate × Operation × transform

>>:  在HTML加入JavaScrip

30天学会 Python: Day 7-无限轮回

range(start=0, stop, step=1) 用来产生 整数等差数列 的函式,常和今天要...

数据中台架构

包含基础设施、架构设计、资料采集(ETL)、主资料管理(MDM)、即时计算、资资料储存和作业排程等。...

Day 28 - 设定 GRE Tunnel

如果有用过 HE 提供的 Tunnel Broker 服务的话,应该对 SIT 隧道不陌生。 但是,...

Golang - debug工具 DELVE

状况 最近的经验是要把公司的程序码翻新 但由於旧有的程序码技术债实在太过庞大,没办法像以前以往接手到...

Day7 - 程序设计报价 (二) - 重新定义甲乙关系

从传统的接案甲乙方关系我们发现,因为利益的冲突,甲方也不可能得到乙方 100% 的专业协助,因为乙方...