Skip to main content

Webhooks

在 AWS Lambda 上渲染时,Remotion 可以发送 Webhooks 来通知您渲染结束的情况,无论是成功还是失败。本页面描述了 Webhook 负载和如何设置 Webhook API 端点。

请参考 renderMediaOnLambda() 文档以了解如何启用 Webhooks 触发渲染。

设置​

您需要设置一个带有 POST 请求处理程序的 API 端点。确保端点可访问并接受来自 AWS 的请求。

info

如果您在本地机器上运行 Webhook 端点(即在 localhost 上),您需要使用诸如 tunnelmole 这样的工具来设置一个公共反向代理,或者使用 ngrok 这样的流行闭源隧道工具。运行任一工具都将生成一个公共 URL,该 URL 将转发到您本地服务。

响应​

每个 Webhook 都具有以下标头:

json
{
"Content-Type": "application/json",
"X-Remotion-Mode": "production" | "demo",
"X-Remotion-Signature": "sha512=HASHED_SIGNATURE" | "NO_SECRET_PROVIDED",
"X-Remotion-Status": "success" | "timeout" | "error",
}
json
{
"Content-Type": "application/json",
"X-Remotion-Mode": "production" | "demo",
"X-Remotion-Signature": "sha512=HASHED_SIGNATURE" | "NO_SECRET_PROVIDED",
"X-Remotion-Status": "success" | "timeout" | "error",
}

您可以使用这些标头来验证请求的真实性,检查渲染过程的状态以及检查 Webhook 是否是从部署到 AWS 的生产代码调用的,还是从下面的工具或您自己的测试套件调用的演示应用。

请求正文具有以下结构:

ts
type WebhookPayload =
| {
type: 'error';
errors: {
message: string;
name: string;
stack: string;
}[];
renderId: string;
expectedBucketOwner: string;
bucketName: string;
customData: Record<string, unkown>;
}
| {
type: 'success';
lambdaErrors: EnhancedErrorInfo[];
outputUrl: string | undefined;
outputFile: string | undefined;
timeToFinish: number | undefined;
renderId: string;
expectedBucketOwner: string;
bucketName: string;
customData: Record<string, unkown>;
// Available from v3.3.11
costs: {
estimatedCost: number;
estimatedDisplayCost: string;
currency: string;
disclaimer: string;
};
}
| {
type: 'timeout';
renderId: string;
expectedBucketOwner: string;
bucketName: string;
customData: Record<string, unkown>;
};
ts
type WebhookPayload =
| {
type: 'error';
errors: {
message: string;
name: string;
stack: string;
}[];
renderId: string;
expectedBucketOwner: string;
bucketName: string;
customData: Record<string, unkown>;
}
| {
type: 'success';
lambdaErrors: EnhancedErrorInfo[];
outputUrl: string | undefined;
outputFile: string | undefined;
timeToFinish: number | undefined;
renderId: string;
expectedBucketOwner: string;
bucketName: string;
customData: Record<string, unkown>;
// Available from v3.3.11
costs: {
estimatedCost: number;
estimatedDisplayCost: string;
currency: string;
disclaimer: string;
};
}
| {
type: 'timeout';
renderId: string;
expectedBucketOwner: string;
bucketName: string;
customData: Record<string, unkown>;
};

字段 renderId、bucketName 将会被返回,就像 renderMediaOnLambda() 本身返回的一样。

您可以使用字段 customData 来设置一个 JSON 可序列化对象,这对于将自定义数据传递给 Webhook 端点非常有用。当序列化后,customData 字段的大小必须小于 1KB(1024 字节),否则会抛出错误。将较大的数据存储在 inputProps 中,并通过调用 getRenderProgress() 并读取 progress.renderMetadata.inputProps 来检索它。

如果渲染过程超时,响应体将不包含任何其他字段。

只有在渲染成功时才会返回 outputUrl、outputFile 和 timeToFinish 键。请注意,成功的渲染过程可能仍然存在非致命的 lambdaErrors:

