使用electron,peerjs实现简单P2P音视频通话及屏幕共享

1,282 阅读2分钟

前言

2023年的第一周文章,整理一下自己使用electron,peerjs实现音视频通话的过程,新的一年与大家共同进步

阅读须知

  1. 代码使用TypeScript,vue-setup
  2. 脚手架 electron-vite
  3. 开发环境:window,node:v16.15.1,pnpm:v7.9.0

项目效果图

20230108203757.png

主要内容

获取屏幕流

主进程

使用electron提供的desktopCapturerapi获取窗口信息

electron获取窗口以及屏幕

使用该api的原因是因为在渲染进程中需要使用窗口id才能获取到流 实现代码如下

function getScreen() {
  ipcMain.handle('getScreenList', async (_event) => {
    const callback = () => {
      return new Promise((resolve) => {
        desktopCapturer.getSources({ types: ['screen'] }).then((sources) => {
          let list: sourcesOption[] = []
          for (const item of sources) {
            list.push({
              id: item.id,
              name: item.name,
              thumbnail: item.thumbnail.toDataURL()
            })
          }
          resolve(list)
        })
      })
    }
    return await callback()
  })
}

渲染进程

在模板中添加两个video标签并简单实现UI

<template>
  <div class="call" id="call-view">
    <div class="head drag">
      <div class="left"></div>
      <div class="center">{{ friend }}</div>
      <div class="right"></div>
    </div>
    <div class="content">
      <n-spin :show="loading">
        <template #description>等待对方接听...</template>
        <div class="view card">
          <video ref="friendVideoRef" class="video-view"></video>
        </div>
      </n-spin>
      <div class="list">
        <div class="card user">
          <video ref="userVideoRef" class="user-video"></video>
        </div>
      </div>
    </div>
    <div class="foot">
      <n-space>
        <n-button strong secondary>
          <template #icon>
            <n-icon size="22"><MicOff24Regular /></n-icon>
          </template>
        </n-button>
        <n-button strong secondary @click="onMedia(true)">
          <template #icon>
            <n-icon size="22"><VideoOff24Regular /></n-icon>
          </template>
        </n-button>
        <n-button strong secondary type="error" @click="onCallQuit">
          <template #icon>
            <n-icon size="22"><CloseOutline /></n-icon>
          </template>
        </n-button>
      </n-space>
    </div>
  </div>
</template>

调用主进程中的方法,获取窗口信息

const screenList = await window.electron.ipcRenderer.invoke('getScreenList')
const stream = await navigator.mediaDevices.getUserMedia({
    audio: true,
    video: {
        // @ts-ignore
        mandatory: {
          chromeMediaSource: 'desktop',
          chromeMediaSourceId: item.id
        }
    }
})

获取相机流

获取相机视频流相对简单,只需要在渲染进程中实现即可

const getMediaDevices = (): Promise<MediaDeviceInfo[]> => {
  return new Promise((resolve) => {
    navigator.mediaDevices.enumerateDevices().then((res) => {
      let list: MediaDeviceInfo[] = []
      for (const iterator of res) {
        //从音视频设备中筛选视频设备
        if (iterator.kind === 'videoinput') {
          list.push(iterator)
        }
      }
      resolve(list)
    })
  })
}
const mediaDeviceList = await getMediaDevices()

使用peerjs实现p2p通话

Peer官网 可以使用peer提供的server服务也可以使用peer-server搭建自己的服务端

新建call.ts文件实现对peerjs的简单封装

1.导入peerjs中方法

//DataConnection为好友对象以实现对好友发送的音视频变更消息进行监听
//MediaConnection为连接通话后的音视频对象以实现对音视频流的状态监听
import { DataConnection, MediaConnection, Peer } from 'peerjs'

2.初始化以及实现基础事件监听

import { DataConnection, MediaConnection, Peer } from 'peerjs'

const TAG = '[PEER]'
export class CallClient {
  client: Peer
  frienid?: DataConnection
  call?: MediaConnection
  remoteStream?: MediaStream
  callCallback?: any
  hangUpCallback?: any
  constructor(user: string) {
    //传入用户id,自定义
    this.client = new Peer(user, {
      host: '192.168.0.105',
      port: 9000,
      path: '/myapp'
    })
    this.event()
  }
  onCallAddEventListener(callback) {
    this.callCallback = callback
  }
  onHangUpAddEventListener(callback) {
    this.hangUpCallback = callback
  }
  
  event() {
    this.client.on('open', async (id) => {
      console.log(TAG, 'peerJs服务连接成功:' + id)
    })
    //当消息连接成功时触发
    this.client.on('connection', (val) => {
      this.frienid = val
      this.frienid.on('data', (data) => {
        this.onFriendData(data)
      })
    })
    //当接入通话时触发
    this.client.on('call', async (call) => {
      this.call = call
      this.callCallback()
    })
    this.client.on('close', function () {
      console.log(TAG, 'close')
    })
    this.client.on('error', (e) => {
      console.log(TAG, 'error', e)
    })
  }
  
  onHangUp() {
    this.call?.close()
  }
  
  //实现对音视频自定义消息的处理
  onFriendData(data) {
    console.log(TAG, 'frienid', data)
    switch (data) {
      //挂断
      case 'closecall':
        this.call = undefined
        this.frienid = undefined
        this.remoteStream = undefined
        this.hangUpCallback()
        break
      default:
        break
    }
  }
}

3.实现呼叫的方法

//传入好友的用户id,以及本地视频流
onCallTo(friendId: string, stream: MediaStream): Promise<MediaStream> {
    return new Promise((resolve) => {
      //创建消息连接
      this.frienid = this.client.connect(friendId)
      //处理音视频消息
      this.frienid.on('data', (data) => {
        this.onFriendData(data)
      })
      this.call = this.client.call(friendId, stream)
      //己方挂断时向对方发送挂断消息
      this.call.on('close', () => {
        console.log(TAG, 'call-close')
        this.frienid?.send('closecall')
      })
      //监听对方流的加入,并回调自定义渲染方法
      this.call.on('stream', (remoteStream) => {
        this.remoteStream = remoteStream
        resolve(remoteStream)
      })
    })
}

4.实现加入通话的方法

//传入本地视频流
onJoinCall(stream: MediaStream): Promise<MediaStream> {
    return new Promise((resolve) => {
      this.call!.answer(stream)
      //己方挂断时向对方发送挂断消息
      this.call!.on('close', () => {
        console.log(TAG, 'call-close')
        this.frienid?.send('closecall')
      })
      //监听对方流的加入,并回调自定义渲染方法
      this.call!.on('stream', (remoteStream) => {
        this.remoteStream = remoteStream
        resolve(remoteStream)
      })
    })
}

参考文档

  1. www.electronjs.org/
  2. peerjs.com/docs/#api