如何使用Express和FFmpeg.wasm在Node.js中构建媒体处理API

来自菜鸟教程
跳转至:导航、​搜索

作为 Write for DOnations 计划的一部分,作者选择了 Electronic Frontier Foundation 来接受捐赠。

介绍

处理媒体资产正在成为现代后端服务的普遍要求。 当您处理大规模或执行昂贵的操作(例如视频转码)时,使用基于云的专用解决方案可能会有所帮助。 但是,当您只需要从视频中提取缩略图或检查用户生成的内容的格式是否正确时,额外的成本和增加的复杂性可能很难证明是合理的。 特别是在规模较小的情况下,将媒体处理能力直接添加到 Node.js API 是有意义的。

在本指南中,您将使用 Expressffmpeg.wasm 在 Node.js 中构建一个媒体 API — 流行媒体处理工具的 WebAssembly 端口。 您将构建一个从视频中提取缩略图的端点作为示例。 您可以使用相同的技术将 FFmpeg 支持的其他功能添加到您的 API。

完成后,您将很好地掌握在 Express 中处理二进制数据并使用 ffmpeg.wasm 处理它们。 您还将处理向 API 发出的无法并行处理的请求。

先决条件

要完成本教程,您需要:

  • Node.js 的本地开发环境。 关注【X7X】如何安装Node.js并创建本地开发环境【X76X】。
  • 熟悉在 Node.js 中使用 Express 构建 API。 请参阅 如何开始使用 Node.js 和 Express
  • 具有使用 HTML 和 JavaScript 构建网站的经验。 您可以查看教程 如何将 JavaScript 添加到 HTML 以查看在 HTML 中放置 JavaScript。
  • 一个 视频 下载以测试您的实现。

本教程已使用 Node v16.11.0、npm v7.15.1、express v4.17.1 和 ffmpeg.wasm v0.10.1 进行了验证。

第 1 步 — 设置项目并创建 Basic Express 服务器

在这一步中,您将创建一个项目目录,初始化 Node.js 并安装 ffmpeg,并设置一个基本的 Express 服务器。

首先打开终端并为项目创建一个新目录:

mkdir ffmpeg-api

导航到新目录:

cd ffmpeg-api

使用 npm init 创建一个新的 package.json 文件。 -y 参数表示您对项目的默认设置感到满意。

npm init -y

最后,使用 npm install 安装构建 API 所需的包。 --save 标志表示您希望将这些作为依赖项保存在 package.json 文件中。

npm install --save @ffmpeg/ffmpeg @ffmpeg/core express cors multer p-queue

现在您已经安装了 ffmpeg,您将设置一个使用 Express 响应请求的 Web 服务器。

首先,使用 nano 或您选择的编辑器打开一个名为 server.mjs 的新文件:

nano server.mjs

此文件中的代码将注册 cors 中间件,该中间件将允许来自具有 不同来源 的网站的请求。 在文件顶部,导入 expresscors 依赖项:

服务器.mjs

import express from 'express';
import cors from 'cors';

然后,通过在 import 语句下方添加以下代码,创建一个 Express 应用程序并在端口 :3000 上启动服务器:

服务器.mjs

...
const app = express();
const port = 3000;

app.use(cors());

app.listen(port, () => {
    console.log(`[info] ffmpeg-api listening at http://localhost:${port}`)
});

您可以通过运行以下命令来启动服务器:

node server.mjs

您将看到以下输出:

Output[info] ffmpeg-api listening at http://localhost:3000

当您尝试在浏览器中加载 http://localhost:3000 时,您会看到 Cannot GET /。 这是 Express 告诉您它正在侦听请求。

现在设置您的 Express 服务器后,您将创建一个客户端来上传视频并向您的 Express 服务器发出请求。

    1. 第 2 步 — 创建客户端并测试服务器

在本节中,您将创建一个网页,让您可以选择一个文件并将其上传到 API 进行处理。

首先打开一个名为 client.html 的新文件:

nano client.html