json
{
"s3Location": "string",
"explanation": "string" | null,
"type": "renderer" | "browser" | "stitcher",
"message": "string",
"name": "string",
"stack": "string",
"frame": "number"| null,
"chunk": "number"| null,
"isFatal": "boolean",
"attempt": "number",
"willRetry": "boolean",
"totalAttempts": "number",
"tmpDir": {
"files": [{
"filename": "string",
"size": "number",
}],
"total": "number"
} | null,
}
json
{
"s3Location": "string",
"explanation": "string" | null,
"type": "renderer" | "browser" | "stitcher",
"message": "string",
"name": "string",
"stack": "string",
"frame": "number"| null,
"chunk": "number"| null,
"isFatal": "boolean",
"attempt": "number",
"willRetry": "boolean",
"totalAttempts": "number",
"tmpDir": {
"files": [{
"filename": "string",
"size": "number",
}],
"total": "number"
} | null,
}

errors 数组将包含在渲染过程中发生的任何 致命 错误的错误消息和堆栈跟踪。

验证 Webhooks​

如果您在 CLI 参数中提供了一个 webhook 密钥,Remotion 将对所有 webhook 请求进行签名。

warning

如果您没有提供密钥,X-Remotion-Signature 将被设置为 NO_SECRET_PROVIDED。无法验证使用 NO_SECRET_PROVIDED 签名发送的 webhook 请求的真实性和数据完整性。如果您想要验证传入的 webhooks,必须提供一个 webhook 密钥。

Remotion 使用 HMAC 与 SHA-512 算法 对其发送的 webhook 请求进行加密签名。这使您能够验证传入 webhook 请求的真实性和数据完整性。

为了验证一个 webhook 请求,您需要使用您提供的 webhook 密钥和请求体创建一个 SHA-512 HMAC 签名的十六进制摘要。如果它与 X-Remotion-Signature 头部匹配,则该请求确实是由 Remotion 发送的,其请求体是完整的。

如果不匹配,要么数据完整性受到损害且请求体不完整,要么该请求不是由 Remotion 发送的。

这是 Remotion 计算签名的方式:

javascript
import * as Crypto from "crypto";
function calculateSignature(payload: string, secret?: string) {
if (!secret) {
return "NO_SECRET_PROVIDED";
}
const hmac = Crypto.createHmac("sha512", secret);
const signature = "sha512=" + hmac.update(payload).digest("hex");
return signature;
}
javascript
import * as Crypto from "crypto";
function calculateSignature(payload: string, secret?: string) {
if (!secret) {
return "NO_SECRET_PROVIDED";
}
const hmac = Crypto.createHmac("sha512", secret);
const signature = "sha512=" + hmac.update(payload).digest("hex");
return signature;
}

在您的 webhook 端点中,payload 参数是请求体,secret 参数是您的 webhook 密钥。

您可以使用 validateWebhookSignature() 函数来验证签名是否有效,而无需自行验证签名。

示例 webhook 端点(Express)​

您可以使用任何 Web 框架和语言来设置您的 webhook 端点。以下示例是使用 Express 框架编写的 JavaScript 示例。

