T-CREATOR

Electron スクリーンレコーダー/キャプチャツールを desktopCapturer で作る

Electron スクリーンレコーダー/キャプチャツールを desktopCapturer で作る

デスクトップアプリケーションで画面録画機能を実装したいと思ったことはありませんか。実は、Electron のdesktopCapturer API を使えば、比較的シンプルなコードでスクリーンレコーダーやキャプチャツールを作ることができます。

この記事では、Electron のdesktopCapturerを使って、実際に動作するスクリーンレコーダーを一から実装する方法を解説します。画面ソースの取得から録画の開始・停止、ファイル保存まで、実践的なコード例とともに詳しくご紹介しますね。

背景

デスクトップアプリでの画面キャプチャニーズ

近年、リモートワークやオンライン会議の普及により、画面共有や画面録画の需要が急増しています。オンライン教育、プレゼンテーション、チュートリアル動画の作成など、さまざまな場面で画面キャプチャ機能が必要とされるようになりました。

こうしたニーズに応えるため、多くの企業やエンジニアがデスクトップアプリケーションで画面録画機能を実装しようとしています。しかし、ネイティブ API を直接扱うのは難しく、プラットフォームごとの違いも大きな課題でした。

Electron の desktopCapturer とは

Electron は、Web 技術でデスクトップアプリを開発できるフレームワークです。その中でdesktopCapturer API は、画面全体やアプリケーションウィンドウのメディアストリームを取得するための機能を提供します。

この API を使うことで、Chromium のメディア API と連携し、画面の映像をキャプチャできます。さらに、取得したストリームはMediaRecorder API で録画したり、Canvas に描画して静止画として保存したりすることが可能です。

以下の図は、desktopCapturer を使った画面キャプチャの基本的な流れを示しています。

mermaidflowchart LR
  user["ユーザー"] -->|録画開始| electron["Electron<br/>アプリ"]
  electron -->|getSources| capturer["desktopCapturer"]
  capturer -->|画面リスト| electron
  electron -->|選択| stream["MediaStream"]
  stream -->|録画| recorder["MediaRecorder"]
  recorder -->|データ| file["ファイル保存"]
  file -->|完了通知| user

図で理解できる要点

  • desktopCapturer が利用可能な画面ソースを列挙
  • ユーザーが選択したソースから MediaStream を取得
  • MediaRecorder で録画してファイルとして保存

課題

クロスプラットフォーム対応の難しさ

画面キャプチャ機能を実装する際の最大の課題は、Windows、macOS、Linux といった異なるプラットフォームで一貫した動作を実現することです。各 OS はそれぞれ独自の画面キャプチャ API を持っており、直接扱うには専門知識が必要になります。

また、セキュリティやプライバシーの観点から、各 OS は画面録画に対して異なる権限管理を行っています。macOS では画面収録の許可が必要ですし、Windows でも特定のアプリケーションウィンドウのキャプチャには制限があるケースがあります。

メディアストリームの取り扱い

画面から取得したメディアストリームは、適切に処理しないとメモリリークやパフォーマンス低下を引き起こします。特に長時間の録画では、データの蓄積やエンコーディング処理が負荷になることがあります。

また、録画したデータをどの形式で保存するか、どのようにユーザーに提供するか、といった設計判断も必要です。WebM、MP4 など、ブラウザや OS でサポートされる形式を選択する必要があります。

ユーザー体験の設計

画面録画ツールとして使いやすくするには、以下のような機能が求められます。

#機能説明
1ソース選択 UIユーザーがキャプチャしたい画面やウィンドウを視覚的に選べるインターフェース
2録画制御開始・一時停止・停止などの直感的な操作
3プレビュー機能録画前に選択した画面が正しいか確認できる機能
4ファイル管理録画したファイルの保存場所指定や自動命名

以下の図は、ユーザーが画面録画ツールを使う際の典型的なフローを示しています。