在您的 client.html 文件中,创建一个文件输入和一个 Create Thumbnail 按钮。 下面,添加一个空的 <div> 元素以显示错误,并添加一个图像,该图像将显示 API 发回的缩略图。 在 <body> 标签的最后,加载一个名为 client.js 的脚本。 您的最终 HTML 模板应如下所示:

客户端.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Create a Thumbnail from a Video</title>
    <style>
        #thumbnail {
            max-width: 100%;
        }
    </style>
</head>
<body>
    <div>
        <input id="file-input" type="file" />
        <button id="submit">Create Thumbnail</button>
        <div id="error"></div>
        <img id="thumbnail" />
    </div>
    <script src="client.js"></script>
</body>
</html>

请注意,每个元素都有一个唯一的 id。 在引用 client.js 脚本中的元素时,您将需要它们。 #thumbnail 元素的样式用于确保图像在加载时适合屏幕。

保存 client.html 文件并打开 client.js

nano client.js

在您的 client.js 文件中,首先定义存储对您创建的 HTML 元素的引用的变量:

客户端.js

const fileInput = document.querySelector('#file-input');
const submitButton = document.querySelector('#submit');
const thumbnailPreview = document.querySelector('#thumbnail');
const errorDiv = document.querySelector('#error');

然后,将单击事件侦听器附加到 submitButton 变量以检查您是否选择了文件:

客户端.js

...
submitButton.addEventListener('click', async () => {
    const { files } = fileInput;
}

接下来,创建一个函数 showError(),它会在未选择文件时输出错误消息。 在事件监听器上方添加 showError() 函数:

客户端.js

const fileInput = document.querySelector('#file-input');
const submitButton = document.querySelector('#submit');
const thumbnailPreview = document.querySelector('#thumbnail');
const errorDiv = document.querySelector('#error');

function showError(msg) {
    errorDiv.innerText = `ERROR: ${msg}`;
}

submitButton.addEventListener('click', async () => {
...

现在,您将构建一个函数 createThumbnail(),它将向 API 发出请求、发送视频并接收缩略图作为响应。 在 client.js 文件的顶部,使用指向 /thumbnail 端点的 URL 定义一个新常量:

const API_ENDPOINT = 'http://localhost:3000/thumbnail';

const fileInput = document.querySelector('#file-input');
const submitButton = document.querySelector('#submit');
const thumbnailPreview = document.querySelector('#thumbnail');
const errorDiv = document.querySelector('#error');
...

您将在 Express 服务器中定义和使用 /thumbnail 端点。

接下来,在 showError() 函数下方添加 createThumbnail() 函数:

客户端.js

...
function showError(msg) {
    errorDiv.innerText = `ERROR: ${msg}`;
}

async function createThumbnail(video) {

}
...

Web API 经常使用 JSON 在客户端之间传输结构化数据。 要将视频包含在 JSON 中,您必须将其编码为 base64,这会将其大小增加约 30%。 您可以改用 多部分请求 来避免这种情况。 多部分请求允许您通过 http 传输包括二进制文件在内的结构化数据,而不会产生不必要的开销。 您可以使用 FormData() 构造函数来执行此操作。

createThumbnail() 函数中,创建 FormData 的实例并将视频文件附加到对象。 然后使用以 FormData() 实例为主体的 Fetch API 向 API 端点发出 POST 请求。 将响应解释为二进制文件(或 blob)并将其转换为数据 URL,以便您可以将其分配给您之前创建的 <img> 标记。

下面是 createThumbnail() 的完整实现:

客户端.js

...
async function createThumbnail(video) {
    const payload = new FormData();
    payload.append('video', video);

    const res = await fetch(API_ENDPOINT, {
        method: 'POST',
        body: payload
    });

    if (!res.ok) {
        throw new Error('Creating thumbnail failed');
    }

    const thumbnailBlob = await res.blob();
    const thumbnail = await blobToDataURL(thumbnailBlob);

    return thumbnail;
}
...

你会注意到 createThumbnail() 在它的主体中有函数 blobToDataURL() 。 这是一个帮助函数,它将 blob 转换为 数据 URL

createThumbnail() 函数上方,创建返回 promise 的函数 blobDataToURL()

客户端.js

...
async function blobToDataURL(blob) {
    return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = () => resolve(reader.result);
        reader.onerror = () => reject(reader.error);
        reader.onabort = () => reject(new Error("Read aborted"));
        reader.readAsDataURL(blob);
    });
}
...

blobToDataURL() 使用 FileReader 读取二进制文件的内容并将其格式化为数据 URL。

现在定义了 createThumbnail()showError() 函数,您可以使用它们来完成事件监听器的实现:

客户端.js

...
submitButton.addEventListener('click', async () => {
    const { files } = fileInput;

    if (files.length > 0) {
        const file = files[0];
        try {
            const thumbnail = await createThumbnail(file);
            thumbnailPreview.src = thumbnail;
        } catch(error) {
            showError(error);
        }
    } else {
        showError('Please select a file');
    }
});

当用户单击按钮时,事件侦听器会将文件传递给 createThumbnail() 函数。 如果成功,它会将缩略图分配给您之前创建的 <img> 元素。 如果用户没有选择文件或请求失败,它会调用showError()函数显示错误。

此时,您的 client.js 文件将如下所示:

客户端.js

const API_ENDPOINT = 'http://localhost:3000/thumbnail';

const fileInput = document.querySelector('#file-input');
const submitButton = document.querySelector('#submit');
const thumbnailPreview = document.querySelector('#thumbnail');
const errorDiv = document.querySelector('#error');

function showError(msg) {
    errorDiv.innerText = `ERROR: ${msg}`;
}

async function blobToDataURL(blob) {
    return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = () => resolve(reader.result);
        reader.onerror = () => reject(reader.error);
        reader.onabort = () => reject(new Error("Read aborted"));
        reader.readAsDataURL(blob);
    });
}

