ASP.NET Core中实现AXIS摄像机RTSP录制流转发至浏览器播放的技术问题
解决Blazor客户端播放AXIS摄像机RTSP录制流的无延迟方案
首先得明确核心问题:你现在直接返回的是RTP封装的H264原始流,而HTML5的<video>元素根本不认识这种格式——浏览器需要的是带容器的视频流(比如MP4、WebM),而且容器结构必须支持流式播放(允许边下载边解码)。
为什么当前代码无法播放?
- 流格式不对:
RtspTcpClient.GetStream()返回的是包含RTP协议头的字节流,不是浏览器能解码的H264裸流,更没有容器结构。 - 缺少必要的解码信息:H264解码需要SPS(序列参数集)和PPS(图像参数集),这些必须在视频数据前传递给浏览器,否则解码器无法初始化。
无磁盘、低延迟的技术方向
下面是具体的实现思路,分为三个核心步骤:
1. 解析RTSP流,提取纯净的H264 NAL单元
不要直接操作RtspTcpClient的原始流,而是用RtspClientSharp的媒体轨道API来自动处理RTP分片和NALU拼接:
// 替换原有的流获取逻辑,改用MediaStreamTrack var client = new RtspClient(connectionParameters); await client.ConnectAsync(cts.Token); await client.SetupTracksAsync(cts.Token); // 获取H264轨道 var h264Track = client.Tracks.OfType<H264StreamTrack>().FirstOrDefault(); if (h264Track == null) { throw new InvalidOperationException("No H264 track found in the RTSP stream"); } // 订阅帧接收事件,获取完整的H264 NALU h264Track.FrameReceived += (sender, args) => { // args.Frame.Buffer 就是完整的H264 NALU字节数组 // 这里需要把这些NALU传递给封装逻辑 }; // 开始播放流 await client.PlayAsync(cts.Token);
2. 将H264 NALU封装成支持流式播放的Fragmented MP4
Fragmented MP4(分片MP4)是浏览器原生支持的流式格式,它把视频分成多个小片段(moof+mdat),并且把关键的moov元数据放在流的最开头,这样浏览器可以边接收边解码,完全不需要写入磁盘。
你可以用第三方库简化封装工作,比如MP4Box.NET或者SharpMP4,也可以手动实现基础的封装逻辑(如果想控制细节)。核心要点:
- 首先提取H264的SPS和PPS(从第一个关键帧的NALU中获取),用它们构建MP4的moov原子。
- 为每个H264帧生成对应的moof(片段元数据)和mdat(帧数据)块,持续写入输出流。
3. 在ASP.NET Core中返回流式响应
创建一个双向管道(比如PipeStream),在后台线程中处理RTSP流的解析和MP4封装,然后把管道的读取端返回给客户端:
[HttpGet("RecordingsDemo")] public async Task<IActionResult> RecordingsDemo(CancellationToken ct) { var deviceIp = "rtsp://192.168.0.125"; var recordingUri = "rtsp://192.168.0.125/axis-media/media.amp?recordingid=20211109_140710_E1A3_B8A44F2D0300"; var credentials = new NetworkCredential("user", "password"); // 创建管道,用于后台封装和前端响应的桥接 var pipe = new Pipe(); var writer = pipe.Writer; var reader = pipe.Reader; // 后台线程处理RTSP流和MP4封装 _ = Task.Run(async () => { try { var connectionParams = new ConnectionParameters(new Uri(deviceIp), credentials); using var client = new RtspClient(connectionParams); await client.ConnectAsync(ct); await client.SetupTracksAsync(ct); var h264Track = client.Tracks.OfType<H264StreamTrack>().First(); var mp4Writer = new FragmentedMp4Writer(writer, h264Track.Sps, h264Track.Pps); // 初始化MP4头部(moov原子) await mp4Writer.WriteInitializationAsync(ct); // 处理每一个H264帧 var frameReceivedTask = new TaskCompletionSource<bool>(); void FrameHandler(object sender, FrameReceivedEventArgs args) { try { mp4Writer.WriteFrameAsync(args.Frame, ct).Wait(ct); } catch (OperationCanceledException) { frameReceivedTask.TrySetResult(true); } catch (Exception ex) { frameReceivedTask.TrySetException(ex); } } h264Track.FrameReceived += FrameHandler; await client.PlayAsync(ct); await frameReceivedTask.Task; } catch (Exception ex) { // 处理异常,比如写入错误信息到管道(可选) Console.WriteLine($"Stream processing failed: {ex.Message}"); } finally { await writer.CompleteAsync(); } }, ct); // 设置响应头 Response.Headers.Add("Cache-Control", "no-cache"); Response.Headers.Add("Accept-Ranges", "bytes"); // 返回管道的读取端作为流式响应 return new FileStreamResult(reader.AsStream(), "video/mp4") { EnableRangeProcessing = true }; }
简化替代方案:用FFmpeg转码(无需手动封装)
如果不想自己处理RTP解析和MP4封装,可以直接调用FFmpeg进程,把RTSP流实时转成浏览器支持的流式MP4,然后把FFmpeg的标准输出返回给客户端。这种方式代码更简洁,延迟也很低:
[HttpGet("RecordingsDemo")] public async Task<IActionResult> RecordingsDemo(CancellationToken ct) { var rtspUrl = "rtsp://user:password@192.168.0.125/axis-media/media.amp?recordingid=20211109_140710_E1A3_B8A44F2D0300"; // FFmpeg命令:复制H264编码,输出分片MP4,禁用缓存,直接输出到标准输出 var ffmpegArgs = $"-i \"{rtspUrl}\" -c:v copy -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof -"; var process = new ProcessStartInfo { FileName = "ffmpeg", Arguments = ffmpegArgs, RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true }; var ffmpegProcess = Process.Start(process); if (ffmpegProcess == null) { return StatusCode(StatusCodes.Status500InternalServerError); } // 确保进程在请求取消时终止 ct.Register(() => ffmpegProcess.Kill()); Response.Headers.Add("Cache-Control", "no-cache"); return new FileStreamResult(ffmpegProcess.StandardOutput.BaseStream, "video/mp4") { EnableRangeProcessing = true }; }
注意:需要在服务器上安装FFmpeg,并且确保ASP.NET进程有执行权限。
关键注意事项
- 响应头设置:一定要把
Content-Type设为video/mp4,而不是video/x-h264。 - 范围请求:启用
EnableRangeProcessing = true,这样浏览器可以跳过部分内容,提升播放体验。 - 取消令牌:一定要正确处理
CancellationToken,在客户端断开连接时终止RTSP客户端或FFmpeg进程,避免资源泄漏。
内容的提问来源于stack exchange,提问作者Alessandro Martinelli
相关产品推荐
相关产品推荐

