分散式渲染
了解跨多台機器進行分散式影片渲染的原理,以及為何 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;考慮 frameRange 和 everyNthFrame 選項以確定實際渲染的幀數。
步驟 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}`);
}));