async function createThumbnail(video) {
    const payload = new FormData();
    payload.append('video', video);

    const res = await fetch(API_ENDPOINT, {
        method: 'POST',
        body: payload
    });

    if (!res.ok) {
        throw new Error('Creating thumbnail failed');
    }

    const thumbnailBlob = await res.blob();
    const thumbnail = await blobToDataURL(thumbnailBlob);

    return thumbnail;
}

submitButton.addEventListener('click', async () => {
    const { files } = fileInput;

    if (files.length > 0) {
        const file = files[0];

        try {
            const thumbnail = await createThumbnail(file);
            thumbnailPreview.src = thumbnail;
        } catch(error) {
            showError(error);
        }
    } else {
        showError('Please select a file');
    }
});

通过运行再次启动服务器:

node server.mjs

现在设置您的客户端后,在此处上传视频文件将导致收到错误消息。 这是因为 /thumbnail 端点尚未构建。 在下一步中,您将在 Express 中创建 /thumbnail 端点以接受视频文件并创建缩略图。

    1. 第 3 步 — 设置端点以接受二进制数据

在此步骤中,您将为 /thumbnail 端点设置 POST 请求,并使用中间件接受多部分请求。

在编辑器中打开 server.mjs

nano server.mjs

然后,在文件顶部导入 multer

服务器.mjs

import express from 'express';
import cors from 'cors';
import multer from 'multer';
...

Multer 是一个中间件,在将传入的 multipart/form-data 请求传递给端点处理程序之前对其进行处理。 它从正文中提取字段和文件,并将它们作为 Express 中请求对象的数组提供。 您可以配置上传文件的存储位置,并设置文件大小和格式的限制。

导入后,使用以下选项初始化 multer 中间件:

服务器.mjs

...
const app = express();
const port = 3000;

const upload = multer({
    storage: multer.memoryStorage(),
    limits: { fileSize: 100 * 1024 * 1024 }
});

app.use(cors());
...

