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
相关产品推荐
相关产品推荐

