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
相关产品推荐
相关产品推荐