mermaidflowchart TD
  start["アプリ起動"] --> select["画面ソース<br/>選択画面"]
  select --> preview["プレビュー<br/>確認"]
  preview -->|OK| record["録画開始"]
  preview -->|変更| select
  record --> recording["録画中"]
  recording --> stop["録画停止"]
  stop --> save["ファイル保存"]
  save --> done["完了"]

解決策

desktopCapturer API の活用

Electron のdesktopCapturerを使うことで、クロスプラットフォームで動作する画面キャプチャ機能を実装できます。この API は、レンダラープロセスから利用可能で、画面やウィンドウのリストを取得し、それらをメディアストリームとして扱えます。

主な利点は以下の通りです。

#利点詳細
1クロスプラットフォームWindows、macOS、Linux で同じコードが動作
2Web 標準 API 連携MediaRecorder、Canvas など標準 API と組み合わせ可能
3サムネイル取得各ソースのプレビュー画像を取得できる
4柔軟な選択画面全体、特定ウィンドウ、特定のディスプレイを選択可能

実装アーキテクチャ

画面録画アプリケーションの実装は、以下のような構造で設計します。

アーキテクチャの概要図

mermaidflowchart TB
  subgraph renderer["レンダラープロセス"]
    ui["UI<br/>コンポーネント"]
    capturer["desktopCapturer"]
    stream["MediaStream"]
    recorder["MediaRecorder"]
  end

  subgraph main["メインプロセス"]
    window["BrowserWindow"]
    file["ファイル保存<br/>処理"]
  end

  ui -->|getSources| capturer
  capturer -->|ソースリスト| ui
  ui -->|getUserMedia| stream
  stream -->|録画| recorder
  recorder -->|Blob データ| ui
  ui -->|IPC| main
  main -->|書き込み| file

図で理解できる要点

  • レンダラープロセスでメディア処理を完結
  • メインプロセスはファイル保存など特権操作のみ担当
  • IPC で必要最小限の通信を行う

セキュリティ設定

Electron 10 以降、セキュリティ強化のためdesktopCapturerの使用には明示的な有効化が必要です。BrowserWindow の作成時に、以下のようにwebPreferencesで許可します。

javascriptconst { app, BrowserWindow } = require('electron');

app.whenReady().then(() => {
  const mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: true,
      enableRemoteModule: false,
      // desktopCapturerを有効化
      desktopCapturer: true,
    },
  });
});

この設定により、レンダラープロセスからdesktopCapturer API にアクセスできるようになります。contextIsolationtrueにすることで、preload スクリプトを通じて安全に API を公開できますね。

具体例

プロジェクトのセットアップ

まず、Electron プロジェクトの基本構造を作成します。

package.json の設定

json{
  "name": "electron-screen-recorder",
  "version": "1.0.0",
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  },
  "dependencies": {
    "electron": "^28.0.0"
  }
}

Yarn を使ってインストールします。

bashyarn install

メインプロセスの実装

メインプロセス(main.js)では、アプリケーションウィンドウを作成し、desktopCapturer を有効化します。

メインプロセスの初期化

javascript// main.js
const {
  app,
  BrowserWindow,
  ipcMain,
  dialog,
} = require('electron');
const path = require('path');
const fs = require('fs');

let mainWindow;

// アプリケーション起動時の処理
app.whenReady().then(() => {
  createMainWindow();
});

メインウィンドウを作成する関数を定義します。

BrowserWindow の作成

javascriptfunction createMainWindow() {
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      nodeIntegration: false,
      contextIsolation: true,
      // desktopCapturerの使用を許可
      desktopCapturer: true,
    },
  });

  mainWindow.loadFile('index.html');

  // 開発時はDevToolsを開く
  mainWindow.webContents.openDevTools();
}

録画データの保存処理を IPC で受け取ります。

ファイル保存の IPC 処理

