23、屏幕共享Electron应用:Electron桌面捕获、主进程与渲染进程通信、打包部署

说实话,做WebRTC屏幕共享这么久,我一直觉得Electron是个很妙的载体。你想想看,浏览器里做屏幕共享,权限限制多,功能也受限。但换成Electron,咱们就能直接调用操作系统的桌面捕获API,想抓哪个窗口就抓哪个窗口,想录全屏就录全屏。嗯,这章我就带你把这个流程完整走一遍。

Electron桌面捕获的核心机制

Electron的桌面捕获,说白了就是通过desktopCapturer这个模块来获取屏幕源。我在项目中遇到过一个问题:用户想共享某个特定窗口,但列表里死活找不到。后来发现是没在BrowserWindowwebPreferences里开启enableRemoteModule。这个坑我踩过,所以先给你提个醒。

核心API:desktopCapturer.getSources(options)

这个方法会返回一个数组,每个元素代表一个可捕获的屏幕源。每个源包含nameidthumbnail等信息。

// 主进程代码
const { desktopCapturer } = require('electron');

async function getScreenSources() {
  const sources = await desktopCapturer.getSources({
    types: ['screen', 'window'],  // 捕获屏幕和窗口
    thumbnailSize: { width: 320, height: 240 }  // 缩略图大小
  });
  
  return sources.map(source => ({
    id: source.id,
    name: source.name,
    thumbnail: source.thumbnail.toDataURL()
  }));
}

这里有个细节:types参数可以传['screen']只捕获整个屏幕,也可以传['window']只捕获窗口。我个人习惯两个都传,让用户自己选。

主进程与渲染进程的通信设计

Electron应用里,主进程负责系统级操作(比如桌面捕获),渲染进程负责UI展示。这两者怎么通信?我用的是ipcMainipcRenderer。说白了就是一套事件机制。

为什么会这样设计?因为安全。渲染进程不能直接调用系统API,必须通过主进程中转。我曾经见过有人图省事,在渲染进程里直接require('electron'),结果打包后各种报错。嗯,这个坑你别踩。

主进程监听请求

// main.js - 主进程
const { ipcMain, desktopCapturer } = require('electron');

ipcMain.handle('GET_SOURCES', async (event) => {
  const sources = await desktopCapturer.getSources({
    types: ['screen', 'window'],
    thumbnailSize: { width: 320, height: 240 }
  });
  
  return sources.map(source => ({
    id: source.id,
    name: source.name,
    thumbnail: source.thumbnail.toDataURL()
  }));
});

渲染进程发起请求

// renderer.js - 渲染进程
const { ipcRenderer } = require('electron');

async function loadSources() {
  const sources = await ipcRenderer.invoke('GET_SOURCES');
  // 渲染到UI
  sources.forEach(source => {
    const div = document.createElement('div');
    div.innerHTML = `
      <img src="${source.thumbnail}" />
      <p>${source.name}</p>
    `;
    div.onclick = () => selectSource(source.id);
    document.getElementById('source-list').appendChild(div);
  });
}

小技巧:如果你需要频繁通信,可以用webContents.send()从主进程主动推送数据给渲染进程。比如实时更新屏幕捕获的帧率信息。

将屏幕流传递给WebRTC

拿到桌面源之后,怎么把它塞进WebRTC的RTCPeerConnection?流程是这样的:

  1. 渲染进程拿到源ID后,通过navigator.mediaDevices.getUserMedia获取流
  2. 把流添加到RTCPeerConnection
  3. 创建Offer/Answer,建立连接
// 渲染进程 - 获取屏幕流
async function startScreenShare(sourceId) {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({
      audio: false,
      video: {
        mandatory: {
          chromeMediaSource: 'desktop',
          chromeMediaSourceId: sourceId,
          maxWidth: 1920,
          maxHeight: 1080,
          maxFrameRate: 30
        }
      }
    });
    
    // 添加到PeerConnection
    stream.getTracks().forEach(track => {
      pc.addTrack(track, stream);
    });
    
    // 本地预览
    document.getElementById('local-video').srcObject = stream;
  } catch (err) {
    console.error('获取屏幕流失败:', err);
  }
}

注意:在Electron中,chromeMediaSource必须设置为'desktop',否则无法捕获桌面。另外,audio: false是因为屏幕共享一般不包含系统音频,如果你需要,可以单独处理。

打包部署:从开发到生产

开发完了怎么打包?我用的是electron-builder。这个工具能帮你生成Windows的exe、macOS的dmg、Linux的AppImage。配置起来也不复杂。

安装依赖

npm install --save-dev electron-builder

配置package.json

{
  "name": "screen-share-app",
  "version": "1.0.0",
  "main": "main.js",
  "scripts": {
    "start": "electron .",
    "build": "electron-builder"
  },
  "build": {
    "appId": "com.example.screenshare",
    "productName": "屏幕共享工具",
    "directories": {
      "output": "dist"
    },
    "win": {
      "target": "nsis",
      "icon": "assets/icon.ico"
    },
    "mac": {
      "target": "dmg",
      "icon": "assets/icon.icns"
    },
    "nsis": {
      "oneClick": false,
      "allowToChangeInstallationDirectory": true
    }
  }
}

运行npm run build,等几分钟,dist目录下就会生成安装包。我第一次打包时忘了配置appId,结果报错说缺少标识符。嗯,这个字段是必填的,别漏了。

避坑指南

  • 权限问题:macOS上需要开启屏幕录制权限,否则desktopCapturer返回空数组。我当初调试了半天才发现是权限没开。
  • 打包后路径问题:开发时用__dirname没问题,打包后路径会变。建议用app.getAppPath()获取应用根目录。
  • 窗口捕获性能:如果用户有多个显示器,捕获整个屏幕时帧率可能下降。我建议限制最大帧率为15fps,用户体验会好很多。

知识体系总览

下面这张图把整个流程串起来了,你可以对照着看:

Electron屏幕共享架构图 主进程 (Main Process) desktopCapturer getSources() → 屏幕/窗口源列表 ipcMain.handle() 处理渲染进程的请求 electron-builder 渲染进程 (Renderer Process) UI界面 显示源列表、预览、控制按钮 ipcRenderer.invoke() 请求桌面源列表 RTCPeerConnection 源列表数据 请求源列表 主进程负责系统级操作,渲染进程负责UI和WebRTC逻辑

这张图里,主进程和渲染进程通过IPC通信,各司其职。桌面捕获在主进程完成,WebRTC连接在渲染进程建立。打包时用electron-builder把整个应用封装成可安装的程序。

好了,这一章的内容就这些。你跟着代码走一遍,应该能跑通一个完整的屏幕共享Electron应用。如果遇到问题,回头看看避坑指南,大部分坑我都帮你踩过了。

公众号:蓝海资料掘金营,微信deep3321