在 ASP.NET Core 中将 RTSP 流从 IP 摄像机转发到浏览器

Posted

技术标签:

【中文标题】在 ASP.NET Core 中将 RTSP 流从 IP 摄像机转发到浏览器【英文标题】:Forwarding RTSP stream from IP Camera to Browser in ASP.NET Core 【发布时间】:2021-12-22 06:02:04 【问题描述】:

我有一个 Blazor 托管应用程序,我需要根据客户端请求从 AXIS 摄像机(通过 RTSP PLAY 命令)获取 h264 记录,并以浏览器可以重现视频的方式返回它.如果向 AXIS 摄像机查询记录列表,答案包括这个,我试图在浏览器上播放的那个

    <recording diskid="SD_DISK" recordingid="20211109_122753_1AB3_B8A44F2D0300" starttime="2021-11-09T11:27:53.060281Z" starttimelocal="2021-11-09T12:27:53.060281+01:00" stoptime="2021-11-09T11:43:01.125987Z" stoptimelocal="2021-11-09T12:43:01.125987+01:00" recordingtype="continuous" eventid="continuous" eventtrigger="continuous" recordingstatus="completed" source="1" locked="No">
        <video mimetype="video/x-h264"   framerate="15:1" resolution="800x600"/>
    </recording>

我可以通过“打开网络流...”和输入的方式成功地用 VLC 重现录音

rtsp://192.168.0.125/axis-media/media.amp?recordingid=20211109_140710_E1A3_B8A44F2D0300

然后提供用户名和密码,所以我确定命令是正确的。通过在 url 中嵌入用户名和密码,录音也可以使用 this 项目播放,其中使用了我在下面使用的更简单的语法,所以我的示例可能有点过于复杂。

由于RtspClientSharp,服务器端我可以成功检索流,但我无法以正确的方式返回它。到目前为止,我有这个:

[HttpGet("RecordingsDemo")]
    public async Task<IActionResult> RecordingsDemo() 
        string deviceIp = "rtsp://192.168.0.125";
        string recordingUri = "rtsp://192.168.0.125/axis-media/media.amp?recordingid=20211109_140710_E1A3_B8A44F2D0300";
        Uri playRequestUri = new Uri(recordingUri);

        CancellationTokenSource cts = new CancellationTokenSource();
        NetworkCredential networkCredential = new NetworkCredential("user", "password");
        ConnectionParameters connectionParameters = new ConnectionParameters(new Uri(deviceIp), networkCredential);
        RtspTcpTransportClient RtspTcpClient = new RtspTcpTransportClient(connectionParameters);
        await RtspTcpClient.ConnectAsync(cts.Token);

        RtspRequestMessage message = new RtspRequestMessage(RtspMethod.SETUP, playRequestUri);
        message.AddHeader("Transport", "RTP/AVP/TCP;unicast");
        RtspResponseMessage response = await RtspTcpClient.EnsureExecuteRequest(message, cts.Token);
        System.Collections.Specialized.NameValueCollection headers = response.Headers;
        string sessionId = headers["SESSION"];
        if (sessionId == null)  throw new Exception("RTSP initialization failed: no session id returned from SETUP command"); 
        message = new RtspRequestMessage(RtspMethod.PLAY, playRequestUri, sessionId);
        response = await RtspTcpClient.EnsureExecuteRequest(message, cts.Token);
        Stream stream = RtspTcpClient.GetStream();
        if (stream != null) 
            Response.Headers.Add("Cache-Control", "no-cache");
            FileStreamResult result = new FileStreamResult(stream, "video/x-h264") 
                EnableRangeProcessing = true
            ;
            return result;
         else 
            return new StatusCodeResult((int)HttpStatusCode.ServiceUnavailable);
        
        return new StatusCodeResult((int)HttpStatusCode.OK);
    

请注意,在上面的代码中,我向 RtspRequestMessage 添加了一个构造函数,以便更快地构建它。特别是我添加了以下代码:

    public uint _lastCSeqUsed  get; private set; 

    /// <param name="method">SETUP, PLAY etc</param>
    /// <param name="connectionUri">rtsp://<servername>/axis-media/media.amp?recordingid=...</param>
    /// <param name="cSeq">Method that generate the sequence number. The receiver will reply with the same sequence number</param>
    /// <param name="protocolVersion">Default to 1.0 if omitted or null</param>
    /// <param name="userAgent">Doesn't matter really</param>
    /// <param name="session">This parameter has to be initialized with the value returned by the SETUP method</param>
    public RtspRequestMessage(RtspMethod method, Uri connectionUri, string session = "", Func<uint> cSeqProvider = null, 
        Version protocolVersion = null, string userAgent = "client")
        : base((protocolVersion != null) ? protocolVersion : new Version("1.0"))
    
        Method = method;
        ConnectionUri = connectionUri;
        UserAgent = userAgent;

        _cSeqProvider = (cSeqProvider != null) ? cSeqProvider : myfun;
        CSeq = (cSeqProvider != null) ? _cSeqProvider() : 0;

        if (!string.IsNullOrEmpty(session))
            Headers.Add("Session", session);
    

    public void AddHeader(string name, string value)
    
        Headers.Add(name, value);
    

    private uint myfun()
    
        return ++CSeq;
    

