Remotion LabRemotion Lab
伺服器端渲染分散式渲染

分散式渲染

了解跨多台機器進行分散式影片渲染的原理,以及為何 Remotion Lambda 是推薦的分散式渲染解決方案

分散式渲染

如果您正在尋找將渲染分割成在不同機器上運行的分塊的方式,Remotion Lambda 是我們的推薦方案。

Remotion Lambda 解決了什麼問題?

Lambda 處理了您自行實現時需要面對的大量工程工作:

編排和分塊

  • 定義工作如何分塊,追蹤進度,合併分塊,儲存到雲端儲存
  • 透過單一連接串流傳輸分塊和進度,實現流暢的進度報告

環境打包

  • 打包 Chrome、Remotion 二進位文件、必要字體和自訂 Emoji 字體,以適應 Lambda 函數的限制

資源管理

  • 在函數調用後清理所有建立的資源,避免函數複用時的記憶體洩漏
  • 在函數調用之間保持瀏覽器實例開啟(以便下次調用快速就緒),同時解除引用以避免計費時錯誤統計

錯誤處理和除錯

  • 啟用日誌,按渲染和分塊分組,收集和呈現錯誤,符號化堆疊追蹤以輔助除錯 Lambda 渲染
  • 對幾乎所有 AWS 操作實現重試機制(調用函數、讀寫物件)
  • 警告錯誤的設定(資源、憑證、權限、載荷)

限制繞過

  • 透過將載荷儲存到 S3 來繞過 Lambda 載荷大小限制

媒體處理

  • 支援多種媒體類型:渲染影片、圖片、GIF 和音訊
  • 使用特殊選項無縫拼接影片和音訊,避免出現瑕疵

多語言客戶端

  • 提供 Node、Go、Ruby、Python 和 PHP 的客戶端

您可能不需要自定義分散式渲染器

我們認為 Lambda 在速度、成本、可擴展性和易用性之間取得了最佳平衡。

許多用戶為 Lambda 設定了過高的記憶體,導致渲染成本不必要地偏高。請參閱如何優化 Lambda 渲染成本

在繼續建立自己的分散式渲染方案之前,請考慮:

  • 您能節省多少錢,並與工程成本相比較
  • Lambda 函數在渲染完成後立即關閉所節省的費用
  • Lambda 的成熟度和開箱即用的功能

實現自定義分散式渲染器

如果您已決定需要自定義分散式渲染方案,以下是實現指南。Remotion Lambda 也遵循相同的架構。

架構概述

分散式渲染的關鍵概念是將渲染分為**協調者(Orchestrator)渲染者(Renderer)**兩個角色:

  • 協調者:負責分割工作、分配給渲染者、追蹤進度、合併結果
  • 渲染者:接收指定的幀範圍,執行實際渲染,返回結果

步驟 1:拆分工作

首先確定要渲染的影片長度:

import { selectComposition } from '@remotion/renderer';
 
const composition = await selectComposition({
  serveUrl: bundleLocation,
  id: compositionId,
  inputProps,
});
 
const totalFrames = composition.durationInFrames;

考慮 frameRangeeveryNthFrame 選項以確定實際渲染的幀數。

步驟 2:建立分塊計畫

const CHUNK_SIZE = 20; // 每個分塊的幀數
const chunks = [];
 
for (let i = 0; i < totalFrames; i += CHUNK_SIZE) {
  chunks.push({
    start: i,
    end: Math.min(i + CHUNK_SIZE - 1, totalFrames - 1),
    chunkIndex: chunks.length,
  });
}

步驟 3:並行渲染各分塊

import { renderMedia } from '@remotion/renderer';
 
const chunkPromises = chunks.map(async (chunk) => {
  await renderMedia({
    composition,
    serveUrl: bundleLocation,
    codec: 'h264',
    outputLocation: `chunks/chunk-${chunk.chunkIndex}.mp4`,
    frameRange: [chunk.start, chunk.end],
    inputProps,
  });
  return chunk;
});
 
await Promise.all(chunkPromises);

步驟 4:合併分塊

import { combineChunks } from '@remotion/renderer';
// 或使用 FFmpeg 合併
 
// 將所有分塊合併為最終影片
await combineChunks({
  chunks: chunks.map((c) => `chunks/chunk-${c.chunkIndex}.mp4`),
  output: 'out/final-video.mp4',
});

步驟 5:清理暫存文件

import { rm } from 'fs/promises';
 
// 刪除分塊文件
for (const chunk of chunks) {
  await rm(`chunks/chunk-${chunk.chunkIndex}.mp4`);
}

進階考量

失敗重試

每個分塊的渲染應具備重試機制:

async function renderChunkWithRetry(chunk, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      await renderMedia({ /* ... */ });
      return;
    } catch (err) {
      if (attempt === maxRetries - 1) throw err;
      console.log(`分塊 ${chunk.chunkIndex} 第 ${attempt + 1} 次重試`);
    }
  }
}

進度追蹤

const completedChunks = new Set();
 
await Promise.all(chunks.map(async (chunk) => {
  await renderChunkWithRetry(chunk);
  completedChunks.add(chunk.chunkIndex);
  console.log(`進度:${completedChunks.size}/${chunks.length}`);
}));

相關資源