javascript// 録画データを保存するIPCハンドラー
ipcMain.handle('save-recording', async (event, buffer) => {
  try {
    // 保存先ダイアログを表示
    const { filePath } = await dialog.showSaveDialog({
      buttonLabel: '保存',
      defaultPath: `recording-${Date.now()}.webm`,
      filters: [
        { name: 'WebM Video', extensions: ['webm'] },
      ],
    });

    if (filePath) {
      // Bufferデータをファイルに書き込み
      await fs.promises.writeFile(
        filePath,
        Buffer.from(buffer)
      );
      return { success: true, filePath };
    }

    return { success: false };
  } catch (error) {
    console.error('保存エラー:', error);
    return { success: false, error: error.message };
  }
});

Preload スクリプトの実装

Preload スクリプトは、レンダラープロセスとメインプロセスの橋渡しをします。contextIsolation が有効な場合、ここで API を安全に公開します。

Preload スクリプトの基本構造

javascript// preload.js
const { contextBridge, ipcRenderer } = require('electron');

// レンダラープロセスに安全にAPIを公開
contextBridge.exposeInMainWorld('electronAPI', {
  // 画面ソースを取得
  getSources: async () => {
    const { desktopCapturer } = require('electron');
    return await desktopCapturer.getSources({
      types: ['window', 'screen'],
      thumbnailSize: { width: 320, height: 180 },
    });
  },

  // 録画データを保存
  saveRecording: (buffer) => {
    return ipcRenderer.invoke('save-recording', buffer);
  },
});

このスクリプトにより、レンダラープロセスからwindow.electronAPI経由で安全に Electron API を呼び出せます。

レンダラープロセス(HTML)の実装

ユーザーインターフェースを作成します。画面ソース選択、プレビュー、録画制御のボタンを配置します。

HTML の基本構造

html<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0"
    />
    <title>画面録画ツール</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <div class="container">
      <h1>画面録画ツール</h1>

      <!-- 録画制御ボタン -->
      <div class="controls">
        <button
          id="selectSourceBtn"
          class="btn btn-primary"
        >
          画面を選択
        </button>
        <button
          id="startBtn"
          class="btn btn-success"
          disabled
        >
          録画開始
        </button>
        <button
          id="stopBtn"
          class="btn btn-danger"
          disabled
        >
          録画停止
        </button>
      </div>
    </div>
  </body>
</html>

プレビュー表示用のビデオ要素を追加します。

ビデオプレビューエリア

html    <!-- プレビューエリア -->
    <div class="preview-area">
      <video id="preview" autoplay muted></video>
      <div id="recordingIndicator" class="recording-indicator hidden">
        ● REC
      </div>
    </div>

    <!-- ソース選択モーダル -->
    <div id="sourceModal" class="modal hidden">
      <div class="modal-content">
        <h2>キャプチャする画面を選択</h2>
        <div id="sourceList" class="source-list"></div>
        <button id="closeModal" class="btn">キャンセル</button>
      </div>
    </div>
  </div>

  <script src="renderer.js"></script>
</body>
</html>

レンダラープロセス(JavaScript)の実装

画面録画の核となる JavaScript ロジックを実装します。

初期変数と DOM 要素の取得

javascript// renderer.js
let mediaRecorder;
let recordedChunks = [];
let selectedSourceId = null;
let currentStream = null;

// DOM要素の取得
const selectSourceBtn = document.getElementById(
  'selectSourceBtn'
);
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const preview = document.getElementById('preview');
const sourceModal = document.getElementById('sourceModal');
const sourceList = document.getElementById('sourceList');
const closeModal = document.getElementById('closeModal');
const recordingIndicator = document.getElementById(
  'recordingIndicator'
);

画面ソース選択機能を実装します。

画面ソース選択処理

