webRTC之peerjs应用 实现简单音频通话

236 阅读2分钟

后端部分:1、简单起一个项目,引入依赖peer 2、代码

const { PeerServer } = require('peer');
const peerServer = PeerServer({
  port: 9000,  // 一致
  path: '/myapp',  // 前后端保持一致
  allow_discovery:true, //设置为真可以查看所有连接的用户
  ssl: {},
});

// http://localhost:9000/myapp/peer/peers  查看所有连接的用户

前端部分: 1、引入peerjs 1.3.1 2、sdk js部分

/*
 * @Descripttion: 
 * @Author: hanb
 * @Date: 2024-03-21 19:57:43
 * @LastEditors: hanb
 * @LastEditTime: 2024-03-21 23:15:08
 */

import Peer from 'peerjs';
// import { MessageBox, Message } from 'element-ui';
export class AudioTalk {
  constructor(options) {
    this.options = options // id、constraints(媒体配置对象)、callFn(任何处理结果的回调)
    this._this = options._this
    this.peer = null
    this.conn = null
    this.connectAnotherId = null
    this.caller = null
    this.localStream = null
    this.anotherName = ''
    this.waitTimer = null
    this.init()
  }
  init() {
    this.peer = new Peer(this.options.id, {
        host: this.options.hostname || location.hostname,
        debug: 1,
        port: 9000,
        path: this.options.path || '/myapp' // 后台peer服务器配置的路径与这里一致
    })
    // 定义创建媒体对象的option
    console.log(window.navigator,window.navigator.mediaDevices,'window.navigator')
    window.navigator.mediaDevices.getUserMedia(this.options.constraints || {video: false, audio: true}).then( stream => {
      // 将本地媒体流存储
      this.localStream = stream
    }).catch(err => {
      // 如果没有麦克风设备申请的话 会报错
        console.log("u got an error:" + err)
    });
    // 监听被call事件 接收端
    this.peer.on('call', (call) => {
      this.waitTimer = setTimeout(() => {
        // 超时未响应 拒绝
        this._this.$Modal.remove()
        this.conn && this.conn.close()
        call && call.close()
        clearTimeout(this.waitTimer)
        this.waitTimer = null
      }, 15000)
      setTimeout(() => {
        this._this.$Modal.confirm({
          title: '消息提醒',
          content: `<p>用户${this.anotherName || call.peer}请求通话,是否接通?</p>`,
          okText: '接听',
          cancelText:'拒绝',
          onOk: () => {
            clearTimeout(this.waitTimer)
            this.waitTimer = null
            call.answer(this.localStream)
            call.on('stream', (stream) => {
              // 接收方返回同意 2
              this.options.callFn && this.options.callFn({
                accept: 2,
                stream
              })
            });
            this.conn.send(JSON.stringify({
              apply: 2,
              user: this.options.id
            }))
          },
          onCancel: () => {
            clearTimeout(this.waitTimer)
            this.waitTimer = null
            this.options.callFn && this.options.callFn({
              accept: 3,
              stream: null
            })
            this.conn.send(JSON.stringify({
              apply: 3,
              user: this.options.id
            }))
            this.conn.close()
            call.close()
            console.log("call denied");
          }
        });
      }, 50);
    });
    this.peer.on('open', function () {
        console.log('ready to receive cast')
    });

    /**
     * When a data connection between peers is open, get the connecting peer's details
     * 接收端
     */
    this.peer.on('connection', (connection) => {
      console.log('peer connection')
      this.conn = connection;
      this.connectAnotherId = connection.peer;
      this.conn.on('open', () => {
        console.log('beconn open')
      })
      this.conn.on('data', (data) => {
        console.log('beconn data', data,this.conn)
        const receiveData = JSON.parse(data)
        if (receiveData.apply === 1) {
          this.anotherName = receiveData.name
          // 询问
          this.options.callFn && this.options.callFn({
            accept: 1,
            stream: null
          })
        }
        if (receiveData.apply === 4) {
          this._this.$Modal.remove()
          this.conn.close()
          if (this.waitTimer) {
            clearTimeout(this.waitTimer)
            this.waitTimer = null
          }
          this.options.callFn && this.options.callFn({
            accept: 4,
            stream: null
          })
        }
      })
      this.conn.on('close', () => {
        this.options.callFn && this.options.callFn({
          accept: 4,
          stream: null
        })
        console.log('beconn close')
      })
    });
    // 监听error报错
    this.peer.on('error', err => {
      this.options.callFn && this.options.callFn({
        accept: 5,
        stream: null,
        text: '无法连接'
      })
      console.error(err)
    });
  }
  // 未响应 提前挂断 申请方调用
  forwardDisconnect() {
    this.conn.send(JSON.stringify({
      apply: 4,
      id: this.options.id,
      name: this.options.name
    }))
  }
  connect(anotherId) {
    this.connectAnotherId = anotherId
    // 连接发起已建立 但还未获得确认的响应
      this.conn = this.peer.connect(anotherId)
      this.conn.on('open', () => {
        console.log('conn open')
        this.conn.send(JSON.stringify({
          apply: 1,
          id: this.options.id,
          name: this.options.name
        }))
      })
      this.conn.on("data", (data) => {
        console.log("conn data", data);
        const receiveData = JSON.parse(data)
        if (receiveData.apply === 2) {
          // 同意 2 走call
        } else {
          // 拒绝 3
          this.options.callFn && this.options.callFn({
            apply: 3,
            stream: null
          })
        }
      });
      this.conn.on('close', function () {
        console.log('conn close')
      })
      this.conn.on('error', function () {
        console.log('conn error')
      })
    // 发起通话 但还未响应 如未监听到对方返回的流 则未建立连接
      this.caller = this.peer.call(this.connectAnotherId, this.localStream);
      // 设置监听
      this.caller.on('stream', (stream)=> {
        // 同意 2 走call
        this.options.callFn && this.options.callFn({
          apply: 2,
          stream
        })
        console.log('caller stream')
      });
      this.caller.on('close', function () {
        console.log('caller close')
      })
      this.caller.on('error', function () {
        console.log('caller error')
      });
  }
  disconnect() {
    this.conn && this.conn.close();
    this.caller && this.caller.close();
  }
  // 销毁
  destroy() {
    this.conn && this.conn.close();
    this.caller && this.caller.close();
    this.peer && this.peer.destroy()
  }
}

