NATS Broker传输流至NestJS:浏览器无法播放问题排查
问题分析与解决方案
问题场景
基于Express封装FFmpeg实现RTSP转MPEGTS流,将流发布至NATS JetStream,通过NestJS接口对外提供流服务。VLC和ffplay可正常播放,但HTML的video标签无法播放。相关代码如下:
FFmpeg转流参数
const options = [ "-rtsp_transport", "tcp", "-i", streamUrl, '-f', 'mpegts', '-pix_fmt', 'yuv420p', '-c:v', 'h264', '-b:v', '800k', '-r', '30', '-muxdelay', '0.4', '-movflags', 'frag_keyframe+empty_moov', '-' ];
FFmpeg流发布至NATS代码
try { const natsConnection = await nats.connect({ servers: 'ws://127.0.0.1:9222', preserveBuffers: true }) const stream = spawn('ffmpeg', options); stream.stdout.on('data', (chunk) => { console.log(chunk); natsConnection.publish('stream.1', chunk); }); stream.stdout.on('error', (error) => { console.error('FFmpeg error:', error); }); stream.stdout.on('close', (code) => { console.log('FFmpeg process closed with code:', code); }); } catch (e) { console.error(e); }
NestJS控制器代码
@Get() async findOne( @Headers() headers, @Res() res, ) { const streamObservable = this.natsService.subscribeToCameraEvents('1'); // 设置视频流响应头 res.setHeader('Content-Type', 'video/mp4'); res.setHeader('Transfer-Encoding', 'chunked'); res.status(200); // 将数据块写入响应 const subscription = streamObservable.subscribe({ next: (chunk) => { console.log('wwww', chunk); res.write(chunk); }, error: (error) => { console.error('Error streaming video:', error); }, complete: () => { res.end(); }, }); res.on('close', () => { subscription.unsubscribe(); }); }
NATS订阅服务代码
subscribeToCameraEvents(cameraId: string) { const subject = new Subject<any>(); this.natsConnection.subscribe(`stream.${cameraId}`, { callback: (err, msg: Msg) => { if (err) { subject.error(err); return; } subject.next(msg.data); }, }); return subject.asObservable(); }
前端播放页面
<!DOCTYPE html> <html> <head> <title>Video Streaming Example</title> </head> <body> <video controls width="640" height="480" style="background: black"> <source src="http://localhost:3301/api/streams" type="video/mp4"> Your browser does not support the video tag. </video> </body> </html>
核心错误点
容器格式与MIME类型不匹配
FFmpeg输出的是MPEG-TS流,但响应头设置为video/mp4。浏览器的video标签对MP4容器有严格要求(必须包含可流式解析的moov原子等),而TS流的正确MIME类型是video/MP2T,两者格式不兼容导致浏览器无法解析。缺少音频轨道
VLC和ffplay可以容忍无音频的视频流,但浏览器的video标签通常要求必须包含音频轨道(即使是静音),否则会拒绝播放。NATS消息分片破坏媒体流结构
FFmpeg输出的是连续的媒体字节流,但NATS发布的消息是按stdout的chunk分片发送的,可能存在关键帧被拆分的情况,导致浏览器无法正确识别媒体帧的边界,进而无法解码播放。
解决方案
方案1:修正容器格式与MIME类型
选项A:继续使用TS流,修正响应头
将NestJS控制器中的Content-Type改为TS流的正确类型:
res.setHeader('Content-Type', 'video/MP2T');
同时前端source的type也对应修改:
<source src="http://localhost:3301/api/streams" type="video/MP2T">
选项B:转成适合浏览器的分段MP4(fmp4)
修改FFmpeg参数,输出可流式播放的分段MP4:
const options = [ "-rtsp_transport", "tcp", "-i", streamUrl, '-f', 'mp4', '-pix_fmt', 'yuv420p', '-c:v', 'h264', '-b:v', '800k', '-r', '30', '-muxdelay', '0.4', '-movflags', 'frag_keyframe+empty_moov+default_base_moof', // 添加default_base_moof确保分片可独立解析 '-frag_duration', '1000000', // 每1秒生成一个分片 '-' ];
此时响应头保持video/mp4即可。
方案2:添加音频轨道
如果原RTSP流没有音频,添加静音音频轨道满足浏览器要求,修改FFmpeg参数:
const options = [ "-rtsp_transport", "tcp", "-i", streamUrl, "-f", "lavfi", "-i", "anullsrc=channel_layout=stereo:sample_rate=44100", // 注入静音音频源 '-f', 'mp4', '-pix_fmt', 'yuv420p', '-c:v', 'h264', '-b:v', '800k', '-r', '30', '-c:a', 'aac', '-b:a', '128k', // 将音频编码为AAC(浏览器支持的格式) '-muxdelay', '0.4', '-movflags', 'frag_keyframe+empty_moov+default_base_moof', '-frag_duration', '1000000', '-' ];
方案3:优化NATS流的分片传输
- 发布端优化:缓存FFmpeg输出的chunk,等到关键帧边界再发布NATS消息,避免关键帧被拆分。可以通过监听FFmpeg的输出,检测H.264关键帧的起始码(0x00000001)来判断边界。
- 订阅端优化:在NestJS服务中拼接收到的NATS消息,确保输出到响应的是连续、完整的媒体流,避免碎片化导致的解析失败。
额外优化建议
- 移除NestJS控制器中的
res.status(200)调用,因为流式传输时响应会自动设置200状态码,提前设置可能导致响应头发送时机异常。 - 确保NATS连接的
preserveBuffers保持为true,避免数据被转换为字符串导致字节丢失。
内容的提问来源于stack exchange,提问作者О. Войтенко
相关产品推荐
相关产品推荐

