Pull to refresh

How to build an Electron desktopCapturer screen picker dialog

Level of difficultyEasy

Since Electron's desktopCapturer API doesn't provide a desktop picker dialog, you need to implement it yourself.

Disclaimer: I don't want to focus on the UI part, so you can use whatever you prefer, such as React, Vue, etc. You can find a simple HTML UI layout in my index.html file.

Firstly, we need to go to our main.js file and import the modules we are going to use:

const {app, BrowserWindow, ipcMain, systemPreferences, desktopCapturer} = require('electron');
const util = require("electron-util");
const path = require('path');
const IS_OSX = process.platform === 'darwin';

The next step is to add the necessary methods for communication from the renderer process to the main process. Let's navigate to the preload.js file where we will utilize the contextBridge and ipcRenderer modules provided by Electron.

Since we only want to receive a single response from the main process, such as the result of a method call, we will use ipcRenderer.invoke.

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronApi', {
	main: {
		isOSX: () => process.platform === 'darwin',
		isWindows: () => process.platform === 'win32',
		isLinux: () => /linux/.test(process.platform),
		openScreenSecurity: () => ipcRenderer.invoke('electronMain:openScreenSecurity'),
		getScreenAccess: () => ipcRenderer.invoke('electronMain:getScreenAccess'),
		getScreenSources: () => ipcRenderer.invoke('electronMain:screen:getSources'),

We have created three channels* that are responsible for sending messages to the Main process:

  • electronMain:openScreenSecurity

  • electronMain:getScreenAccess

  • electronMain:screen:getSources

*The names of the channels can be customized as per your preference.

The next step is to create handlers in the Main process to handle messages from the Renderer process.

Let's navigate back to the main.js file and add the necessary handlers.

ipcMain.handle('electronMain:openScreenSecurity', () => util.openSystemPreferences('security', 'Privacy_ScreenCapture'));
ipcMain.handle('electronMain:getScreenAccess', () => !IS_OSX || systemPreferences.getMediaAccessStatus('screen') === 'granted');
ipcMain.handle('electronMain:screen:getSources', () => {
    return desktopCapturer.getSources({types: ['window', 'screen']}).then(async sources => {
        return sources.map(source => {
            source.thumbnailURL = source.thumbnail.toDataURL();
            return source;

Now that everything is set up for communication from the Renderer process, let's move to our renderer.js file.

The main magic happens in the getDisplayMedia() function. We request the Main process for ScreenAccess permission, then we request ScreenSources and display our modal Screen Picker with the available sources.

function getDisplayMedia() {
	if (main.isOSX()) {
		screenPickerOptions.system_preferences = true;

	return new Promise(async (resolve, reject) => {
		let has_access = await main.getScreenAccess();
		if (!has_access) {
			return reject('none');

		try {
			const sources = await main.getScreenSources();
			screenPickerShow(sources, async (id) => {
				try {
					const source = sources.find(source => source.id === id);
					if (!source) {
						return reject('none');

					const stream = await window.navigator.mediaDevices.getUserMedia({
						audio: false,
						video: {
							mandatory: {
								chromeMediaSource: 'desktop',
								chromeMediaSourceId: source.id
				catch (err) {
			}, {});
		catch (err) {

Simple and it will work on all platforms.

You can find the complete code on GitHub.

Good luck and have fun!

You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.