Remotion LabRemotion Lab
LambdaLambda Webhooks

Lambda Webhooks

說明如何在 Remotion Lambda 中設定 Webhook 通知,包含 Webhook 配置、Payload 格式、簽名驗證與錯誤處理。

Lambda Webhooks

在 AWS Lambda 上渲染時,Remotion 可以傳送 webhook 通知,讓你知道渲染何時結束——無論是成功完成或發生錯誤。

若要了解如何在觸發渲染時啟用 webhook,請參閱 renderMediaOnLambda() 文件中的 webhook 部分

設定

你需要建立一個接受 POST 請求的 API 端點。請確保該端點可從 AWS 存取。

本地開發提示:若你的 webhook 端點運行在本地端(localhost),你需要使用反向代理工具建立公開 URL,例如:

執行這些工具後,會產生一個公開 URL,並將流量轉發至你本地的服務。

請求格式

請求標頭

每個 webhook 請求都包含以下標頭:

{
  "Content-Type": "application/json",
  "X-Remotion-Mode": "production | demo",
  "X-Remotion-Signature": "sha512=HASHED_SIGNATURE | NO_SECRET_PROVIDED",
  "X-Remotion-Status": "success | timeout | error"
}
  • X-Remotion-Mode:用於辨別請求來自生產環境或測試工具
  • X-Remotion-Signature:用於驗證請求的真實性(需設定 webhook 密鑰)
  • X-Remotion-Status:渲染的最終狀態

請求主體型別定義

type StaticWebhookPayload = {
  renderId: string;
  expectedBucketOwner: string;
  bucketName: string;
  customData: Record<string, unknown> | null;
};
 
// 渲染錯誤時
type WebhookErrorPayload = StaticWebhookPayload & {
  type: 'error';
  errors: {
    message: string;
    name: string;
    stack: string;
  }[];
};
 
// 渲染成功時
type WebhookSuccessPayload = StaticWebhookPayload & {
  type: 'success';
  lambdaErrors: EnhancedErrorInfo[];
  outputUrl: string | undefined;
  outputFile: string | undefined;
  timeToFinish: number | undefined;
  costs: AfterRenderCost;
};
 
// 渲染逾時時
type WebhookTimeoutPayload = StaticWebhookPayload & {
  type: 'timeout';
};
 
type WebhookPayload =
  | WebhookErrorPayload
  | WebhookSuccessPayload
  | WebhookTimeoutPayload;

customData 欄位

你可以在 customData 中傳入任何 JSON 可序列化的物件,方便將自訂資料帶入 webhook 端點。

重要限制customData 序列化後必須小於 1KB(1024 位元組),否則會拋出錯誤。若需要傳遞較大的資料,請將其存入 inputProps,並透過呼叫 getRenderProgress() 讀取 progress.renderMetadata.inputProps 來取得。

非致命錯誤

一次成功的渲染過程仍可能包含非致命的 lambdaErrors,其結構如下:

{
  "s3Location": "string",
  "explanation": "string | null",
  "type": "renderer | browser | stitcher",
  "message": "string",
  "name": "string",
  "stack": "string",
  "frame": "number | null",
  "chunk": "number | null",
  "isFatal": false,
  "attempt": "number",
  "willRetry": "boolean",
  "totalAttempts": "number"
}

errors 陣列則包含渲染過程中任何致命錯誤的訊息與堆疊追蹤。

驗證 Webhook 簽名

若在 CLI 參數中提供了 webhook 密鑰,Remotion 會對所有 webhook 請求進行簽名。

警告:若未提供密鑰,X-Remotion-Signature 將被設為 NO_SECRET_PROVIDED。在這種情況下,無法驗證 webhook 請求的真實性與資料完整性。若要驗證傳入的 webhook,必須提供 webhook 密鑰。

Remotion 使用 HMAC 搭配 SHA-512 演算法 對 webhook 請求進行密碼學簽名。

觸發渲染時設定 Webhook

import { renderMediaOnLambda } from '@remotion/lambda/client';
 
await renderMediaOnLambda({
  region: 'us-east-1',
  functionName: 'remotion-render-bds9aab',
  serveUrl: 'https://remotionlambda-example.s3.amazonaws.com/sites/my-site/index.html',
  composition: 'MyVideo',
  inputProps: { title: '範例影片' },
  codec: 'h264',
  webhook: {
    url: 'https://your-api.example.com/webhook',
    secret: process.env.WEBHOOK_SECRET!,
    customData: {
      userId: '123',
      orderId: 'abc456',
    },
  },
});

在 Node.js 中驗證簽名

import { validateWebhookSignature } from '@remotion/lambda/client';
 
// Express / Next.js API 路由範例
export async function POST(request: Request) {
  const body = await request.text();
  const signature = request.headers.get('X-Remotion-Signature') ?? '';
 
  try {
    validateWebhookSignature({
      secret: process.env.WEBHOOK_SECRET!,
      body,
      signatureHeader: signature,
    });
  } catch (err) {
    // 簽名驗證失敗,拒絕請求
    return new Response('Unauthorized', { status: 401 });
  }
 
  const payload = JSON.parse(body) as WebhookPayload;
 
  if (payload.type === 'success') {
    console.log('渲染成功!', payload.outputUrl);
    // 處理成功邏輯...
  } else if (payload.type === 'error') {
    console.error('渲染失敗', payload.errors);
    // 處理錯誤邏輯...
  } else if (payload.type === 'timeout') {
    console.warn('渲染逾時', payload.renderId);
    // 處理逾時邏輯...
  }
 
  return new Response('OK', { status: 200 });
}

Next.js API 路由完整範例

// app/api/remotion-webhook/route.ts
import { validateWebhookSignature, WebhookPayload } from '@remotion/lambda/client';
 
export async function POST(request: Request) {
  const body = await request.text();
  const signature = request.headers.get('X-Remotion-Signature') ?? '';
  const status = request.headers.get('X-Remotion-Status');
 
  // 驗證 webhook 簽名
  try {
    validateWebhookSignature({
      secret: process.env.WEBHOOK_SECRET!,
      body,
      signatureHeader: signature,
    });
  } catch {
    return Response.json({ error: 'Invalid signature' }, { status: 401 });
  }
 
  const payload: WebhookPayload = JSON.parse(body);
 
  switch (payload.type) {
    case 'success':
      // 更新資料庫,通知使用者
      await updateRenderStatus(payload.renderId, 'completed', payload.outputUrl);
      break;
 
    case 'error':
      // 記錄錯誤,通知管理員
      await updateRenderStatus(payload.renderId, 'failed');
      console.error('渲染錯誤:', payload.errors);
      break;
 
    case 'timeout':
      // 標記為逾時,可能需要重新嘗試
      await updateRenderStatus(payload.renderId, 'timeout');
      break;
  }
 
  return Response.json({ received: true });
}
 
async function updateRenderStatus(
  renderId: string,
  status: string,
  outputUrl?: string
) {
  // 你的資料庫更新邏輯
}

參閱