storage 选项可让您选择存储传入文件的位置。 调用 multer.memoryStorage() 将初始化一个存储引擎,它将文件保存在内存中的 Buffer 对象中,而不是将它们写入磁盘。 limits 选项允许您定义 各种限制 将接受哪些文件。 将 fileSize 限制设置为 100MB 或与您的需要和服务器上可用内存量相匹配的其他数字。 这将防止您的 API 在输入文件太大时崩溃。

注意: 由于 WebAssembly 的限制,ffmpeg.wasm 无法处理大小超过 2GB 的输入文件。


接下来,设置 POST /thumbnail 端点本身:

服务器.mjs

...
app.use(cors());

app.post('/thumbnail', upload.single('video'), async (req, res) => {
    const videoData = req.file.buffer;

    res.sendStatus(200);
});

app.listen(port, () => {
    console.log(`[info] ffmpeg-api listening at http://localhost:${port}`)
});

upload.single('video') 调用将为该端点设置一个中间件,该中间件将解析包含单个文件的多部分请求的主体。 第一个参数是字段名。 它必须与您在 client.js 中创建请求时提供给 FormData 的那个匹配。 在这种情况下,它是 videomulter 然后将解析的文件附加到 req 参数。 该文件的内容将在 req.file.buffer 下。

此时,端点不对其接收到的数据做任何事情。 它通过发送一个空的 200 响应来确认请求。 在下一步中,您将用从收到的视频数据中提取缩略图的代码替换它。

第 4 步 — 使用 ffmpeg.wasm 处理媒体

在此步骤中,您将使用 ffmpeg.wasmPOST /thumbnail 端点接收的视频文件中提取缩略图。

ffmpeg.wasm 是 FFmpeg 的纯 WebAssembly 和 JavaScript 端口。 它的主要目标是允许直接在浏览器中运行 FFmpeg。 然而,因为 Node.js 是建立在 V8 之上的——Chrome 的 JavaScript 引擎——你也可以在服务器上使用这个库。

ffmpeg 命令之上构建的包装器上使用 FFmpeg 的本机端口的好处是,如果您计划使用 Docker 部署应用程序,则不必构建包含FFmpeg 和 Node.js。 这将节省您的时间并减轻您的服务的维护负担。

将以下导入添加到 server.mjs 的顶部:

服务器.mjs

import express from 'express';
import cors from 'cors';
import multer from 'multer';
import { createFFmpeg } from '@ffmpeg/ffmpeg';
...

然后,创建 ffmpeg.wasm 的实例并开始加载核心:

服务器.mjs

...
import { createFFmpeg } from '@ffmpeg/ffmpeg';

const ffmpegInstance = createFFmpeg({ log: true });
let ffmpegLoadingPromise = ffmpegInstance.load();

const app = express();
...

ffmpegInstance 变量保存对库的引用。 调用 ffmpegInstance.load() 开始将内核异步加载到内存中并返回一个承诺。 将 promise 存储在 ffmpegLoadingPromise 变量中,以便您可以检查核心是否已加载。

接下来,定义以下辅助函数,该函数将使用 fmpegLoadingPromise 等待核心加载,以防第一个请求在准备好之前到达:

服务器.mjs

...
let ffmpegLoadingPromise = ffmpegInstance.load();

async function getFFmpeg() {
    if (ffmpegLoadingPromise) {
        await ffmpegLoadingPromise;
        ffmpegLoadingPromise = undefined;
    }

    return ffmpegInstance;
}

const app = express();
...

getFFmpeg() 函数返回对存储在 ffmpegInstance 变量中的库的引用。 在返回它之前,它会检查库是否已经完成加载。 如果没有,它将等到 ffmpegLoadingPromise 解决。 如果对 POST /thumbnail 端点的第一个请求在 ffmpegInstance 准备好使用之前到达,您的 API 将等待并尽可能解决它,而不是拒绝它。

现在,实现 POST /thumbnail 端点处理程序。 将函数末尾的 res.sendStatus(200); 替换为对 getFFmpeg 的调用,以便在准备好时获取对 ffmpeg.wasm 的引用:

