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

macOS下Electron生产版desktopCapturer无法获取桌面视频流问题

问题:Electron macOS生产构建中desktopCapturer流空白白屏

我在Electron中使用desktopCapturer API,Windows系统下本地开发和最终exe应用都正常工作。但在macOS系统中,仅开发/本地运行时能显示流,生成的dmg应用显示空白白屏。具体表现为:Mac本地能查看/控制屏幕,但其他平台无法查看Mac屏幕(仅生产构建有问题,开发环境正常)。

已尝试添加包含NSScreenCaptureUsageDescription的Info.plist文件,启动生产应用时也弹出了辅助功能设置弹窗,我已勾选该应用获取屏幕访问权限,但问题依旧。

屏幕共享核心代码

import "./styles.css";
import { useState, useRef, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import JoinCode from "./joincode";
import Peer from "simple-peer";
import FileShare from "../fileSharing/index";
import { Button } from "antd";
import Request from "../../request";

function SharingPage(props) {
  const { connectionRef, yourRoomId, answerCallDATA } = useSelector(
    (state) => ({
      connectionRef: state.global.connectionRef,
      yourRoomId: state.global.yourRoomId,
      answerCallDATA: state.global.answerCallDATA,
    })
  );
  const [roomId, setRoomId] = useState(null);
  const [resolution, setResolution] = useState({
    width: null,
    height: null,
  });
  const [Stream, setStream] = useState();
  const [receivingCall, setReceivingCall] = useState(false);
  const [caller, setCaller] = useState("");
  const [clearConnection, setClearConnection] = useState(false);
  const [callerSignal, setCallerSignal] = useState();
  const [callAccept, setCallAccept] = useState(false);
  const [callAccepted, setCallAccepted] = useState(false);
  const [showModal, setShowModal] = useState(false);
  const [cancelSession, setCancelSession] = useState(false);
  const videoData = useRef();

  const dispatch = useDispatch();
  useEffect(() => {
    startShare();
  }, []);

  useEffect(() => {
    setReceivingCall(false);
  }, [yourRoomId]);

  useEffect(async () => {
    let { success, data } = await Request.listCSessions({
      socketid: yourRoomId,
    });
    if (success && data) {
      await Request.updateCSessions({
        socketid: yourRoomId,
        current_plan_id: answerCallDATA?.current_plan_id,
      });
    }
  }, [callAccepted]);

  useEffect(() => {
    window.ipcRenderer.on("stream", async (event, value) => {
      const stream = await navigator.mediaDevices.getUserMedia({
        audio: false,
        video: {
          mandatory: {
            chromeMediaSource: "desktop",
            chromeMediaSourceId: value?.sourceId,
            minWidth: value?.videoResolution?.width,
            maxWidth: value?.videoResolution?.width,
            minHeight: value?.videoResolution?.height,
            maxHeight: value?.videoResolution?.height,
          },
        },
      });
      setStream(stream);
      setRoomId(value?.userId);
      dispatch({ type: "SET_USER_ROOMID", payload: value?.userId });
    });

    window.ipcRenderer.on("answerCall", async (event, data) => {
      setReceivingCall(true);
      setCaller(data.from);
      dispatch({ type: "SET_SOCKET_ID", payload: data.from });
      dispatch({ type: "SET_ANSWERCALL_DATA", payload: data });
      setCallerSignal(data.signal);
      setResolution({
        width: data.screenResolution.width,
        height: data.screenResolution.height,
      });
    });

    window.ipcRenderer.on("CallConnectDisconnnect", async (event, data) => {
      if (data == "Accept") {
        setCallAccept(true);
        setCancelSession(false);
      } else if (data == "Disconnect") {
        setCallAccept(false);
        setCancelSession(true);
      } else if (data == "Reject") {
        setReceivingCall(false);
        setCallAccept(false);
        setCancelSession(true);
        dispatch({ type: "INCOMING_ACCEPTED", payload: false });
        window.ipcRenderer.send("closedSession", {
          room: caller,
          Reject: true,
        });
      }
    });

    window.ipcRenderer.on("socketDisconneted", (event, value) => {
      setCancelSession(true);
    });

    window.ipcRenderer.on("clearConnection", (event, value) => {
      setClearConnection(true);
    });

    return () => {
      window.ipcRenderer.removeAllListeners("stream");
      window.ipcRenderer.removeAllListeners("answerCall");
      window.ipcRenderer.removeAllListeners("CallConnectDisconnnect");
      window.ipcRenderer.removeAllListeners("clearConnection");
    };
  });

  useEffect(() => {
    setReceivingCall(false);
    dispatch({ type: "SET_PEER_CONNECTION", payload: null });
    dispatch({ type: "INCOMING_ACCEPTED", payload: false });
    dispatch({ type: "SET_SOCKET_ID", payload: null });
    setStream();
  }, [clearConnection]);

  const startShare = () => {
    window.ipcRenderer.send("start-share", {});
  };

  const accRej = () => {
    setShowModal(true);
  };

  useEffect(() => {
    if (roomId != null) {
      setInterval(() => {
        window.ipcRenderer.send("clipBoard", { roomId });
      }, 2000);
    }
  }, [roomId]);

  useEffect(() => {
    if (receivingCall == true) {
      window.ipcRenderer.send("Accept-Call", {});
    }
  }, [receivingCall]);

  useEffect(() => {
    if (callAccept == true) {
      answerCall();
      setReceivingCall(false);
      setCallAccept(false);
    }
  }, [callAccept]);

  useEffect(() => {
    if (cancelSession == true) {
      window.ipcRenderer.send("closedSession", true);
      setReceivingCall(false);
      connectionRef?.destroy();
      dispatch({ type: "SET_PEER_CONNECTION", payload: null });
      dispatch({ type: "INCOMING_ACCEPTED", payload: false });
      setStream();
      dispatch({ type: "SET_FILE_RECEIVED", payload: false });
    }
  }, [cancelSession]);

  const answerCall = () => {
    const peer = new Peer({
      initiator: false,
      trickle: false,
      stream: Stream,
    });
    peer.on("signal", (data) => {
      window.ipcRenderer.send("answerInCall", {
        signal: data,
        from: yourRoomId,
        to: caller,
        resolution: resolution,
      });
    });
    peer.on("stream", (stream) => {
      // videoData.current.srcObject = Stream;
    });

    peer.on("connect", (msg) => {
      setCallAccepted(true);
      setCancelSession(false);
      dispatch({ type: "INCOMING_ACCEPTED", payload: true });
      dispatch({ type: "SET_PEER_CONNECTION", payload: peer });

      videoData.current.srcObject = Stream;
    });

    peer._pc.ondatachannel = (data) => {
      if (data.channel.label == "mouseChannel") {
        data.channel.onmessage = (event) => {
          window.ipcRenderer.send("mouseCoords", event.data);
        };
      }
      if (data.channel.label == "mouseClickChannel") {
        data.channel.onmessage = (event) => {
          window.ipcRenderer.send("mouse-click", event.data);
        };
      }
      if (data.channel.label == "keyChannel") {
        data.channel.onmessage = (event) => {
          window.ipcRenderer.send("key-up", event.data);
        };
      }
    };

    peer._pc.onconnectionstatechange = (data) => {
      if (peer._pc.connectionState == "connected") {
        setCallAccepted(true);
        setCancelSession(false);
        dispatch({ type: "INCOMING_ACCEPTED", payload: true });
        dispatch({ type: "SET_PEER_CONNECTION", payload: peer });

        videoData.current.srcObject = Stream;
      }
      if (peer._pc.connectionState == "disconnected") {
        setCallAccepted(false);
        setCancelSession(true);
        window.ipcRenderer.send("closedSession", true);
        setStream();
        dispatch({ type: "SET_FILE_RECEIVED", payload: false });
        dispatch({ type: "SET_FILER_DATA", payload: 0 });
      }
    };

    peer.on("error", (err) => {
      console.log(err, "erroorrr");
      setStream();
      dispatch({ type: "SET_FILE_RECEIVED", payload: false });
      dispatch({ type: "SET_FILER_DATA", payload: 0 });
    });

    peer.signal(callerSignal);
  };
  return (
    <>
      <video
        ref={videoData}
        autoPlay
        style={{
          opacity: 0.6,
          zIndex: -1,
          width: "90vw",
          height: "90vh",
          position: "absolute",
          padding: 0,
        }}
      ></video>
    </>
  );
}

export default SharingPage;

可能的原因及排查方案

1. 权限配置未正确嵌入生产包

虽然你添加了Info.plist,但打包工具(如electron-builder)可能未将权限描述正确写入最终应用的Info.plist中:

  • 检查package.json的build配置,确保mac字段包含权限声明:
    "build": {
      "mac": {
        "extendInfo": {
          "NSScreenCaptureUsageDescription": "需要屏幕录制权限以实现屏幕共享",
          "NSAccessibilityUsageDescription": "需要辅助功能权限以实现远程控制"
        }
      }
    }
    
  • 手动打开dmg包内的Contents/Info.plist,确认是否存在上述权限键值对。

2. 生产构建沙箱限制

Electron生产构建默认启用沙箱,会限制desktopCapturer的访问权限:

  • 在主进程的BrowserWindow配置中禁用沙箱(如果不需要):
    const mainWindow = new BrowserWindow({
      webPreferences: {
        sandbox: false,
        // 其他配置
      }
    });
    
  • 若必须启用沙箱,需在沙箱配置中添加screenCapture权限。

3. getUserMedia参数兼容性问题

代码中使用的mandatory约束是旧版Chrome语法,生产环境的Chromium版本可能不兼容:

  • 替换为标准约束语法:
    const stream = await navigator.mediaDevices.getUserMedia({
      audio: false,
      video: {
        width: { exact: value?.videoResolution?.width },
        height: { exact: value?.videoResolution?.height },
        chromeMediaSource: "desktop",
        chromeMediaSourceId: value?.sourceId
      }
    });
    

4. IPC权限与上下文隔离问题

生产构建中若启用contextIsolation,直接使用window.ipcRenderer会失效:

  • 确保通过preload脚本暴露IPC接口,而非直接在渲染进程调用:
    // preload.js
    const { contextBridge, ipcRenderer } = require('electron');
    contextBridge.exposeInMainWorld('api', {
      startShare: () => ipcRenderer.send('start-share'),
      onStream: (callback) => ipcRenderer.on('stream', callback)
    });
    
  • 主进程中调用desktopCapturer.getSources()时,需确保返回的sourceId正确传递到渲染进程。

5. macOS权限未真正生效

macOS有时会出现权限设置不生效的情况,尤其是应用签名后:

  • 前往系统设置 > 隐私与安全性 > 屏幕录制,移除应用后重新启动并授权。
  • 检查应用是否正确签名,未签名的应用会被系统限制权限。

6. WebRTC流传递时机问题

生产环境中Stream可能未就绪就初始化SimplePeer:

  • 在answerCall函数中添加流就绪检查:
    if (!Stream) {
      console.error('屏幕流未就绪');
      return;
    }
    const peer = new Peer({
      initiator: false,
      trickle: false,
      stream: Stream,
    });
    
  • 给getUserMedia添加错误捕获,排查生产环境中是否获取流失败:
    try {
      const stream = await navigator.mediaDevices.getUserMedia({...});
      setStream(stream);
    } catch (err) {
      console.error('获取屏幕流失败:', err);
    }
    

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 03:22:33