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

5
我有一个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" width="800" height="600" 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方法调用此方法时,我很确定录像已正确检索,从带宽和Wireshark的输出可以看出。您可以在下一张图片中看到Wireshark的输出,其中192.168.0.125是摄像头,而192.168.0.120是服务器。

stream from camera to server

然而,似乎服务器返回的文件无法播放。即使使用VLC也无法播放返回的文件或流。客户端与服务器之间的通信显示在下图中,其中192.168.0.16是客户端,192.168.0.51是服务器。

strema from server to client

我需要返回一个流,可以在html5视频元素中播放。请问你能指导我吗?谢谢。
编辑:如您所见,我已经找到了一种方法(下面有帖子)。但是我希望有更好的解决方案,不需要写入磁盘,并且不需要通过生成.ts文件来添加延迟。因此,如果有人愿意提供帮助,我会保持问题的开放状态。
1个回答

6

最终我成功实现了目标,尽管方式不同。以下是我需要完成的步骤,我已经详细说明了它们。

从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 参数进行转换录制内容。因为用户需要能够在录制中前后跳转,所以我必须设置 "delete no downloaded .ts segments"。请参见FFmpeg文档。我可以利用这个.m3u8播放器演示版来检查问题是由我生成的视频还是其他原因造成的。这个视频如何通过NodeJS、FFMPEG和ReactJS将IP摄像头RTSP流作为HLS流传输到浏览器也对我有所帮助。

从控制器返回.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的文章以及这篇有关实现动态控制器操作的帖子。

在浏览器中播放 .m3u8 文件

最终,为了在浏览器中播放 .m3u8 文件,我不得不使用这个HLS JavaScript 项目,我是通过这篇帖子发现的。

下面是一个我制作的可以正常工作的 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,它似乎更接近于你在代码中使用的库。你能确认一下吗? - Mephisto
不是RtspTransportTcpClient,而是RtspTcpTransportClient,在这里找到它 https://github.com/BogdanovKirill/RtspClientSharp/blob/master/RtspClientSharp/Rtsp/RtspTcpTransportClient.cs 相反,您可能会遇到RtspRequestMessage的问题,因为我使用了我自己添加的构造函数。我已更新问题,添加了新构造函数的代码。 - Alessandro Martinelli

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接