server.js
javascript
import express from "express";
import bodyParser from "body-parser";
import * as Crypto from "crypto";
import {
validateWebhookSignature,
WebhookPayload,
} from "@remotion/lambda/client";
const router = express();
// You'll need to add a JSON parser middleware globally or
// for the webhook route in order to get access to the request
// body.
const jsonParser = bodyParser.json();
// Enable testing through the tool below
const ENABLE_TESTING = true;
// Express API endpoint
router.post("/my-remotion-webhook-endpoint", jsonParser, (req, res) => {
if (ENABLE_TESTING) {
res.setHeader("Access-Control-Allow-Origin", "https://www.remotion.dev");
res.setHeader("Access-Control-Allow-Methods", "OPTIONS,POST");
res.setHeader(
"Access-Control-Allow-Headers",
"X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, X-Remotion-Status, X-Remotion-Signature, X-Remotion-Mode"
);
}
if (req.method === "OPTIONS") {
res.status(200).end();
return;
}
validateWebhookSignature({
signatureHeader: req.header("X-Remotion-Signature"),
body: req.body,
secret: process.env.WEBHOOK_SECRET as string
});
const status = req.header("X-Remotion-Status"); // success, timeout, error
const mode = req.header("X-Remotion-Mode"); // demo or production
const payload = JSON.parse(req.body) as WebhookPayload;
if (payload.type === "success") {
// ...
} else if (payload.type === "timeout") {
// ...
}
});
server.js
javascript
import express from "express";
import bodyParser from "body-parser";
import * as Crypto from "crypto";
import {
validateWebhookSignature,
WebhookPayload,
} from "@remotion/lambda/client";
const router = express();
// You'll need to add a JSON parser middleware globally or
// for the webhook route in order to get access to the request
// body.
const jsonParser = bodyParser.json();
// Enable testing through the tool below
const ENABLE_TESTING = true;
// Express API endpoint
router.post("/my-remotion-webhook-endpoint", jsonParser, (req, res) => {
if (ENABLE_TESTING) {
res.setHeader("Access-Control-Allow-Origin", "https://www.remotion.dev");
res.setHeader("Access-Control-Allow-Methods", "OPTIONS,POST");
res.setHeader(
"Access-Control-Allow-Headers",
"X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, X-Remotion-Status, X-Remotion-Signature, X-Remotion-Mode"
);
}
if (req.method === "OPTIONS") {
res.status(200).end();
return;
}
validateWebhookSignature({
signatureHeader: req.header("X-Remotion-Signature"),
body: req.body,
secret: process.env.WEBHOOK_SECRET as string
});
const status = req.header("X-Remotion-Status"); // success, timeout, error
const mode = req.header("X-Remotion-Mode"); // demo or production
const payload = JSON.parse(req.body) as WebhookPayload;
if (payload.type === "success") {
// ...
} else if (payload.type === "timeout") {
// ...
}
});

示例 Webhook 端点 (Next.JS 应用路由器)​

同样,在 Next.JS 中为应用路由器提供一个示例端点。

由于此端点将在 AWS Lambda 函数中独立执行,您需要从 @remotion/lambda/client 导入 Remotion 函数。

app/api/webhook.ts
tsx
import {
validateWebhookSignature,
WebhookPayload,
} from '@remotion/lambda/client';
 
// Enable testing through the tool below
// You may disable it in production
const ENABLE_TESTING = true;
 
export const POST = async (req: Request, res: Response) => {
let headers = {};
 
if (ENABLE_TESTING) {
const testingheaders = {
'Access-Control-Allow-Origin': 'https://www.remotion.dev',
'Access-Control-Allow-Headers':
'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, X-Remotion-Status, X-Remotion-Signature, X-Remotion-Mode',
'Access-Control-Allow-Methods': 'OPTIONS,POST',
};
headers = {...headers, ...testingheaders};
}
 
if (req.method === 'OPTIONS') {
return new Response(null, {
headers,
});
}
 
// Parse the body properly
const body = await req.json();
 
validateWebhookSignature({
secret: process.env.WEBHOOK_SECRET as string,
body: body,
signatureHeader: req.headers.get('X-Remotion-Signature') as string,
});
 
const payload = body as WebhookPayload;
 
if (payload.type === 'success') {
//...
} else if (payload.type === 'timeout') {
//...
}
 
return new Response(JSON.stringify({success: true}));
};
 
export const OPTIONS = POST;
app/api/webhook.ts
tsx
import {
validateWebhookSignature,
WebhookPayload,
} from '@remotion/lambda/client';
 
// Enable testing through the tool below
// You may disable it in production
const ENABLE_TESTING = true;
 
