You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

无需WebRTC,如何通过Express传输Electron桌面捕获的MediaStream?

嘿,我来帮你搞定这个问题!你说的没错,fs.createReadStream根本没法处理Electron捕获的MediaStream——前者是用来读取本地文件的静态流,后者是实时在内存里生成的媒体流,完全不是一个路数。而且不用WebRTC确实能实现,我给你两种靠谱的方案,你可以根据需求选:

方案一:MediaRecorder + Express响应流(简单易实现)

这种方式适合对延迟要求不高的场景,用MediaRecorder把桌面流编码成webm格式,再通过Express的响应对象实时推送给前端,前端直接用video标签播放即可。

后端(Electron + Express)代码修改

var express = require('express');
var app = express();
const { desktopCapturer } = require('electron');

// 先初始化桌面捕获流
let desktopStream;
async function initDesktopStream() {
  const sources = await desktopCapturer.getSources({ types: ['window', 'screen'] });
  // 这里默认选第一个捕获源,你可以改成用户选择的目标窗口/屏幕
  const selectedSource = sources[0];
  desktopStream = await navigator.mediaDevices.getUserMedia({
    audio: false, // 需要音频的话改成true
    video: {
      mandatory: {
        chromeMediaSource: 'desktop',
        chromeMediaSourceId: selectedSource.id,
        minWidth: 1280,
        maxWidth: 1280,
        minHeight: 720,
        maxHeight: 720
      }
    }
  });
}

// 启动时初始化流
initDesktopStream();

// 提供桌面流的接口
app.get('/desktop-stream', (req, res) => {
  if (!desktopStream) {
    res.status(500).send('桌面流未初始化');
    return;
  }

  // 设置响应头,告诉前端这是webm格式的流式数据
  res.setHeader('Content-Type', 'video/webm');
  res.setHeader('Transfer-Encoding', 'chunked');

  // 创建MediaRecorder,编码为vp9格式的webm
  const recorder = new MediaRecorder(desktopStream, { mimeType: 'video/webm; codecs=vp9' });

  // 每当有编码后的数据块生成,就推送给前端
  recorder.ondataavailable = (e) => {
    if (e.data.size > 0) {
      e.data.arrayBuffer().then(buffer => {
        res.write(Buffer.from(buffer));
      });
    }
  };

  // 录制停止时结束响应
  recorder.onstop = () => {
    res.end();
  };

  // 每100ms生成一个数据块(值越小延迟越低,但传输压力越大)
  recorder.start(100);

  // 客户端断开连接时停止录制,释放资源
  req.on('close', () => {
    recorder.stop();
    console.log('客户端断开连接,已停止桌面流');
  });
});

// 托管前端静态页面
app.use(express.static('public'));

app.listen(3000, () => {
  console.log('服务已启动:http://localhost:3000');
});

前端页面(public/index.html)

<!DOCTYPE html>
<html>
<head>
  <title>桌面流预览</title>
</head>
<body>
  <video id="desktop-video" autoplay muted controls style="width: 100%;"></video>
  <script>
    const video = document.getElementById('desktop-video');
    // 直接把流接口地址赋值给video的src
    video.src = 'http://localhost:3000/desktop-stream';
  </script>
</body>
</html>

方案二:WebSocket实时推送帧(低延迟场景)

如果需要更低的延迟,可以用WebSocket把桌面流的每一帧推送给前端,再通过MediaSource拼接成可播放的流。

后端代码修改

var express = require('express');
var app = express();
const http = require('http');
const WebSocket = require('ws');
const { desktopCapturer } = require('electron');

const server = http.createServer(app);
const wss = new WebSocket.Server({ server });

let desktopStream;
async function initDesktopStream() {
  const sources = await desktopCapturer.getSources({ types: ['window', 'screen'] });
  const selectedSource = sources[0];
  desktopStream = await navigator.mediaDevices.getUserMedia({
    audio: false,
    video: {
      mandatory: {
        chromeMediaSource: 'desktop',
        chromeMediaSourceId: selectedSource.id,
        minWidth: 1280,
        maxWidth: 1280,
        minHeight: 720,
        maxHeight: 720
      }
    }
  });
}

initDesktopStream();

// 处理WebSocket连接
wss.on('connection', (ws) => {
  console.log('客户端已连接');
  if (!desktopStream) {
    ws.send(JSON.stringify({ type: 'error', message: '桌面流未初始化' }));
    return;
  }

  // 创建隐藏的video元素用于捕获帧
  const videoElement = document.createElement('video');
  videoElement.srcObject = desktopStream;
  videoElement.play();

  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  canvas.width = 1280;
  canvas.height = 720;

  // 每33ms捕获一帧(约30fps),转成webp格式推送
  const intervalId = setInterval(() => {
    ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
    canvas.toBlob((blob) => {
      if (ws.readyState === WebSocket.OPEN) {
        ws.send(blob);
      }
    }, 'image/webp');
  }, 33);

  // 客户端断开时清理资源
  ws.on('close', () => {
    clearInterval(intervalId);
    videoElement.pause();
    videoElement.srcObject = null;
    console.log('客户端已断开连接');
  });
});

app.use(express.static('public'));

server.listen(3000, () => {
  console.log('服务已启动:http://localhost:3000');
});

前端页面(public/index.html)

<!DOCTYPE html>
<html>
<head>
  <title>低延迟桌面流</title>
</head>
<body>
  <video id="desktop-video" autoplay muted controls style="width: 100%;"></video>
  <script>
    const video = document.getElementById('desktop-video');
    const ws = new WebSocket('ws://localhost:3000');
    const mediaSource = new MediaSource();
    video.src = URL.createObjectURL(mediaSource);

    let sourceBuffer;

    // MediaSource就绪后创建源缓冲区
    mediaSource.addEventListener('sourceopen', () => {
      sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp9"');
    });

    // 接收WebSocket推送的帧数据
    ws.onmessage = (event) => {
      if (event.data instanceof Blob && !sourceBuffer.updating) {
        sourceBuffer.appendBuffer(event.data);
      }
    };

    ws.onclose = () => {
      mediaSource.endOfStream();
      console.log('WebSocket连接已关闭');
    };
  </script>
</body>
</html>

关键提醒

  • 绝对不要用fs.createReadStream处理MediaStream,二者的应用场景完全不同:前者针对本地静态文件,后者是内存中的实时媒体数据。
  • 方案一代码更简洁,适合大多数场景;方案二延迟更低,但需要处理帧编码和MediaSource的拼接逻辑,复杂度稍高。
  • 如果需要同步推送音频,记得在获取MediaStream时开启audio: true,并修改对应的MIME类型(比如video/webm; codecs=vp9,opus)。

内容的提问来源于stack exchange,提问作者Industrial Comet

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 09:29:22