You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Node.js调用FFmpeg分块处理Buffer时内存泄漏及视频花屏问题

问题根因

代码存在4个核心逻辑错误,直接导致视频花屏、内存占用异常:

  • 剩余Buffer拷贝逻辑完全错误:取完单帧数据后,留存剩余数据的copy调用源起始偏移计算错误,且拷贝长度不匹配。原逻辑取buffer.length - bufferData.length作为拷贝起点,正确起点应为已消费的单帧长度colorsLength;同时目标leftBuffer仅分配了剩余数据长度的空间,但错误偏移下待拷贝的源数据长度远大于目标空间,Buffer.copy会自动截断拷贝内容,直接导致每帧的中间段数据大量丢失、帧首错位,是视频顶部花屏的核心原因。
  • 帧处理触发条件遗漏:仅当拼接后Buffer长度大于单帧长度时才暂停流处理,忽略了长度等于单帧长度的场景,会导致整帧数据滞留内存,后续新数据进入后帧边界完全错乱。
  • 事件依赖错误:将帧处理逻辑绑定在pause事件上,但Node.js流调用pause()后不会同步触发pause事件,流可能还会吐出缓存的剩余数据块,导致拼接逻辑时序错乱;且每次pause回调仅处理1帧,若缓存数据已足够多帧,剩余帧会滞留在内存中,增加内存占用。
  • 未处理流背压:调用encode.stdin.write()时未判断返回值,当编码进程输入缓冲区已满时,待写入数据会全部缓存在Node.js内存中,既会造成内存持续上涨,也可能导致数据写入顺序错乱。另外流结束时直接写入所有剩余缓存,会把不足一帧的无效残片送入编码器,造成尾帧花屏。
修复后代码
const { spawn } = require('child_process');
const path = require('path');

const width = 854;
const height = 480;
const channels = 3;
const fps = 23.97602397602398;
const frameSize = width * height * channels;

// 解码进程配置
const decArgs = [
    '-i', path.join(__dirname, 'public/test.mp4'),
    '-an',
    '-pix_fmt', 'bgr24',
    '-f', 'rawvideo',
    '-avioflags', 'direct', // 关闭ffmpeg侧输出缓存,降低chunk波动
    '-'
];
const decode = spawn('ffmpeg', decArgs, {
    stdio: ['ignore', 'pipe', 'inherit'] // stderr直接输出到控制台,方便排查报错
});

// 编码进程配置
const encArgs = [
    '-f', 'rawvideo',
    '-pix_fmt', 'bgr24',
    '-s', `${width}x${height}`,
    '-r', fps.toString(),
    '-i', '-',
    '-c:v', 'libx264',
    '-preset', 'ultrafast',
    '-crf', '30',
    '-pix_fmt', 'yuv420p',
    '-r', fps.toString(),
    '-y',
    path.join(__dirname, 'public/output.mp4')
];
const encode = spawn('ffmpeg', encArgs, {
    stdio: ['pipe', 'ignore', 'inherit'] // stderr直接输出到控制台
});

let chunkCache = Buffer.alloc(0);
let canWrite = true;

// 编码缓冲区清空后恢复解码流
encode.stdin.on('drain', () => {
    canWrite = true;
    decode.stdout.resume();
});

decode.stdout.on('data', (data) => {
    chunkCache = Buffer.concat([chunkCache, data]);

    // 循环处理所有缓存中的完整帧
    while (chunkCache.length >= frameSize) {
        // 截取单帧数据,subarray为内存视图,无额外拷贝开销
        const frame = chunkCache.subarray(0, frameSize);
        chunkCache = chunkCache.subarray(frameSize);

        // 写入编码端,处理背压
        canWrite = encode.stdin.write(frame);
        if (!canWrite) {
            decode.stdout.pause();
            return;
        }
    }
});

decode.stdout.on('end', () => {
    // 丢弃最后不足一帧的无效残片,避免尾帧花屏
    encode.stdin.end();
});

// 监听进程异常退出
decode.on('exit', (code) => {
    if (code !== 0) console.error(`解码进程异常退出,退出码:${code}`);
});
encode.on('exit', (code) => {
    if (code !== 0) console.error(`编码进程异常退出,退出码:${code}`);
});
优化说明
  • 移除对pause事件的依赖,帧处理逻辑全部在data事件内同步完成,避免事件时序导致的逻辑错乱
  • 用Buffer.subarray替代手动alloc+copy做切片,无额外内存拷贝,从根源上降低内存泄漏风险
  • 新增流背压处理:编码缓冲区满时自动暂停解码,待缓冲区清空后恢复,避免内存无限制增长
  • 将两个FFmpeg进程的stderr配置为inherit,运行时可直接看到FFmpeg的日志与报错,无需额外监听打印
  • 流结束时丢弃不足一帧的无效残片,避免残片数据被编码器识别为损坏帧导致尾帧花屏
  • 给解码进程新增-avioflags direct参数,关闭FFmpeg侧的输出缓存,降低stdout输出的数据块大小波动,减少内存缓存压力

内容的提问来源于stack exchange,提问作者imagesck

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 12:09:45