3、调用 (要求写混入,要不会影响之前的代码 所以这部分也是js)

import { AudioTalk } from './audio-talk'
export default {
  data() {
    return {
      activeName: '',
      tempClickName: '',
      connTimer: null
    }
  },
  mounted() {
    const user = localStorage.getItem('logindata')
    const parseUser = JSON.parse(user)
    console.log(parseUser, 'parseUser')
    // 使用pkUser作为peer的id
    this.audioTalk = new AudioTalk({ id: parseUser.pkUser,_this: this,callFn:this.handleResult,name:parseUser.userName })
  },
  methods: {
    handleTalk(row) {
      console.log(row, 'row')
      if (this.tempClickName === row.pk_user && !this.activeName) {
        this.audioTalk.forwardDisconnect()
        setTimeout(() => {
          this.connTimer && clearTimeout(this.connTimer)
          // 响应中途要关闭
          this.closeTalk()
        }, 50);
        return
      }
      if (this.tempClickName&&!this.activeName) {
        // 当前正在拨打或正在通话
        this.$toast.center('currentHeader.png', `当前正在拨打,请稍后`,null,null,3000)
        return
      }
      if (row.pk_user === this.activeName) {
        this.closeTalk()
      } else if (this.activeName) {
        // 当前正在通话
        this.$toast.center('currentHeader.png', `当前正在通话,请先挂断`,null,null,3000)
      } else {
        this.audioTalk.connect(row.pk_user)
        this.tempClickName = row.pk_user
        this.$toast.center('currentHeader.png', `正在呼叫${row.studyroom_name}-${row.user_name}`, null, null, 3000)
        // 15s没响应的话提示并挂断 如有相应则关闭
        this.connTimer = setTimeout(() => {
          this.closeTalk('对方没有响应')
          clearTimeout(this.connTimer)
        },15000)
      }
    },
    handleResult({ accept, apply, stream }) {
      this.connTimer && clearTimeout(this.connTimer)
      // isAccepted 对方同意true
      if (apply === 2) {
        this.createAudioNode(stream)
        this.activeName = this.tempClickName
        this.$toast.center('success.png', '已接通')
      } else if (apply === 3) {
        this.$toast.center('fail.png', '对方已拒绝')
      }
      // 作为被接收方 同意2
      if (accept === 2) {
        this.createAudioNode(stream)
        this.$toast.center('success.png', '已接通')
      } else if (accept === 3) {
        this.$toast.center('fail.png', '已拒绝')
      } else if (accept === 4) {
        this.deleteAudioNode()
        this.closeTalk()
        this.$toast.center('fail.png', '对方已挂断')
      }
      if (accept === 5 || apply === 5) {
        this.$toast.center('fail.png', '无法连接')
      }
    },
    closeTalk(text = '已挂断') {
      this.activeName = this.tempClickName = ''
      this.deleteAudioNode()
      this.audioTalk.disconnect()
      this.$toast.center('fail.png', text)
    },
    createAudioNode(stream) {
      const audionode = document.createElement('audio')
      audionode.id = 'current-audio'
      audionode.srcObject = stream
      audionode.muted = false
      audionode.autoplay = true
      audionode.play()
      document.body.appendChild(audionode)
    },
    deleteAudioNode() {
      const audionode = document.getElementById('current-audio')
      if (audionode) {
        audionode.srcObject = null
        audionode.pause()
        document.body.removeChild(audionode)
      }
    }
  },
  beforeDestroy () {
    this.audioTalk.destroy();
  },
}