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

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>

核心错误点

  1. 容器格式与MIME类型不匹配
    FFmpeg输出的是MPEG-TS流,但响应头设置为video/mp4。浏览器的video标签对MP4容器有严格要求(必须包含可流式解析的moov原子等),而TS流的正确MIME类型是video/MP2T,两者格式不兼容导致浏览器无法解析。

  2. 缺少音频轨道
    VLC和ffplay可以容忍无音频的视频流,但浏览器的video标签通常要求必须包含音频轨道(即使是静音),否则会拒绝播放。

  3. 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,提问作者О. Войтенко

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 11:34:51