基于Node.js + FFmpeg 读取音视频文件信息


前言:什么是 FFmpeg 和 FFprobe

FFmpeg是一个强大的跨平台音视频处理工具,它可以处理几乎所有常见的音视频格式,实现格式转换、编解码、流媒体处理等功能。而FFprobe则是FFmpeg套件中的一个工具,专门用于分析媒体文件的元数据信息,包括文件格式、编码信息、时长、比特率等。

Node.js环境中,我们可以通过fluent-ffmpeg库来便捷地使用FFmpeg的功能,同时通过ffprobe-static获取FFprobe工具的静态版本。

一、技术栈选型与核心工具介绍

1.1 核心工具

  • FFmpeg:跨平台的音视频处理瑞士军刀,支持格式转换、编解码、流媒体处理等功能,是音视频领域的工业级标准工具。
  • FFprobeFFmpeg套件内置的媒体元数据分析工具,专门用于提取音视频文件的详细信息(格式、时长、流信息等),性能高效且解析维度全面。
  • 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通过读取音视频文件的头部信息和流数据,解析出标准化的元数据结构。其核心工作流程如下:

  1. 打开目标媒体文件,识别文件容器格式(如 MP4、MKV、MP3 等);
  2. 遍历文件中的媒体流(音频流、视频流、字幕流等);
  3. 提取每个流的关键参数(编码格式、采样率、分辨率、帧率等);
  4. 汇总文件级元数据(时长、比特率、文件大小等);
  5. 以结构化数据(JSON 格式)返回解析结果。

Node.js 中,fluent-ffmpeg封装了FFprobe的命令行调用,通过回调或Promise方式返回解析结果,避免了直接操作命令行的复杂性;而原生child_process模块则可直接执行FFmpeg命令行,适配更灵活的自定义转码需求。

三、完整实现方案

3.1 基础配置:FFmpeg/FFprobe 路径设置

由于ffmpeg-staticffprobe-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对象包含海量信息,我们筛选并结构化了核心字段:

  1. 格式信息format_name(如 mp4)、format_long_name(如 MPEG-4 Part 14),用于格式校验;
  2. 核心参数duration(时长)、bitrate(比特率),用于转码参数决策;
  3. 流信息
    • 音频流:采样率、声道数、编码格式(如 aac);
    • 视频流:分辨率、帧率、编码格式(如 h264);
    • 字幕流:字幕编码、语言等;
  4. 分离audioStreamsvideoStreams等数组,方便业务直接使用(如仅处理音频流)。

4.3 原生命令行转码核心亮点

  1. 路径安全处理:通过path.resolve和引号包裹路径,兼容含空格、中文的文件路径;
  2. 灵活参数配置:支持自定义视频编码器、音频编码器、分辨率、比特率等核心参数;
  3. 超时控制:设置timeout避免大文件转码无限制堵塞;
  4. 进度监听:通过监听stderr输出,解析 FFmpeg 原生的时间进度信息;
  5. 错误详情输出:捕获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 等常见格式),避免解析不支持的文件导致报错;

  • durationbitrate等核心字段增加默认值处理(如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-staticffprobe-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 foundspawn ffprobe ENOENT找不到FFmpeg可执行文件
解决方案

  • 检查ffmpeg-static/ffprobe-static是否安装成功,可通过console.log(ffmpegPath)打印路径验证;

  • 生产环境(Electron)确保路径替换正确(app.asarapp.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替代libx265aac替代mp3)。

八、总结

本文基于Node.js + FFmpeg实现了一套完整的音视频文件信息解析与格式转换方案,核心包含:

  1. 基于FFprobe的媒体元数据深度解析,覆盖文件系统信息、格式信息、流信息等维度;
  2. 两种格式转换实现方式(封装 API + 原生命令行),兼顾易用性与灵活性;
  3. 跨环境适配、错误处理、性能优化等工程化实践建议。

文章作者: 弈心
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 弈心 !
评论
  目录