Remotion LabRemotion Lab
建構應用在 Electron 中使用 Remotion

在 Electron 中使用 Remotion

學習如何將 Remotion 整合到 Electron 桌面應用程式中,包括打包配置、二進位檔案管理和平台特定注意事項

使用官方範本作為參考。 你的最終設定將取決於你的打包工具、打包目標和平台特定需求。請參考官方 Electron 範本

使用本頁面了解 Electron 整合的注意事項,而不是作為嚴格的設定指南。不同應用程式的結構、安裝程式、程式碼簽署、自動更新和打包掛勾各不相同。

官方 Electron 範本是推薦的基準範本。

使用範本作為參考

範本展示了一個使用 Electron Forge 和 Vite 的打包渲染流程:

  • 將 Remotion 專案保留在 remotion/
  • 透過 IPC 從渲染器觸發渲染
  • 在 Electron 主程序中執行 selectComposition()renderMedia()
  • 在打包期間建構 Remotion bundle,而非在打包後的執行期建構

你的應用程式中確切的打包設定可能有所不同。

在主程序中渲染

渲染器不應直接呼叫 renderMedia()

preload.ts 暴露一個小型 API,然後讓 Electron 主程序執行渲染。這可以讓瀏覽器下載、檔案系統存取、儲存對話框和打包二進位路徑遠離你的 UI 程式碼。

preload.ts
import { contextBridge, ipcRenderer } from 'electron';
 
// 暴露給渲染器的 API
contextBridge.exposeInMainWorld('remotionAPI', {
  renderVideo: (options: {
    compositionId: string;
    inputProps: Record<string, unknown>;
    outputPath: string;
  }) => ipcRenderer.invoke('render-video', options),
 
  onRenderProgress: (
    callback: (progress: { percent: number; frame: number }) => void
  ) => {
    ipcRenderer.on('render-progress', (_event, data) => callback(data));
    return () => ipcRenderer.removeAllListeners('render-progress');
  },
});
main.ts
import { app, BrowserWindow, ipcMain, dialog } from 'electron';
import { bundle } from '@remotion/bundler';
import { renderMedia, selectComposition } from '@remotion/renderer';
import path from 'path';
 
let mainWindow: BrowserWindow | null = null;
 
app.whenReady().then(() => {
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false,
    },
  });
 
  // 處理渲染請求
  ipcMain.handle(
    'render-video',
    async (
      _event,
      options: {
        compositionId: string;
        inputProps: Record<string, unknown>;
        outputPath: string;
      }
    ) => {
      try {
        // 在開發環境中:動態建構 bundle
        // 在生產環境中:使用預建構的 bundle
        const bundleLocation =
          process.env.NODE_ENV === 'development'
            ? await bundle({
                entryPoint: path.resolve('./remotion/index.ts'),
              })
            : path.join(process.resourcesPath, 'remotion-bundle');
 
        const composition = await selectComposition({
          serveUrl: bundleLocation,
          id: options.compositionId,
          inputProps: options.inputProps,
        });
 
        await renderMedia({
          composition,
          serveUrl: bundleLocation,
          codec: 'h264',
          outputLocation: options.outputPath,
          onProgress: ({ progress }) => {
            mainWindow?.webContents.send('render-progress', {
              percent: Math.round(progress * 100),
            });
          },
        });
 
        return { success: true, outputPath: options.outputPath };
      } catch (error) {
        return { success: false, error: (error as Error).message };
      }
    }
  );
});

開發環境與打包應用程式

開發環境

在開發環境中,通常按需呼叫 bundle(),以便立即獲取 composition 的更改。

main.ts(開發環境)
import { bundle } from '@remotion/bundler';
 
// 開發環境:動態建構 bundle
const bundleLocation = await bundle({
  entryPoint: path.resolve('./remotion/index.ts'),
  webpackOverride: (config) => config,
});

打包應用程式

