基于 FFmpeg 与 Nuxt 3 实现服务端图片压缩与视频截帧
业务场景与技术方案
在Web应用开发中,多媒体资源的优化是提升用户体验的关键环节。常见的业务需求包括:提取视频关键帧作为列表封面,以及对高清原图进行压缩以生成缩略图。虽然各大云存储服务商提供了相应的图片处理和视频截帧API,但这通常会产生额外的计费。本文将探讨如何利用开源工具 FFmpeg 结合 Nuxt 3 框架,在自建服务端低成本地实现上述功能。
FFmpeg 核心命令解析
FFmpeg 是一款强大的开源音视频处理工具。针对本文的场景,我们主要使用以下两个核心命令:
1. 图片等比例缩放
ffmpeg -i input.jpg -vf scale=300:-1 output.jpg
该命令使用 scale 视频滤镜。将宽度固定为 300 像素,高度参数设置为 -1 表示 FFmpeg 会自动计算高度以保持原始宽高比。
2. 视频指定时间截帧
ffmpeg -i input.mp4 -ss 00:00:03 -frames:v 1 frame.jpg
-ss 参数用于定位时间戳(此处为第3秒),-frames:v 1 表示仅提取一帧视频画面并保存为图片。
Nuxt 3 项目实战
我们将使用 Nuxt 3 构建一个全栈应用,前端负责文件上传,后端调用 FFmpeg 进行异步处理。
初始化项目
npx nuxi@latest init media-processor
cd media-processor
yarn install
前端页面实现
在 pages/index.vue 中构建文件上传组件,并使用 Nuxt 3 的 $fetch 提交数据。
<template>
<div class="upload-container">
<input type="file" @change="handleUpload" accept="image/*,video/*" />
</div>
</template>
<script setup lang="ts">
const handleUpload = async (event: Event) => {
const target = event.target as HTMLInputElement;
const selectedFile = target.files?.[0];
if (!selectedFile) return;
const payload = new FormData();
payload.append('media', selectedFile);
const response = await $fetch('/api/process-media', {
method: 'POST',
body: payload
});
console.log('处理结果:', response);
};
</script>
后端 API 与 FFmpeg 集成
在 server/api/process-media.post.ts 中处理上传逻辑。这里我们摒弃了传统的第三方解析库,直接使用 Nuxt 3 内置的 readMultipartFormData,并通过 util.promisify 将 FFmpeg 命令异步化,避免阻塞 Node.js 事件循环。
import { exec } from 'child_process';
import { promisify } from 'util';
import { writeFile, mkdir } from 'fs/promises';
import { join } from 'path';
const execAsync = promisify(exec);
const UPLOAD_DIR = join(process.cwd(), 'public', 'uploads');
export default defineEventHandler(async (event) => {
// 确保上传目录存在
await mkdir(UPLOAD_DIR, { recursive: true });
const formData = await readMultipartFormData(event);
if (!formData) throw createError({ statusCode: 400, message: '未接收到文件' });
const filePart = formData.find(part => part.name === 'media');
if (!filePart) throw createError({ statusCode: 400, message: '缺少 media 字段' });
const originalName = filePart.filename || 'unknown';
const ext = originalName.split('.').pop();
const baseName = originalName.replace(`.${ext}`, '');
const tempPath = join(UPLOAD_DIR, `temp_${Date.now()}.${ext}`);
// 将内存中的文件流写入临时文件
await writeFile(tempPath, filePart.data);
let resultMessage = '';
if (filePart.type?.startsWith('image/')) {
// 图片压缩:限制高度为 200,宽度等比例缩放
const outputPath = join(UPLOAD_DIR, `thumb_${baseName}.${ext}`);
await execAsync(`ffmpeg -i "${tempPath}" -vf scale=-1:200 "${outputPath}" -y`);
resultMessage = `图片已压缩,输出至: thumb_${baseName}.${ext}`;
} else if (filePart.type?.startsWith('video/')) {
// 视频截帧:截取第 2 秒的画面
const outputPath = join(UPLOAD_DIR, `cover_${baseName}.jpg`);
await execAsync(`ffmpeg -i "${tempPath}" -ss 00:00:02 -frames:v 1 "${outputPath}" -y`);
resultMessage = `视频已截帧,输出至: cover_${baseName}.jpg`;
} else {
throw createError({ statusCode: 415, message: '不支持的文件类型' });
}
return { success: true, message: resultMessage };
});
PM2 进程管理配置
在项目根目录创建 ecosystem.config.js 以配置集群模式启动。
module.exports = {
apps: [
{
name: 'media-server',
port: '3000',
exec_mode: 'cluster',
instances: 2,
script: './.output/server/index.mjs'
}
]
};
Linux 服务端部署
将代码部署至 Ubuntu 服务器,首先需要安装必要的系统依赖和运行环境。
环境准备
# 安装 FFmpeg
sudo apt update
sudo apt install ffmpeg -y
# 验证 FFmpeg 安装
ffmpeg -version
# 使用 nvm 安装 Node.js (Nuxt 3 需要 Node 16.x+)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc
nvm install 18
nvm use 18
# 安装 Yarn 和 PM2
npm install -g yarn pm2
构建与启动
拉取代码并安装依赖后,执行构建命令并通过 PM2 启动服务。
yarn install
yarn build
pm2 start ecosystem.config.js
服务启动后,访问服务器 IP 及对应端口(如 http://<server-ip>:3000),即可通过前端页面上传媒体文件。服务端将自动在 public/uploads 目录下生成带有 thumb_ 前缀的压缩图片,或带有 cover_ 前缀的视频封面帧。