Day24 [实作] 一对一视讯通话(4): 加入通话及挂断机制

结合前两篇我们已经实现了 MVP(Minimum Viable Product;最小可行产品),完成了最简单的一对一通话,後续我们会慢慢强化他的功能,今天我们会加上通话及挂断机制,运作方法如下图。

https://ithelp.ithome.com.tw/upload/images/20211008/20130062bzuaVjosqv.png

Server 订阅挂断电话

收到 leave 消息後向对方发送bye 对自己发送 leaved

// 离开房间
socket.on('leave', (room) => {
  socket.to(room).emit('bye')
  socket.emit('leaved')
})

Client 添加挂电话功能

  1. index.html 中增加挂电话按钮

    <div>
      <button id="leaveBtn">Leave</button>
    </div>
    
  2. main.js 中增加 挂断电话

    function hangup() {
      if (peerConn) {
        peerConn.close()
        peerConn = null
      }
    }
    
  3. main.js 中增加 byeleaved 两个订阅

    // 收到 leaved 把 socket 中断连线
    socket.on('leaved', (room, id) => {
      console.log('收到 leaved')
      // 中断 socket
      socket.disconnect()
    })
    
    socket.on('bye', (room, id) => {
      console.log('收到 bye')
      // 对方要挂掉电话
      hunghp()
    })
    
  4. main.js 中增加 按钮绑定

    const leaveBtn = document.querySelector('button#leaveBtn')
    
    // 点击後发送离开的讯息并挂断电话
    leaveBtn.onclick = () => {
      if (socket) {
        socket.emit('leave', room)
      }
      hangup()
    }
    

Client 加入启动通话的功能

  1. index.html 中增加启动通话按钮

    <div>
      <button id="startBtn">Start</button>
      <button id="leaveBtn">Leave</button>
    </div>
    
  2. socket.io 相关程序码使用function 包起来

    function connectIO() {
      // socket
      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)
      })
    
      socket.on('leaved', () => {
        console.log('收到 leaved')
        socket.disconnect()
      })
    
      socket.on('bye', () => {
        console.log('收到 bye')
        hangup()
      })
    
      socket.emit('join', room)
    }
    
  3. 替换初始化方法

    const startBtn = document.querySelector('button#startBtn')
    
    ...
    
    // window.onload = init()
    startBtn.onclick = init
    
  4. 加上按钮控制

    async function init() {
      await createStream()
      initPeerConnection()
      connectIO()
      startBtn.disabled = true
      leaveBtn.disabled = false
    }
    
    leaveBtn.onclick = () => {
      if (socket) {
        socket.emit('leave', room)
      }
      hangup()
      startBtn.disabled = false
      leaveBtn.disabled = true
    }
    

完整程序码:

  1. 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>
          <button id="startBtn">Start</button>
          <button id="leaveBtn">Leave</button>
        </div>
    
        <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. main.js

    // video 标签
    const localVideo = document.querySelector('video#localVideo')
    const remoteVideo = document.querySelector('video#remoteVideo')
    
    // button 标签
    const startBtn = document.querySelector('button#startBtn')
    const leaveBtn = document.querySelector('button#leaveBtn')
    
    let localStream
    let peerConn
    let socket
    const room = 'room1'
    
    function connectIO() {
      // socket
      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)
      })
    
      socket.on('leaved', () => {
        console.log('收到 leaved')
        socket.disconnect()
        closeLocalMedia()
      })
    
      socket.on('bye', () => {
        console.log('收到 bye')
        hangup()
      })
    
      socket.emit('join', room)
    }
    
    /**
     * 取得本地串流
     */
    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) {
          initPeerConnection()
        }
    
        // 创建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
      }
    }
    
    /**
     * 关闭自己的视讯
     */
    function closeLocalMedia() {
      if (localStream && localStream.getTracks()) {
        localStream.getTracks().forEach((track) => {
          track.stop()
        })
      }
      localStream = null
    }
    
    /**
     * 挂掉电话
     */
    function hangup() {
      if (peerConn) {
        peerConn.close()
        peerConn = null
      }
    }
    
    /**
     * 初始化
     */
    async function init() {
      await createStream()
      initPeerConnection()
      connectIO()
      startBtn.disabled = true
      leaveBtn.disabled = false
    }
    
    // window.onload = init()
    
    leaveBtn.onclick = () => {
      if (socket) {
        socket.emit('leave', room)
      }
      hangup()
      startBtn.disabled = false
      leaveBtn.disabled = true
    }
    
    startBtn.onclick = init
    

<<:  [Day 24] Reactive Programming - Spring WebFlux(Router)

>>:  【设计+切版30天实作】|Day24 - Steps区块 - 如何做出渐层背景?

Day17 Let's ODOO: Data Files

通常我们在写module的时候,会需要一些初始资料或是固定需要的资料,我们可以定义资料在创立Mode...

[Day04] - 新拟物风按钮(二) - shadow dom 介绍

昨天我们做了一个不能点的 neuomorphic-button 今天我们把他可以点击 & 加...

Python & Celery 学习笔记_任务链结

Celery 提供任务链结的功能,字面上的意思,就是将任务一个一个串联在一起,下面的叙述 or 范例...

【把玩Azure DevOps】Day5 版本控制系统Repos:建立第二个Repo,TFVC Repo

前一篇文章提到了Azure DevOps上的Repos可以分成Git Repo和TFVC Repo,...

Day 21. slate × Operation × Entrance

上一篇我们介绍了 interfaces/operations.ts 里定义的 Operation ...