export const POST = async (req: Request, res: Response) => {
let headers = {};
 
if (ENABLE_TESTING) {
const testingheaders = {
'Access-Control-Allow-Origin': 'https://www.remotion.dev',
'Access-Control-Allow-Headers':
'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, X-Remotion-Status, X-Remotion-Signature, X-Remotion-Mode',
'Access-Control-Allow-Methods': 'OPTIONS,POST',
};
headers = {...headers, ...testingheaders};
}
 
if (req.method === 'OPTIONS') {
return new Response(null, {
headers,
});
}
 
// Parse the body properly
const body = await req.json();
 
validateWebhookSignature({
secret: process.env.WEBHOOK_SECRET as string,
body: body,
signatureHeader: req.headers.get('X-Remotion-Signature') as string,
});
 
const payload = body as WebhookPayload;
 
if (payload.type === 'success') {
//...
} else if (payload.type === 'timeout') {
//...
}
 
return new Response(JSON.stringify({success: true}));
};
 
export const OPTIONS = POST;

示例 Webhook 端点 (Next.JS 页面路由器)​

与上面相同的端点,但使用页面路由器。

pages/api/webhook.ts
tsx
import {
validateWebhookSignature,
WebhookPayload,
} from '@remotion/lambda/client';
 
// Enable testing through the tool below
// You may disable it in production
const ENABLE_TESTING = true;
 
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (ENABLE_TESTING) {
res.setHeader('Access-Control-Allow-Origin', 'https://www.remotion.dev');
res.setHeader('Access-Control-Allow-Methods', 'OPTIONS,POST');
res.setHeader(
'Access-Control-Allow-Headers',
'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, X-Remotion-Status, X-Remotion-Signature, X-Remotion-Mode',
);
}
if (req.method === 'OPTIONS') {
res.status(200).end();
return;
}
 
validateWebhookSignature({
secret: process.env.WEBHOOK_SECRET as string,
body: req.body,
signatureHeader: req.headers['x-remotion-signature'] as string,
});
 
// If code reaches this path, the webhook is authentic.
const payload = req.body as WebhookPayload;
if (payload.type === 'success') {
// ...
} else if (payload.type === 'timeout') {
// ...
}
 
res.status(200).json({
success: true,
});
}
pages/api/webhook.ts
tsx
import {
validateWebhookSignature,
WebhookPayload,
} from '@remotion/lambda/client';
 
// Enable testing through the tool below
// You may disable it in production
const ENABLE_TESTING = true;
 
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (ENABLE_TESTING) {
res.setHeader('Access-Control-Allow-Origin', 'https://www.remotion.dev');
res.setHeader('Access-Control-Allow-Methods', 'OPTIONS,POST');
res.setHeader(
'Access-Control-Allow-Headers',
'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, X-Remotion-Status, X-Remotion-Signature, X-Remotion-Mode',
);
}
if (req.method === 'OPTIONS') {
res.status(200).end();
return;
}
 
validateWebhookSignature({
secret: process.env.WEBHOOK_SECRET as string,
body: req.body,
signatureHeader: req.headers['x-remotion-signature'] as string,
});
 
// If code reaches this path, the webhook is authentic.
const payload = req.body as WebhookPayload;
if (payload.type === 'success') {
// ...
} else if (payload.type === 'timeout') {
// ...
}
 
res.status(200).json({
success: true,
});
}

测试您的 Webhook 端点​

您可以使用此工具验证您的 Webhook 端点是否正常工作。该工具将发送一个适当的演示负载并将响应记录到屏幕上。此工具发送的所有请求都将设置 "X-Remotion-Mode" 标头为 "demo"。

info

此工具直接从您的浏览器发送演示 Webhook 请求,这具有以下含义:

  • CORS 要求:
    • 确保您的 API 端点配置为接受来自 remotion.dev 的请求,方法是设置 "Access-Control-Allow-Origin": "https://www.remotion.dev"。这对于此工具工作是必要的,但对于您的生产 Webhook 端点则不是必需的。
    • 您必须设置 "Access-Control-Allow-Headers": "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, X-Remotion-Status, X-Remotion-Signature, X-Remotion-Mode"
    • 您必须设置 "Access-Control-Allow-Methods": "OPTIONS,POST"。
    • 阅读 DevTools 中的错误消息以调试潜在的 CORS 问题。
  • 您可以使用在 localhost 上监听的服务器,而无需使用反向代理。
info
Your webhook URL:
Your webhook secret:

另请参阅​