Node.js如何分块读取流数据 解决ffmpeg编码中途冻结问题
问题核心原因
你的代码存在4个关键逻辑错误,直接导致编码提前冻结、尾部内容丢失:
- 事件绑定逻辑完全错误:Node.js流的
pause事件仅在流状态从「流动」切换为「暂停」时触发,不是调用pause()方法就会100%同步执行回调,你把帧切分逻辑完全绑定在这个事件上,会出现凑够整帧但逻辑不触发、帧处理时序混乱的问题。 - 缓冲区偏移计算错误:提取完一帧数据后,你计算剩余数据起始偏移的公式
buffer.length - bufferData.length是错的——正确的剩余数据起始位置应该是已提取的帧长度colorsLength,你写的公式仅在缓冲区总长度恰好为2倍单帧大小时生效,只要缓冲区长度超过2倍帧大小,就会丢失中间的帧数据,累计下来就会出现总时长少2秒的问题。 - 未处理流背压:直接调用
encode.stdin.write()不判断返回值,当编码端ffmpeg的输入缓冲区占满时,写入的数据会被丢弃或者造成流阻塞,是进程无报错冻结的核心诱因。 - 结尾逻辑不完整:解码流结束后,你直接把所有剩余缓冲区全部写入编码端,既不判断是否为完整帧,也没有主动调用
encode.stdin.end()通知编码端输入完成,ffmpeg会一直等待后续输入导致永久挂起。
修复后可运行代码
const { spawn } = require('child_process'); const path = require('path'); const decArgs = [ '-i', path.join(__dirname, '/public/future.mkv'), '-an', '-pix_fmt', 'rgb32', '-f', 'rawvideo', '-' ]; const decode = spawn('ffmpeg', decArgs, { stdio: [ 'ignore', 'pipe', 'ignore' ] }); // 编码参数配置 const width = 854; const height = 480; const channels = 4; const fps = '29.97002997002997'; const frameSize = width * height * channels; // 单帧字节大小 const encArgs = [ '-pix_fmt', 'rgb32', '-f', 'rawvideo', '-s', `${width}x${height}`, '-r', fps, '-i', '-', '-c:v', 'libx264', '-preset', 'ultrafast', '-crf', '30', '-pix_fmt', 'yuv420p', '-r', fps, '-y', path.join(__dirname, '/public/output.mp4') ]; const encode = spawn('ffmpeg', encArgs, { stdio: [ 'pipe', 'ignore', 'pipe' ] }); let buffer = Buffer.alloc(0); // 处理编码端背压:缓冲区满时暂停解码,排空后恢复 encode.stdin.on('drain', () => { decode.stdout.resume(); }); decode.stdout.on('data', data => { buffer = Buffer.concat([buffer, data]); // 循环切分完整帧,不依赖pause事件 while (buffer.length >= frameSize) { // 提取一帧完整数据 const frameData = buffer.subarray(0, frameSize); // 保留剩余未处理数据,修正偏移计算 buffer = buffer.subarray(frameSize); // 写入编码端,若缓冲区满则暂停解码流 if (!encode.stdin.write(frameData)) { decode.stdout.pause(); } } }); decode.stdout.on('end', () => { // 末尾剩余不足一帧的冗余数据直接丢弃,写完主动关闭编码输入流 encode.stdin.end(); }); // 打印编码端日志方便排查问题 encode.stderr.on('data', data => { console.log(data.toString()); }); // 监听进程退出,捕获异常 encode.on('exit', code => { console.log(`编码进程退出,退出码:${code}`); }); decode.on('exit', code => { console.log(`解码进程退出,退出码:${code}`); });
关键修改说明
- 帧切分逻辑直接放在
data事件回调中,用循环持续切分所有完整帧,pause()/resume()仅用于流控,不再承载业务逻辑,避免事件时序问题 - 用
Buffer.subarray()替代手动拷贝+偏移计算,直接截取缓冲区片段,从根源避免偏移计算错误,性能也比手动copy更高 - 新增
drain事件监听,处理流背压:编码端输入缓冲区满时自动暂停解码,等缓冲区排空后再恢复读取,避免数据丢失和进程阻塞 - 解码流结束后主动调用
encode.stdin.end()通知ffmpeg输入完成,避免进程永久等待挂起;末尾不足一帧的冗余数据是rawvideo流的正常填充数据,丢弃不影响实际视频内容 - 新增进程退出监听,方便后续排查运行异常
内容的提问来源于stack exchange,提问作者imagesck
相关产品推荐
相关产品推荐