服务器.mjs

...
app.post('/thumbnail', upload.single('video'), async (req, res) => {
    const videoData = req.file.buffer;

    const ffmpeg = await getFFmpeg();
});
...

ffmpeg.wasm 在内存文件系统之上工作。 您可以使用 ffmpeg.FS 对其进行读写。 运行 FFmpeg 操作时,您会将虚拟文件名作为参数传递给 ffmpeg.run 函数,就像使用 CLI 工具时一样。 FFmpeg 创建的任何输出文件都将写入文件系统供您检索。

在这种情况下,输入文件是视频。 输出文件将是单个 PNG 图像。 定义以下变量:

服务器.mjs

...
    const ffmpeg = await getFFmpeg();

    const inputFileName = `input-video`;
    const outputFileName = `output-image.png`;
    let outputData = null;
});
...

文件名将用于虚拟文件系统。 outputData 是您在缩略图准备好后存储缩略图的位置。

调用 ffmpeg.FS() 将视频数据写入内存文件系统:

服务器.mjs

...
    let outputData = null;

    ffmpeg.FS('writeFile', inputFileName, videoData);
});
...

然后,运行 FFmpeg 操作:

服务器.mjs

...
    ffmpeg.FS('writeFile', inputFileName, videoData);

    await ffmpeg.run(
        '-ss', '00:00:01.000',
        '-i', inputFileName,
        '-frames:v', '1',
        outputFileName
    );
});
...

-i 参数指定输入文件。 -ss 搜索到指定的时间(在这种情况下,距离视频开头 1 秒)。 -frames:v 限制将写入输出的帧数(在这种情况下为单个帧)。 末尾的 outputFileName 表示 FFmpeg 将输出写入到哪里。

FFmpeg 退出后,使用 ffmpeg.FS() 从文件系统中读取数据并删除输入和输出文件以释放内存:

服务器.mjs

...
    await ffmpeg.run(
        '-ss', '00:00:01.000',
        '-i', inputFileName,
        '-frames:v', '1',
        outputFileName
    );

    outputData = ffmpeg.FS('readFile', outputFileName);
    ffmpeg.FS('unlink', inputFileName);
    ffmpeg.FS('unlink', outputFileName);
});
...

最后,在响应正文中分派输出数据:

服务器.mjs

...
    ffmpeg.FS('unlink', outputFileName);

    res.writeHead(200, {
        'Content-Type': 'image/png',
        'Content-Disposition': `attachment;filename=${outputFileName}`,
        'Content-Length': outputData.length
    });
    res.end(Buffer.from(outputData, 'binary'));
});
...

调用 res.writeHead() 调度响应头。 第二个参数包括自定义的 http headers),其中包含有关将遵循的请求正文中的数据的信息。 res.end() 函数将其第一个参数中的数据作为请求的主体发送并最终确定请求。 outputData 变量是由 ffmpeg.FS() 返回的原始字节数组。 将其传递给 Buffer.from() 会初始化 Buffer 以确保 res.end() 正确处理二进制数据。

此时,您的 POST /thumbnail 端点实现应如下所示:

服务器.mjs

...
app.post('/thumbnail', upload.single('video'), async (req, res) => {
    const videoData = req.file.buffer;

    const ffmpeg = await getFFmpeg();

    const inputFileName = `input-video`;
    const outputFileName = `output-image.png`;
    let outputData = null;

    ffmpeg.FS('writeFile', inputFileName, videoData);

    await ffmpeg.run(
        '-ss', '00:00:01.000',
        '-i', inputFileName,
        '-frames:v', '1',
        outputFileName
    );

    outputData = ffmpeg.FS('readFile', outputFileName);
    ffmpeg.FS('unlink', inputFileName);
    ffmpeg.FS('unlink', outputFileName);

    res.writeHead(200, {
        'Content-Type': 'image/png',
        'Content-Disposition': `attachment;filename=${outputFileName}`,
        'Content-Length': outputData.length
    });
    res.end(Buffer.from(outputData, 'binary'));
});
...