javascript// 画面ソース選択ボタンのクリック処理
selectSourceBtn.addEventListener('click', async () => {
  try {
    // 利用可能な画面ソースを取得
    const sources = await window.electronAPI.getSources();

    // ソースリストをクリア
    sourceList.innerHTML = '';

    // 各ソースをカード形式で表示
    sources.forEach((source) => {
      const sourceCard = document.createElement('div');
      sourceCard.className = 'source-card';
      sourceCard.innerHTML = `
        <img src="${source.thumbnail.toDataURL()}" alt="${
        source.name
      }">
        <p>${source.name}</p>
      `;

      // クリックで選択
      sourceCard.addEventListener('click', () => {
        selectSource(source.id);
        sourceModal.classList.add('hidden');
      });

      sourceList.appendChild(sourceCard);
    });

    // モーダルを表示
    sourceModal.classList.remove('hidden');
  } catch (error) {
    console.error('ソース取得エラー:', error);
    alert('画面ソースの取得に失敗しました');
  }
});

選択したソースから MediaStream を取得し、プレビューを表示します。

MediaStream の取得とプレビュー

javascript// 画面ソースを選択してストリームを取得
async function selectSource(sourceId) {
  selectedSourceId = sourceId;

  try {
    // 既存のストリームがあれば停止
    if (currentStream) {
      currentStream
        .getTracks()
        .forEach((track) => track.stop());
    }

    // MediaStreamを取得
    currentStream =
      await navigator.mediaDevices.getUserMedia({
        audio: false,
        video: {
          mandatory: {
            chromeMediaSource: 'desktop',
            chromeMediaSourceId: sourceId,
          },
        },
      });

    // プレビューに表示
    preview.srcObject = currentStream;
    preview.play();

    // 録画開始ボタンを有効化
    startBtn.disabled = false;

    console.log('画面ソースを選択しました:', sourceId);
  } catch (error) {
    console.error('MediaStream取得エラー:', error);
    alert('画面キャプチャの開始に失敗しました');
  }
}

録画開始処理を実装します。MediaRecorder を使って録画を行います。

録画開始処理

javascript// 録画開始ボタンのクリック処理
startBtn.addEventListener('click', () => {
  if (!currentStream) {
    alert('先に画面を選択してください');
    return;
  }

  // 録画データをリセット
  recordedChunks = [];

  // MediaRecorderを作成
  mediaRecorder = new MediaRecorder(currentStream, {
    mimeType: 'video/webm; codecs=vp9',
    videoBitsPerSecond: 3000000, // 3Mbps
  });

  // データが利用可能になったら保存
  mediaRecorder.ondataavailable = (event) => {
    if (event.data.size > 0) {
      recordedChunks.push(event.data);
    }
  };

  // 録画停止時の処理
  mediaRecorder.onstop = handleRecordingStop;

  // 録画開始
  mediaRecorder.start();

  // UIの更新
  startBtn.disabled = true;
  stopBtn.disabled = false;
  selectSourceBtn.disabled = true;
  recordingIndicator.classList.remove('hidden');

  console.log('録画を開始しました');
});

録画停止処理を実装します。

録画停止処理

javascript// 録画停止ボタンのクリック処理
stopBtn.addEventListener('click', () => {
  if (mediaRecorder && mediaRecorder.state !== 'inactive') {
    mediaRecorder.stop();

    // UIの更新
    startBtn.disabled = false;
    stopBtn.disabled = true;
    selectSourceBtn.disabled = false;
    recordingIndicator.classList.add('hidden');

    console.log('録画を停止しました');
  }
});

録画データをファイルとして保存する処理を実装します。

録画データの保存処理

javascript// 録画停止時の処理
async function handleRecordingStop() {
  // Blobデータを作成
  const blob = new Blob(recordedChunks, {
    type: 'video/webm',
  });

  // BlobをArrayBufferに変換
  const buffer = await blob.arrayBuffer();

  try {
    // メインプロセスに保存を依頼
    const result = await window.electronAPI.saveRecording(
      buffer
    );

    if (result.success) {
      alert(`録画を保存しました:\n${result.filePath}`);
    } else {
      alert('録画の保存がキャンセルされました');
    }
  } catch (error) {
    console.error('保存エラー:', error);
    alert('録画の保存に失敗しました');
  }
}

モーダルのクローズ処理を追加します。

モーダルクローズ処理

