前言
2023年的第一周文章,整理一下自己使用electron,peerjs实现音视频通话的过程,新的一年与大家共同进步
阅读须知
- 代码使用TypeScript,vue-setup
- 脚手架 electron-vite
- 开发环境:window,node:v16.15.1,pnpm:v7.9.0
项目效果图
主要内容
获取屏幕流
主进程
使用electron提供的
desktopCapturerapi获取窗口信息使用该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)
})
})
}