当客户端通过 GET 方法调用此方法时,我很确定通过查看 bandwitdh 和 Wireshark 可以正确检索到记录。在下图中可以看到 Wireshark 的输出,其中 192.168.0.125 是摄像头,192.168.0.120 是服务器。

但是,服务器返回的文件似乎无法播放。即使使用 VLC,我也无法播放返回的文件或流。 客户端-服务器通信如下图所示,其中 192.168.0.16 是客户端,192.168.0.51 是服务器。

我需要能够返回 html5 视频元素可以播放的流。

你能指出我正确的方向吗?谢谢

编辑: 如您所见,我找到了一种方法,贴在下面。但是我希望有一个更好的解决方案,不需要在磁盘上写入,也不需要生成 .ts 文件所增加的延迟。因此,如果有人愿意贡献,我将问题悬而未决。

【问题讨论】:

【参考方案1】:

我终于能够达到目标,即使不是以我想要的方式。这些是我必须完成的步骤,我在下面详细说明了它们。

从 RTSP 流生成 HTTP 流

从 RTSP 流生成 .m3u8 和 .ts 文件。 从控制器返回 .m3u8 文件。 播放返回 .m3u8 文件。这需要一个 javascript 库。

从 RTSP 流生成 .m3u8 和 .ts 文件

我使用以下 FFmpeg 命令从 RTSP 流中提取视频,这样我可以通过命令返回它

ffmpeg.exe -i rtsp://username:password@192.168.0.125/axis-media/media.amp?recordingid=20211109_122753_1AB3_B8A44F2D0300 -fflags flush_packets -max_delay 5 -flags -global_header -hls_time 3 -hls_list_size 0 -vcodec copy -y .\example.m3u8

我必须使用 -hls_list_size 0 因为在我的情况下我必须转换录音,并且由于用户需要能够在录音中来回查找,我必须设置“删除不下载的 .ts 段",请参阅FFmpeg documentation。我可以利用这个.m3u8 player demo 来检查问题是出在我生成的视频上还是其他问题上。这个视频How to stream IP Camera RTSP stream into browser as HLS via NodeJS, FFMPEG and ReactJS也帮助了我。

从控制器返回 .m3u8 文件

这里我遇到了两个问题:由于缺少跨域标头,请求被阻止。此外,一旦浏览器检索到 .m3u8 文件,它就可以向控制器请求 .ts 文件。所以我不得不像这样构造代码:

[ApiController]
[Route("[controller]")]
public class CameraSystemController : ControllerBase 
    [HttpGet("Example")]
    public async Task<IActionResult> Example() 
        Response.Headers.Add("Access-Control-Allow-Origin", "*");
        return File(System.IO.File.OpenRead("Output/Video/example.m3u8"), "application/octet-stream", enableRangeProcessing: true);
    

    [HttpGet("tsFileName")]
    public async Task<IActionResult> Example_GetTS(string tsFileName) 
        Response.Headers.Add("Access-Control-Allow-Origin", "*");
        return File(System.IO.File.OpenRead("Output/Video/" + tsFileName), "application/octet-stream", enableRangeProcessing: true);
    

感谢CORS 上的这篇文章和implementing dynamic controller action 上的这篇文章。

在浏览器中播放 .m3u8 文件

最后,为了在浏览器中播放 .m3u8 文件,我不得不使用 HLS javascript project,感谢 post。

我制作的工作 html 页面示例如下:

<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<title>Example</title>
  <link href="https://unpkg.com/video.js/dist/video-js.css" rel="stylesheet">
  <script src="https://unpkg.com/video.js/dist/video.js"></script>
  <script src="https://unpkg.com/videojs-contrib-hls/dist/videojs-contrib-hls.js"></script>
   
</head>
<body>
  <video id="my_video_1" class="video-js vjs-fluid vjs-default-skin" controls preload="auto"
  data-setup=''>
            <source src="http://localhost:5000/CameraSystem/Example" type="application/x-mpegURL">
  </video>
<script>
var player = videojs('my_video_1');
player.play();
</script>
</body>
</html>

【讨论】:

在你的问题中你说你正在使用 RtspClientCSharp,但我在其中找不到类 RtspTransportTcpClient。经过一番搜索,我找到了这个库:SharpRTSP,它似乎更接近您在代码中使用的库。你能确认一下吗? 它不是 RtspTransportTcpClient,它是 RtspTcpTransportClient,在这里找到github.com/BogdanovKirill/RtspClientSharp/blob/master/… 相反,你可能对 RtspRequestMessage 有问题,因为我使用了我自己添加的构造函数。我已经更新了为新构造函数添加代码的问题。

以上是关于在 ASP.NET Core 中将 RTSP 流从 IP 摄像机转发到浏览器的主要内容,如果未能解决你的问题,请参考以下文章

在 ASP.NET Core 中将 html 导出为 pdf

如何在 ASP.NET Core 中将角色添加到 Windows 身份验证

在 ASP.NET Core 中将 Razor 视图渲染为字符串

防止 AddRedirectToWwwPermanent() 在 ASP.NET Core 2.1 中将“www”添加到 *.azurewebsites.net 的前面

在 Asp.Net Core 2 中将 SignInManager 和 AspNetUserManager 注入中间件

如何在 ASP .NET Core 2.1 中将登录页面设为默认路由?