javascript// モーダルを閉じる
closeModal.addEventListener('click', () => {
  sourceModal.classList.add('hidden');
});

// ウィンドウ終了時にストリームをクリーンアップ
window.addEventListener('beforeunload', () => {
  if (currentStream) {
    currentStream
      .getTracks()
      .forEach((track) => track.stop());
  }
});

スタイルシートの実装

見栄えの良い UI を作成するため、CSS を追加します。

基本スタイル

css/* styles.css */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont,
    'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
  background: linear-gradient(
    135deg,
    #667eea 0%,
    #764ba2 100%
  );
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}

.container {
  background: white;
  border-radius: 12px;
  padding: 30px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
  max-width: 1000px;
  width: 100%;
}

ボタンとコントロールのスタイルを定義します。

ボタンスタイル

cssh1 {
  text-align: center;
  color: #333;
  margin-bottom: 30px;
  font-size: 28px;
}

.controls {
  display: flex;
  gap: 15px;
  justify-content: center;
  margin-bottom: 30px;
}

.btn {
  padding: 12px 24px;
  border: none;
  border-radius: 6px;
  font-size: 16px;
  cursor: pointer;
  transition: all 0.3s ease;
  font-weight: 600;
}

.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.btn-primary {
  background: #667eea;
  color: white;
}

.btn-success {
  background: #10b981;
  color: white;
}

.btn-danger {
  background: #ef4444;
  color: white;
}

.btn:not(:disabled):hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}

プレビューエリアのスタイルを追加します。

プレビューエリアスタイル

css.preview-area {
  position: relative;
  background: #000;
  border-radius: 8px;
  overflow: hidden;
  aspect-ratio: 16 / 9;
}

#preview {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.recording-indicator {
  position: absolute;
  top: 20px;
  right: 20px;
  background: #ef4444;
  color: white;
  padding: 8px 16px;
  border-radius: 20px;
  font-weight: bold;
  animation: pulse 1.5s infinite;
}

@keyframes pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.6;
  }
}

.hidden {
  display: none;
}

ソース選択モーダルのスタイルを定義します。

モーダルスタイル

css.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.8);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.modal-content {
  background: white;
  padding: 30px;
  border-radius: 12px;
  max-width: 800px;
  max-height: 80vh;
  overflow-y: auto;
  width: 90%;
}

.modal-content h2 {
  margin-bottom: 20px;
  color: #333;
}

.source-list {
  display: grid;
  grid-template-columns: repeat(
    auto-fill,
    minmax(250px, 1fr)
  );
  gap: 20px;
  margin-bottom: 20px;
}

.source-card {
  border: 2px solid #e5e7eb;
  border-radius: 8px;
  padding: 15px;
  cursor: pointer;
  transition: all 0.3s ease;
}

.source-card:hover {
  border-color: #667eea;
  transform: scale(1.05);
  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}

.source-card img {
  width: 100%;
  border-radius: 4px;
  margin-bottom: 10px;
}

