skip to content
Logo 裁晨

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

/ 5 min read

Table of Contents

前言

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. https://www.electronjs.org/
  2. https://peerjs.com/docs/#api