除了上传的 100MB 文件限制外,没有输入验证或错误处理。 当 ffmpeg.wasm 处理文件失败时,从虚拟文件系统读取输出将失败并阻止响应发送。 出于本教程的目的,将端点的实现包装在 try-catch 块中以处理该场景:

服务器.mjs

...
app.post('/thumbnail', upload.single('video'), async (req, res) => {
    try {
        const videoData = req.file.buffer;

        const ffmpeg = await getFFmpeg();

        const inputFileName = `input-video`;
        const outputFileName = `output-image.png`;
        let outputData = null;

        ffmpeg.FS('writeFile', inputFileName, videoData);

        await ffmpeg.run(
            '-ss', '00:00:01.000',
            '-i', inputFileName,
            '-frames:v', '1',
            outputFileName
        );

        outputData = ffmpeg.FS('readFile', outputFileName);
        ffmpeg.FS('unlink', inputFileName);
        ffmpeg.FS('unlink', outputFileName);

        res.writeHead(200, {
            'Content-Type': 'image/png',
            'Content-Disposition': `attachment;filename=${outputFileName}`,
            'Content-Length': outputData.length
        });
        res.end(Buffer.from(outputData, 'binary'));
    } catch(error) {
        console.error(error);
        res.sendStatus(500);
    }
...
});

其次,ffmpeg.wasm 不能并行处理两个请求。 您可以通过启动服务器自己尝试:

node --experimental-wasm-threads server.mjs

注意 ffmpeg.wasm 工作所需的标志。 该库依赖于 WebAssembly 线程大容量内存操作 。 这些自 2019 年以来一直在 V8/Chrome 中。 但是,从 Node.js v16.11.0 开始,WebAssembly 线程仍保留在标志后面,以防在提案最终确定之前可能会发生变化。 在旧版本的 Node.js 中,大容量内存操作也需要一个标志。 如果您运行的是 Node.js 15 或更低版本,请同时添加 --experimental-wasm-bulk-memory

该命令的输出将如下所示:

Output[info] use ffmpeg.wasm v0.10.1
[info] load ffmpeg-core
[info] loading ffmpeg-core
[info] fetch ffmpeg.wasm-core script from @ffmpeg/core
[info] ffmpeg-api listening at http://localhost:3000
[info] ffmpeg-core loaded

在网络浏览器中打开 client.html 并选择一个视频文件。 当您单击 Create Thumbnail 按钮时,您应该会看到缩略图出现在页面上。 在幕后,该站点将视频上传到 API,API 对其进行处理并以图像进行响应。 但是,当您快速连续重复单击该按钮时,API 将处理第一个请求。 后续请求将失败:

OutputError: ffmpeg.wasm can only run one command at a time
    at Object.run (.../ffmpeg-api/node_modules/@ffmpeg/ffmpeg/src/createFFmpeg.js:126:13)
    at file://.../ffmpeg-api/server.mjs:54:26
    at runMicrotasks (<anonymous>)
    at processTicksAndRejections (internal/process/task_queues.js:95:5)

在下一节中,您将学习如何处理并发请求。

第 5 步 - 处理并发请求

由于 ffmpeg.wasm 一次只能执行一个操作,因此您需要一种方法来序列化传入的请求并一次处理一个请求。 在这种情况下, 承诺队列 是一个完美的解决方案。 不是立即开始处理每个请求,而是在处理完之前到达的所有请求后排队处理。

在您喜欢的编辑器中打开 server.mjs

nano server.mjs

server.mjs顶部导入p-queue

服务器.mjs

import express from 'express';
import cors from 'cors';
import { createFFmpeg } from '@ffmpeg/ffmpeg';
import PQueue from 'p-queue';
...

然后,在 server.mjs 文件顶部的变量 ffmpegLoadingPromise 下创建一个新队列:

服务器.mjs

...
const ffmpegInstance = createFFmpeg({ log: true });
let ffmpegLoadingPromise = ffmpegInstance.load();