.source-card p {
  text-align: center;
  color: #666;
  font-size: 14px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

アプリケーションの実行

すべてのファイルが準備できたら、アプリケーションを起動します。

bashyarn start

これで画面録画ツールが起動し、以下の操作が可能になります。

#操作説明
1画面選択利用可能な画面やウィンドウのサムネイルから選択
2プレビュー確認選択した画面がリアルタイムで表示される
3録画開始ワンクリックで録画スタート
4録画停止録画を終了し、保存ダイアログを表示

エラーハンドリングの強化

本番環境では、より詳細なエラーハンドリングを追加することをお勧めします。

権限エラーの処理

javascript// MediaStream取得時のエラー処理を強化
async function selectSource(sourceId) {
  selectedSourceId = sourceId;

  try {
    if (currentStream) {
      currentStream
        .getTracks()
        .forEach((track) => track.stop());
    }

    currentStream =
      await navigator.mediaDevices.getUserMedia({
        audio: false,
        video: {
          mandatory: {
            chromeMediaSource: 'desktop',
            chromeMediaSourceId: sourceId,
          },
        },
      });

    preview.srcObject = currentStream;
    preview.play();
    startBtn.disabled = false;
  } catch (error) {
    // エラーの種類に応じて適切なメッセージを表示
    let errorMessage = '画面キャプチャの開始に失敗しました';

    if (error.name === 'NotAllowedError') {
      errorMessage =
        '画面録画の権限が拒否されました。\n' +
        'システム設定で権限を許可してください。';
    } else if (error.name === 'NotFoundError') {
      errorMessage =
        '指定された画面ソースが見つかりませんでした。';
    } else if (error.name === 'NotReadableError') {
      errorMessage =
        '画面ソースにアクセスできません。\n' +
        '他のアプリケーションが使用中の可能性があります。';
    }

    console.error('MediaStream取得エラー:', error);
    alert(errorMessage);
  }
}

音声キャプチャの追加

画面だけでなく、システム音声も一緒に録音したい場合は、以下のように変更します。

音声付き録画の実装

javascriptasync function selectSource(sourceId) {
  selectedSourceId = sourceId;

  try {
    if (currentStream) {
      currentStream
        .getTracks()
        .forEach((track) => track.stop());
    }

    // ビデオストリームを取得
    const videoStream =
      await navigator.mediaDevices.getUserMedia({
        audio: false,
        video: {
          mandatory: {
            chromeMediaSource: 'desktop',
            chromeMediaSourceId: sourceId,
          },
        },
      });

    // オーディオストリームを取得
    const audioStream =
      await navigator.mediaDevices.getUserMedia({
        audio: {
          mandatory: {
            chromeMediaSource: 'desktop',
          },
        },
        video: false,
      });

    // ビデオとオーディオを結合
    currentStream = new MediaStream([
      ...videoStream.getVideoTracks(),
      ...audioStream.getAudioTracks(),
    ]);

    preview.srcObject = currentStream;
    preview.play();
    startBtn.disabled = false;
  } catch (error) {
    console.error('ストリーム取得エラー:', error);
    alert('画面・音声キャプチャの開始に失敗しました');
  }
}

音声キャプチャを含む場合、macOS ではマイクの権限も必要になることに注意が必要です。

プロジェクト構造の全体像

最終的なプロジェクトのファイル構造は以下のようになります。

textelectron-screen-recorder/
├── package.json
├── main.js           # メインプロセス
├── preload.js        # プリロードスクリプト
├── index.html        # UI
├── renderer.js       # レンダラープロセスのロジック
└── styles.css        # スタイルシート

この構成により、関心の分離が明確になり、保守性の高いコードベースが実現できます。

まとめ

Electron のdesktopCapturer API を使うことで、クロスプラットフォームで動作するスクリーンレコーダーを比較的簡単に実装できます。この記事では、画面ソースの取得から MediaStream の処理、MediaRecorder による録画、そしてファイル保存まで、実践的な実装方法を解説しました。

重要なポイントをまとめます。

#ポイント詳細
1desktopCapturer の有効化BrowserWindow の webPreferences で明示的に許可
2contextIsolation との連携preload スクリプトで安全に API を公開
3MediaRecorder の活用Web 標準 API で録画処理を実装
4エラーハンドリング権限エラーやソース取得失敗に対応
5ストリームの適切な管理メモリリークを防ぐためのクリーンアップ

この基本実装をベースに、以下のような機能拡張も可能です。

  • カウントダウンタイマーの追加
  • 録画中の一時停止・再開機能
  • ウェブカメラの映像をピクチャーインピクチャーで重ねる
  • 録画中の描画やアノテーション機能
  • カスタム録画設定(解像度、フレームレート、ビットレート)
  • クラウドストレージへの自動アップロード

Electron の強力な API 群を活用することで、Web の知識だけで本格的なデスクトップアプリケーションを構築できることがお分かりいただけたのではないでしょうか。ぜひ、この実装をベースに、独自の画面録画ツールを開発してみてください。

関連リンク