前言:什么是 FFmpeg 和 FFprobe
FFmpeg是一个强大的跨平台音视频处理工具,它可以处理几乎所有常见的音视频格式,实现格式转换、编解码、流媒体处理等功能。而FFprobe则是FFmpeg套件中的一个工具,专门用于分析媒体文件的元数据信息,包括文件格式、编码信息、时长、比特率等。
在Node.js环境中,我们可以通过fluent-ffmpeg库来便捷地使用FFmpeg的功能,同时通过ffprobe-static获取FFprobe工具的静态版本。
一、技术栈选型与核心工具介绍
1.1 核心工具
- FFmpeg:跨平台的音视频处理瑞士军刀,支持格式转换、编解码、流媒体处理等功能,是音视频领域的工业级标准工具。
- FFprobe:
FFmpeg套件内置的媒体元数据分析工具,专门用于提取音视频文件的详细信息(格式、时长、流信息等),性能高效且解析维度全面。 - Node.js:服务端 JavaScript 运行环境,通过丰富的
npm包生态,可便捷集成FFmpeg/FFprobe工具。
1.2 关键依赖包
| 依赖包 | 作用 |
|---|---|
fluent-ffmpeg |
Node.js 环境下的FFmpeg封装库,提供简洁的 API 操作FFmpeg/FFprobe |
ffmpeg-static |
提供FFmpeg的静态二进制文件,无需额外安装系统级FFmpeg |
ffprobe-static |
提供FFprobe的静态二进制文件,与ffmpeg-static配套使用 |
fs/path |
Node.js内置模块,用于文件系统操作和路径处理 |
child_process |
Node.js内置模块,用于执行原生系统命令(补充命令行调用FFmpeg) |
1.3 环境准备
首先通过 npm 安装所需依赖:
npm install fluent-ffmpeg ffmpeg-static ffprobe-static --save
二、核心原理:FFprobe 如何解析媒体文件
FFprobe通过读取音视频文件的头部信息和流数据,解析出标准化的元数据结构。其核心工作流程如下:
- 打开目标媒体文件,识别文件容器格式(如 MP4、MKV、MP3 等);
- 遍历文件中的媒体流(音频流、视频流、字幕流等);
- 提取每个流的关键参数(编码格式、采样率、分辨率、帧率等);
- 汇总文件级元数据(时长、比特率、文件大小等);
- 以结构化数据(JSON 格式)返回解析结果。
在 Node.js 中,fluent-ffmpeg封装了FFprobe的命令行调用,通过回调或Promise方式返回解析结果,避免了直接操作命令行的复杂性;而原生child_process模块则可直接执行FFmpeg命令行,适配更灵活的自定义转码需求。
三、完整实现方案
3.1 基础配置:FFmpeg/FFprobe 路径设置
由于ffmpeg-static和ffprobe-static提供的是静态二进制文件,需要先配置工具路径。特别注意 Electron 打包后的生产环境(asar 文件解压问题):
const ffmpeg = require("fluent-ffmpeg");
const ffmpegPath = require("ffmpeg-static");
const ffprobePath = require("ffprobe-static");
// 设置 FFmpeg 和 FFprobe 的路径
ffmpeg.setFfmpegPath(ffmpegPath);
ffmpeg.setFfprobePath(ffprobePath.path);
// 生产环境适配(Electron打包后asar文件处理)
if (process.env.NODE_ENV === "production") {
// 替换asar路径为解压后的路径,避免文件访问权限问题
ffmpeg.setFfmpegPath(ffmpegPath.replace("app.asar", "app.asar.unpacked"));
ffmpeg.setFfprobePath(ffprobePath.path.replace("app.asar", "app.asar.unpacked"));
}
3.2 核心函数:readFileInfo 实现
该函数整合了文件存在性校验、文件系统信息读取、媒体元数据解析三大核心逻辑,返回结构化的完整信息:
import fs from "fs";
import path from "path";
import ffmpeg from "fluent-ffmpeg";
import ffmpegPath from "ffmpeg-static";
import ffprobePath from "ffprobe-static";
// 设置 FFmpeg 和 FFprobe 的路径
ffmpeg.setFfmpegPath(ffmpegPath);
ffmpeg.setFfprobePath(ffprobePath.path);
// 生产环境适配(Electron打包后asar文件处理)
if (process.env.NODE_ENV === "production") {
// 替换asar路径为解压后的路径,避免文件访问权限问题
ffmpeg.setFfmpegPath(ffmpegPath.replace("app.asar", "app.asar.unpacked"));
ffmpeg.setFfprobePath(ffprobePath.path.replace("app.asar", "app.asar.unpacked"));
}
/**
* 读取音视频文件的系统信息和媒体元数据
* @param {string} file - 音视频文件绝对路径
* @returns {Promise<Object>} 包含完整信息的对象
*/
export const readFileInfo = file => {
return new Promise((resolve, reject) => {
// 存储最终返回的文件信息
let fileInfo = {};
// 第一步:检查文件是否存在
fs.access(file, fs.constants.F_OK, err => {
if (err) {
console.error(`[readFileInfo] 文件不存在:${file}`, err);
return reject(new Error(`文件不存在:${file}`));
}
// 第二步:获取文件系统基础信息
fs.stat(file, (err, stats) => {
if (err) {
console.error(`[readFileInfo] 获取文件系统信息失败:${file}`, err);
return reject(new Error(`获取文件系统信息失败:${err.message}`));
}
// 整合文件系统信息
Object.assign(fileInfo, {
size: stats.size, // 文件大小(单位:字节)
birthtime: stats.birthtime, // 文件创建时间(Date对象)
mtime: stats.mtime, // 文件最后修改时间(Date对象)
atime: stats.atime, // 文件最后访问时间(Date对象)
isFile: stats.isFile(), // 是否为文件(排除目录)
});
// 第三步:使用ffprobe解析媒体元数据
ffmpeg.ffprobe(file, (err, metadata) => {
if (err) {
console.error(`[readFileInfo] 解析媒体元数据失败:${file}`, err);
return reject(new Error(`解析媒体元数据失败:${err.message}`));
}
// 整合媒体核心信息
Object.assign(fileInfo, {
// 文件格式相关
fileExt: path.extname(file).slice(1), // 文件扩展名(不含.)
fileName: path.basename(file), // 文件名(含扩展名)
fullPath: metadata.filename, // 文件完整路径
formatName: metadata.format_name, // 媒体格式(如mp4、flv、mp3)
formatLongName: metadata.format_long_name, // 媒体格式全称
// 媒体核心参数
duration: Number(metadata.format.duration.toFixed(2)), // 时长(单位:秒,保留2位小数)
bitrate: Number(metadata.format.bit_rate), // 总比特率(单位:bps)
size: metadata.format.size || fileInfo.size, // 媒体文件大小(优先取ffprobe结果)
// 媒体流信息(音频流、视频流、字幕流等)
streams: metadata.streams.map(stream => ({
type: stream.codec_type, // 流类型(audio/video/subtitle)
codecName: stream.codec_name, // 编码格式(如aac、h264)
codecLongName: stream.codec_long_name, // 编码全称
profile: stream.profile, // 编码配置文件
width: stream.width || null, // 视频宽度(仅视频流)
height: stream.height || null, // 视频高度(仅视频流)
frameRate: stream.r_frame_rate
? Number(stream.r_frame_rate.split("/").reduce((a, b) => a / b))
: null, // 帧率(仅视频流)
sampleRate: stream.sample_rate ? Number(stream.sample_rate) : null, // 采样率(仅音频流,单位:Hz)
channels: stream.channels || null, // 声道数(仅音频流)
channelLayout: stream.channel_layout || null, // 声道布局(仅音频流)
bitrate: stream.bit_rate ? Number(stream.bit_rate) : null, // 流比特率(单位:bps)
duration: stream.duration ? Number(stream.duration.toFixed(2)) : null, // 流时长(单位:秒)
codecTag: stream.codec_tag_string || null, // 编码标签
disposition: stream.disposition, // 流属性(如是否默认、强制等)
})),
// 分离音频流和视频流(便捷使用)
audioStreams: metadata.streams.filter(
stream => stream.codec_type === "audio"
),
videoStreams: metadata.streams.filter(
stream => stream.codec_type === "video"
),
subtitleStreams: metadata.streams.filter(
stream => stream.codec_type === "subtitle"
),
});
// 第四步:返回完整信息
resolve(fileInfo);
});
});
});
});
};
3.3 格式转换
在实际项目中,读取文件信息后常需进行格式转换,这里提供基于FFmpeg的格式转换工具函数,与信息读取功能形成完整工作流。
3.3.1 ffmpeg 配套工具函数
/**
* 音视频格式转换
* @param {string} inputFilePath - 输入文件路径
* @param {string} outputFilePath - 输出文件路径
* @returns {Promise<boolean>} 转换成功返回true,失败返回false
*/
export const formatConvert = (inputFilePath, outputFilePath) => {
return new Promise((resolve, reject) => {
// 验证输入文件是否存在
fs.access(inputFilePath, fs.constants.F_OK, err => {
if (err) {
console.error(`[formatConvert] 输入文件不存在:${inputFilePath}`, err);
return reject(new Error(`输入文件不存在:${inputFilePath}`));
}
// 构建FFmpeg转换命令
ffmpeg(inputFilePath)
.output(outputFilePath)
.overwriteOutput() // 覆盖已存在的输出文件
.on("start", commandLine => {
console.log(`[formatConvert] 开始转换,命令:${commandLine}`);
})
.on("progress", progress => {
// 输出转换进度(百分比)
const percent = progress.percent ? progress.percent.toFixed(2) : 0;
console.log(`[formatConvert] 转换进度:${percent}%`);
})
.on("end", () => {
console.log(`[formatConvert] 转换完成:${outputFilePath}`);
resolve(true);
})
.on("error", (err, stdout, stderr) => {
console.error(`[formatConvert] 转换失败:${err.message}`);
console.error(`[formatConvert] FFmpeg stdout:${stdout}`);
console.error(`[formatConvert] FFmpeg stderr:${stderr}`);
reject(new Error(`转换失败:${err.message}`));
})
.run();
});
});
};
3.3.2 原生命令行调用实现
相较于fluent-ffmpeg封装的 API,原生命令行调用更灵活,可直接拼接自定义FFmpeg参数,满足复杂转码需求:
/**
* 音视频格式转换(原生FFmpeg命令行调用版本)
* @param {string} inputFilePath - 输入文件绝对路径
* @param {string} outputFilePath - 输出文件绝对路径
* @param {Object} [options] - 转码可选配置
* @param {string} [options.videoCodec='libx264'] - 视频编码器(如libx264、copy)
* @param {string} [options.audioCodec='aac'] - 音频编码器(如aac、mp3、copy)
* @param {string} [options.size] - 视频分辨率(如1280x720)
* @param {string} [options.audioBitrate] - 音频比特率(如128k)
* @param {string} [options.videoBitrate] - 视频比特率(如500k)
* @returns {Promise<boolean>} 转换成功返回true,失败返回false
*/
export const formatConvertByCommand = (inputFilePath, outputFilePath, options = {}) => {
return new Promise((resolve, reject) => {
try {
// 第一步:验证输入文件是否存在
fs.accessSync(inputFilePath, fs.constants.F_OK);
// 第二步:处理路径转义(兼容空格、中文路径)
const safeInputPath = path.resolve(inputFilePath).replace(/"/g, '\\"');
const safeOutputPath = path.resolve(outputFilePath).replace(/"/g, '\\"');
// 第三步:构建FFmpeg命令参数
const {
videoCodec = "libx264",
audioCodec = "aac",
size,
audioBitrate,
videoBitrate,
} = options;
// 基础命令:-i 指定输入,-c:v 指定视频编码,-c:a 指定音频编码
let commandArr = [
`"${ffmpegPath}"`, // FFmpeg二进制路径(加引号兼容空格)
`-i "${safeInputPath}"`, // 输入文件
`-c:v ${videoCodec}`, // 视频编码器
`-c:a ${audioCodec}`, // 音频编码器
];
// 可选参数:视频分辨率
if (size) {
commandArr.push(`-s ${size}`);
}
// 可选参数:音频比特率
if (audioBitrate) {
commandArr.push(`-b:a ${audioBitrate}`);
}
// 可选参数:视频比特率
if (videoBitrate) {
commandArr.push(`-b:v ${videoBitrate}`);
}
// 覆盖输出文件(无需交互确认)
commandArr.push("-y");
// 输出文件
commandArr.push(`"${safeOutputPath}"`);
// 拼接最终命令
const command = commandArr.join(" ");
console.log(`[formatConvertByCommand] 执行FFmpeg命令:${command}`);
// 第四步:执行同步命令(也可使用exec异步执行,支持进度回调)
execSync(command, {
stdio: "pipe", // 重定向标准输出/错误,避免堵塞
timeout: 300000, // 超时时间:5分钟(根据文件大小调整)
});
console.log(`[formatConvertByCommand] 转换完成:${outputFilePath}`);
resolve(true);
} catch (err) {
console.error(`[formatConvertByCommand] 转换失败:${err.message}`);
// 输出FFmpeg原始错误信息,便于排查
if (err.stdout) console.error("FFmpeg stdout:", err.stdout.toString());
if (err.stderr) console.error("FFmpeg stderr:", err.stderr.toString());
reject(new Error(`格式转换失败:${err.message}`));
}
});
};
// 异步版本(支持进度监听)
export const formatConvertByCommandAsync = (inputFilePath, outputFilePath, options = {}) => {
return new Promise((resolve, reject) => {
try {
fs.accessSync(inputFilePath, fs.constants.F_OK);
const safeInputPath = path.resolve(inputFilePath).replace(/"/g, '\\"');
const safeOutputPath = path.resolve(outputFilePath).replace(/"/g, '\\"');
const {
videoCodec = "libx264",
audioCodec = "aac",
size,
audioBitrate,
videoBitrate,
} = options;
let commandArr = [
`"${ffmpegPath}"`,
`-i "${safeInputPath}"`,
`-c:v ${videoCodec}`,
`-c:a ${audioCodec}`,
];
if (size) commandArr.push(`-s ${size}`);
if (audioBitrate) commandArr.push(`-b:a ${audioBitrate}`);
if (videoBitrate) commandArr.push(`-b:v ${videoBitrate}`);
commandArr.push("-y", `"${safeOutputPath}"`);
const command = commandArr.join(" ");
// 异步执行命令,实时监听输出
const childProcess = exec(command, (err, stdout, stderr) => {
if (err) {
console.error(`[formatConvertByCommandAsync] 执行失败:${err.message}`);
if (stderr) console.error("FFmpeg错误输出:", stderr.toString());
reject(new Error(`转换失败:${err.message}`));
return;
}
console.log(`[formatConvertByCommandAsync] 转换完成:${outputFilePath}`);
resolve(true);
});
// 监听FFmpeg输出,解析转码进度(可选)
childProcess.stderr.on("data", data => {
const output = data.toString();
// 匹配FFmpeg输出的时间进度(如:time=00:01:20.12)
const timeMatch = output.match(/time=(\d{2}:\d{2}:\d{2}\.\d{2})/);
if (timeMatch) {
console.log(`[转码进度] 当前处理时间:${timeMatch[1]}`);
}
});
} catch (err) {
console.error(`[formatConvertByCommandAsync] 前置校验失败:${err.message}`);
reject(new Error(`格式转换前置校验失败:${err.message}`));
}
});
};
四、核心功能解析
4.1 文件存在性与系统信息校验
fs.access/fs.accessSync:分别用于异步/同步检查文件是否存在,避免后续操作报错;fs.stat:获取文件系统级信息,包括大小、创建时间、修改时间等,这些信息是媒体文件管理的基础;- 额外增加
isFile判断,确保输入路径指向文件而非目录,提升函数健壮性。
4.2 媒体元数据解析(FFprobe 核心能力)
通过ffmpeg.ffprobe获取的metadata对象包含海量信息,我们筛选并结构化了核心字段:
- 格式信息:
format_name(如 mp4)、format_long_name(如 MPEG-4 Part 14),用于格式校验; - 核心参数:
duration(时长)、bitrate(比特率),用于转码参数决策; - 流信息:
- 音频流:采样率、声道数、编码格式(如 aac);
- 视频流:分辨率、帧率、编码格式(如 h264);
- 字幕流:字幕编码、语言等;
- 分离
audioStreams、videoStreams等数组,方便业务直接使用(如仅处理音频流)。
4.3 原生命令行转码核心亮点
- 路径安全处理:通过
path.resolve和引号包裹路径,兼容含空格、中文的文件路径; - 灵活参数配置:支持自定义视频编码器、音频编码器、分辨率、比特率等核心参数;
- 超时控制:设置
timeout避免大文件转码无限制堵塞; - 进度监听:通过监听
stderr输出,解析 FFmpeg 原生的时间进度信息; - 错误详情输出:捕获
stdout/stderr,便于定位转码失败原因(如编码不支持、参数错误)。
4.4 两种转码方式对比
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
fluent-ffmpeg封装 API |
语法简洁、内置进度回调、跨平台兼容好 | 自定义参数拼接不灵活 | 简单格式转换(如 MP4 转 MP3、AVI 转 MP4) |
| 原生命令行调用 | 参数完全自定义、支持复杂转码逻辑 | 需要手动处理路径转义、进度解析复杂 | 精细化转码(如指定码率/分辨率/编码器、多轨处理) |
五、使用示例与返回结果
5.1 基础使用:读取文件信息
// 导入函数
import { readFileInfo } from "./media-utils.js";
// 读取文件信息示例
async function getMediaDetailInfo() {
const filePath = "/Users/test/media/sample.mp4"; // 替换为实际文件路径
try {
const info = await readFileInfo(filePath);
console.log("音视频文件完整信息:", JSON.stringify(info, null, 2));
} catch (error) {
console.error("获取文件信息失败:", error.message);
}
}
// 执行函数
getMediaDetailInfo();
5.2 进阶使用:原生命令行转码
// 导入函数
import { formatConvertByCommand, formatConvertByCommandAsync } from "./media-utils.js";
// 同步转码示例(MP4转MKV,指定编码器和分辨率)
async function convertMp4ToMkvSync() {
const inputPath = "/Users/test/media/sample.mp4";
const outputPath = "/Users/test/media/sample.mkv";
try {
await formatConvertByCommand(inputPath, outputPath, {
videoCodec: "libx265", // 更高效的视频编码
audioCodec: "mp3", // 音频转MP3
size: "1280x720", // 缩放到720P
audioBitrate: "192k", // 音频比特率192k
videoBitrate: "1000k", // 视频比特率1000k
});
console.log("同步转码完成!");
} catch (error) {
console.error("同步转码失败:", error.message);
}
}
// 异步转码示例(AVI转MP4,直接拷贝视频流)
async function convertAviToMp4Async() {
const inputPath = "/Users/test/media/sample.avi";
const outputPath = "/Users/test/media/sample.mp4";
try {
await formatConvertByCommandAsync(inputPath, outputPath, {
videoCodec: "copy", // 直接拷贝视频流,不重新编码(速度快)
audioCodec: "aac", // 音频重新编码为AAC
});
console.log("异步转码完成!");
} catch (error) {
console.error("异步转码失败:", error.message);
}
}
// 执行转码
convertMp4ToMkvSync();
convertAviToMp4Async();
5.3 典型返回结果示例
{
"size": 12582912,
"birthtime": "2024-01-01T10:00:00.000Z",
"mtime": "2024-01-02T15:30:00.000Z",
"atime": "2024-01-03T09:15:00.000Z",
"isFile": true,
"fileExt": "mp4",
"fileName": "sample.mp4",
"fullPath": "/Users/test/media/sample.mp4",
"formatName": "mp4",
"formatLongName": "MPEG-4 Part 14",
"duration": 120.5,
"bitrate": 838860,
"streams": [
{
"type": "video",
"codecName": "h264",
"codecLongName": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
"profile": "High",
"width": 1920,
"height": 1080,
"frameRate": 30,
"sampleRate": null,
"channels": null,
"channelLayout": null,
"bitrate": 699000,
"duration": 120.5,
"codecTag": "avc1",
"disposition": {
"default": 1,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0,
"timed_thumbnails": 0
}
},
{
"type": "audio",
"codecName": "aac",
"codecLongName": "AAC (Advanced Audio Coding)",
"profile": "LC",
"width": null,
"height": null,
"frameRate": null,
"sampleRate": 44100,
"channels": 2,
"channelLayout": "stereo",
"bitrate": 128000,
"duration": 120.5,
"codecTag": "mp4a",
"disposition": {
"default": 1,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0,
"timed_thumbnails": 0
}
}
],
"audioStreams": [
/* 音频流详情,同上述audio类型流 */
],
"videoStreams": [
/* 视频流详情,同上述video类型流 */
],
"subtitleStreams": []
}
六、工程化实践建议
6.1 错误处理优化
增加文件格式白名单校验(如仅允许 mp4、mp3、flv、avi、mkv 等常见格式),避免解析不支持的文件导致报错;
对
duration、bitrate等核心字段增加默认值处理(如duration || 0),防止部分异常文件导致数据缺失;转码时增加输出目录检查,若输出目录不存在则自动创建:
const outputDir = path.dirname(outputFilePath); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); // 递归创建目录 }
6.2 性能优化
对于大文件(如 GB 级),FFprobe 解析耗时较长,可通过
timeout设置超时时间:ffmpeg.ffprobe(file, { timeout: 30000 }, (err, metadata) => { ... }); // 30秒超时转码时优先使用
copy编码器(直接拷贝流,不重新编码),大幅提升速度:// 示例:仅转换容器格式,不重新编码 await formatConvertByCommand(inputPath, outputPath, { videoCodec: "copy", audioCodec: "copy", });若仅需部分信息(如仅时长),可通过 FFprobe 参数过滤,减少解析开销:
ffmpeg.ffprobe(file, { show_entries: "format=duration" }, (err, metadata) => { ... });
6.3 跨平台兼容
Windows 系统下文件路径需注意转义(使用
path.resolve处理路径,避免\转义问题);生产环境中,若部署在 Linux 服务器,需确保
ffmpeg-static和ffprobe-static适配对应架构(x64/arm64):# 安装指定架构的静态包(以arm64为例) npm install ffmpeg-static@latest --arch=arm64 --platform=linux中文路径兼容:确保 Node.js 进程的字符编码为 UTF-8(Linux/macOS 默认支持,Windows 需设置
process.env.LANG = "zh_CN.UTF-8")。
6.4 安全考量
避免直接使用用户输入的文件路径,需进行路径校验(如
path.resolve后检查是否在指定目录内),防止路径遍历攻击:const allowedDir = "/Users/test/media"; const resolvedPath = path.resolve(inputFilePath); if (!resolvedPath.startsWith(allowedDir)) { throw new Error("禁止访问非授权目录的文件"); }转码时限制输出文件大小,通过
fs.watch监听输出文件,超过阈值则终止进程;避免使用
exec执行用户可控的参数,防止命令注入攻击(如过滤;、|、&&等特殊字符)。
七、常见问题排查
7.1 FFprobe/FFmpeg 路径配置错误
现象:报错ffprobe not found、spawn ffprobe ENOENT或找不到FFmpeg可执行文件。
解决方案:
检查
ffmpeg-static/ffprobe-static是否安装成功,可通过console.log(ffmpegPath)打印路径验证;生产环境(Electron)确保路径替换正确(
app.asar→app.asar.unpacked);手动指定系统级 FFmpeg 路径(若已安装):
ffmpeg.setFfmpegPath("/usr/local/bin/ffmpeg"); // macOS/Linux // ffmpeg.setFfmpegPath("C:\\ffmpeg\\bin\\ffmpeg.exe"); // Windows
7.2 解析/转码中文路径文件失败
现象:中文文件名或路径导致解析报错、转码无输出文件。
解决方案:
- 使用
path.resolve处理路径,确保编码正确; - 原生命令行调用时,路径需用双引号包裹(本文实现已处理);
- Windows 系统下,确保 Node.js 使用 UTF-8 编码(启动脚本时添加
chcp 65001)。
7.3 转码耗时过长/内存溢出
现象:大文件转码卡住、进程内存占用过高。
解决方案:
降低转码参数(如分辨率改为 720P、比特率降低);
ffmpeg(inputFilePath) .output(outputFilePath) .videoCodec("libx264") .audioCodec("aac") .size("1280x720") // 降低分辨率 .audioBitrate("128k") // 音频码率 .videoBitrate("500k") // 视频码率 .run();使用
exec异步执行命令,避免execSync堵塞事件循环;拆分大文件分段转码,最后通过 FFmpeg 合并:
# 合并命令示例(需先生成文件列表)
ffmpeg -f concat -safe 0 -i filelist.txt -c copy output.mp4
7.4 编码器不支持
现象:转码报错Unknown encoder 'libx265'或Codec not found。
解决方案:
- 检查
ffmpeg-static是否包含对应编码器(部分精简版静态包缺少小众编码器); - 安装系统级 FFmpeg(编译时开启全编码器),并指定系统路径;
- 替换为通用编码器(如
libx264替代libx265、aac替代mp3)。
八、总结
本文基于Node.js + FFmpeg实现了一套完整的音视频文件信息解析与格式转换方案,核心包含:
- 基于
FFprobe的媒体元数据深度解析,覆盖文件系统信息、格式信息、流信息等维度; - 两种格式转换实现方式(封装 API + 原生命令行),兼顾易用性与灵活性;
- 跨环境适配、错误处理、性能优化等工程化实践建议。