const requestQueue = new PQueue({ concurrency: 1 });
...

POST /thumbnail 端点处理程序中,将对 ffmpeg 的调用包装在将排队的函数中:

服务器.mjs

...
app.post('/thumbnail', upload.single('video'), async (req, res) => {
    try {
        const videoData = req.file.buffer;

        const ffmpeg = await getFFmpeg();

        const inputFileName = `input-video`;
        const outputFileName = `thumbnail.png`;
        let outputData = null;

        await requestQueue.add(async () => {
            ffmpeg.FS('writeFile', inputFileName, videoData);

            await ffmpeg.run(
                '-ss', '00:00:01.000',
                '-i', inputFileName,
                '-frames:v', '1',
                outputFileName
            );

            outputData = ffmpeg.FS('readFile', outputFileName);
            ffmpeg.FS('unlink', inputFileName);
            ffmpeg.FS('unlink', outputFileName);
        });

        res.writeHead(200, {
            'Content-Type': 'image/png',
            'Content-Disposition': `attachment;filename=${outputFileName}`,
            'Content-Length': outputData.length
        });
        res.end(Buffer.from(outputData, 'binary'));
    } catch(error) {
        console.error(error);
        res.sendStatus(500);
    }
});
...

每次有新请求进来时,它只会在前面没有其他东西排队时才开始处理。 请注意,响应的最终发送可以异步发生。 一旦 ffmpeg.wasm 操作完成运行,另一个请求可以在响应消失时开始处理。

要测试一切是否按预期工作,请再次启动服务器:

node --experimental-wasm-threads server.mjs

在浏览器中打开 client.html 文件并尝试上传文件。

有了队列,API 现在每次都会响应。 请求将按照到达的顺序依次处理。

结论

在本文中,您构建了一个 Node.js 服务,该服务使用 ffmpeg.wasm 从视频中提取缩略图。 您学习了如何使用多部分请求将二进制数据从浏览器上传到 Express API,以及如何在 Node.js 中使用 FFmpeg 处理媒体,而无需依赖外部工具或将数据写入磁盘。

FFmpeg 是一个非常通用的工具。 您可以使用本教程中的知识来利用 FFmpeg 支持的任何功能并在您的项目中使用它们。 例如,要生成一个三秒的 GIF,在 POST /thumbnail 端点上将 ffmpeg.run 调用更改为此:

服务器.mjs

...
await ffmpeg.run(
    '-y',
    '-t', '3',
    '-i', inputFileName,
    '-filter_complex', 'fps=5,scale=720:-1:flags=lanczos[x];[x]split[x1][x2];[x1]palettegen[p];[x2][p]paletteuse',
    '-f', 'gif',
    outputFileName
);
...

该库接受与原始 ffmpeg CLI 工具相同的参数。 你可以使用【X16X】官方文档【X42X】为你的用例找到解决方案,并在终端快速测试。

由于 ffmpeg.wasm 是自包含的,您可以使用库存的 Node.js 基础镜像对这个服务进行 docker 化,并通过在负载均衡器后面保留多个节点来扩展您的服务。 按照教程 如何使用 Docker 构建 Node.js 应用程序来了解更多信息。

如果您的用例需要执行更昂贵的操作,例如对大型视频进行转码,请确保您在具有足够内存来存储它们的机器上运行您的服务。 由于当前 WebAssembly 的限制,最大输入文件大小不能超过 2GB,尽管这可能会在未来 更改

此外,ffmpeg.wasm 无法利用原始 FFmpeg 代码库中的一些 x86 汇编优化。 这意味着某些操作可能需要很长时间才能完成。 如果是这种情况,请考虑这是否适合您的用例。 或者,向您的 API 发出异步请求。 与其等待操作完成,不如将其排队并以唯一 ID 响应。 创建客户端可以查询的另一个端点,以确定处理是否结束并且输出文件是否准备好。 详细了解 REST API 的 异步请求-回复模式 以及如何实现它。