使用electron,peerjs实现简单P2P音视频通话及屏幕共享
/ 5 min read
Table of Contents
前言
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) }) })}