在打包應用程式中,不要在執行時呼叫 bundle()。在打包步驟期間建構 Remotion bundle,並從應用程式載入預建構的目錄。

package.ts(打包腳本)
import { bundle } from '@remotion/bundler';
import { copyRecursive } from 'fs-extra';
import path from 'path';
 
// 在打包期間預建構 bundle
const bundleLocation = await bundle({
  entryPoint: path.resolve('./remotion/index.ts'),
});
 
// 將 bundle 複製到資源目錄
await copyRecursive(
  bundleLocation,
  path.join('./app-build', 'resources', 'remotion-bundle')
);

組合器二進位檔案

使用 binariesDirectory 來指定打包渲染的路徑,並將其指向 app.asar.unpacked 中的組合器套件。

Remotion 二進位檔案不得從 app.asar 內部載入。

main.ts(打包應用程式)
import { renderMedia } from '@remotion/renderer';
import { app } from 'electron';
import path from 'path';
 
const binariesDirectory = app.isPackaged
  ? path.join(
      process.resourcesPath,
      'app.asar.unpacked',
      'node_modules',
      '@remotion',
      `compositor-${process.platform}-${process.arch}`
    )
  : undefined; // 開發環境使用預設路徑
 
await renderMedia({
  // ...
  binariesDirectory,
});

平台特定注意事項

瀏覽器下載

第一次渲染可能會下載 Chrome Headless Shell。

如果你的應用程式預期使用者在啟動後很快進行渲染,你可以提前呼叫 ensureBrowser(),例如在啟動後在背景呼叫。

main.ts(提前下載瀏覽器)
import { ensureBrowser } from '@remotion/renderer';
 
app.whenReady().then(async () => {
  // 在背景下載瀏覽器
  ensureBrowser().catch(console.error);
 
  // 繼續正常初始化...
  createWindow();
});

如果你想在打包版本中避免首次使用時下載,你也可以將瀏覽器視為打包資源。在打包期間呼叫 ensureBrowser(),將下載的瀏覽器複製到 app.asar.unpacked,並在打包版本的渲染中透過 browserExecutable 選項傳遞其路徑。

官方 Electron 範本預設禁用此功能。如果你想讓打包版本完全離線渲染,請在打包時設定 REMOTION_ELECTRON_PACKAGE_BROWSER=true。這將顯著增加最終應用程式的大小。

macOS 通用版本不支援此功能。

macOS 通用版本

通用打包需要在打包期間同時提供兩個 macOS 組合器套件(x64 和 arm64)。

如果你的打包流程從分離的 x64 和 arm64 應用程式包生成通用應用程式,通用合并步驟可能需要對特定架構的二進位檔案設置允許列表。Electron Forge 透過 packagerConfig.osxUniversal 來暴露此功能。

Linux 變體

在 Linux 上,除了 CPU 架構之外,你還可能需要針對基於 glibc 的發行版(如 Ubuntu 和 Debian)以及基於 musl 的發行版(如 Alpine)使用不同的二進位檔案。

Remotion 為這些環境提供了單獨的 Linux 組合器套件,例如 @remotion/compositor-linux-x64-gnu@remotion/compositor-linux-x64-musl

在打包期間,包含與你的目標 Linux 變體匹配的組合器套件,並將其放置到 app.asar.unpacked 中。

package.json(Linux 打包配置)
{
  "optionalDependencies": {
    "@remotion/compositor-linux-x64-gnu": "*",
    "@remotion/compositor-linux-x64-musl": "*",
    "@remotion/compositor-linux-arm64-gnu": "*"
  }
}

設定不同於範本的情況

Electron Forge 是一個很好的基準,但它不是唯一有效的設定。

你的應用程式可能使用 Electron Builder、不同的渲染器堆疊、自定義的預載 API 或不同的打包流程。重要的是要將 Remotion 渲染保留在主程序中,並明確指定打包的二進位路徑。

另請參閱