前文介绍了如何通过SignalR将捕获到的摄像头信息,通过Streaming的方式传输到客户端。在PiMonitor例子中,作者在后台通过SignalR后台服务的方式,隔一段时间获取图像帧,然后连接Azure的认知服务来判断Baby是否在哭,从而向客户端触发提醒,这里就用到了SignalR的后台服务以及涉及到了开发Chrome插件来作为SignalR的客户端,下面逐个介绍。

SignalR后台服务

   要在后台长期运行,并且能够通过SignalR向客户端发送消息,就需要用到SignalR的后台服务(background service)。ASP.NET Core承载SignalR后台服务,跟承载SignalR的Hub相似。只需要在ConfigurationServices中AddSignalR以及AddHostedService中注册需要的后台服务类.

public void ConfigureServices(IServiceCollection services)
{
    services.AddSignalR();
    services.AddHostedService<CameraWorker>();
    services.AddSingleton<CameraService>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...
    app.UseEndpoints(endpoints =>
    {
        ...
        endpoints.MapHub<CameraStreamHub>("/CameraR");
    });
}

      这里我们是CameraWorker类,后台服务类需要继承自BackgroundService类,并重写ExecuteAsync方法,在构造函数中,通过IHubContext<CameraStreamHub>参数注入对SignalR对象的操作,CameraWorker类的具体内容如下:

public class CameraWorker : BackgroundService
{
    private readonly IHubContext<CameraStreamHub> cameraStreamHub;
    private readonly CameraService cameraService;


    public CameraWorker(IHubContext<CameraStreamHub> hub, CameraService service)
    {
        cameraStreamHub = hub;
        cameraService = service;
        cameraService.Start();
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var keyvalue = CameraStreamHub.isStreamRunning.FirstOrDefault();
            if (keyvalue.Value)
            {
                var stream = cameraService.CapturePictureStream();
                if (stream != null)//detect baby cray
                {
                    await cameraStreamHub.Clients.All.SendAsync("ReceiveNotification", "Baby Crying Detected! You want to start streaming?");
                }
            }
            await Task.Delay(10000);
        }
    }
}

   在构造函数中,注入了IHubContext和CameraService,分别用来向SignalR客户单发送通知和获取摄像头头像。

   在ExecuteAsync方法中,每隔10秒,循环获取摄像头获取的图像,然后连接Azure的认知服务,判断小孩是否在哭(这里我没有连Azure认知服务,实际中可以根据逻辑调整),如果为true,则通过Clinets.All.SendAsync向所有处于连接中的客户端发送通知。至此SignalR的后台服务部分就完成了。具体的后台服务的详细内容,可以查看MSDN上 Host ASP.NET Core SignalR in background services 的内容。

SignalR Chrome Extension客户端

   Chrome的扩展(Extension)是可以定制的,一般人都将其称为插件,在Chrome浏览器输入chrome://extensions/ 就可以打开。 比如我们在调试API接口时的Postman插件,调试JavaScript的FireBug,禁用广告和弹出的插件AdBlock等等,这些可以在Chrome的插件页面配置,默认插件只能在Chrome应用商店里面获取,如果开启开发者模式,可以加载本地插件。

▲ Chrome插件配置

    本文不专门讲解Chrome插件开发,这里只参考PiMonitor里的实现,更多的Chrome插件开发可以查看Chrome论坛(需要梯子)以及这篇文章。 要新建一个Chrome扩展,在项目文件夹下新建一个ChromeExtensions文件夹,然后添加一些image,js和html,以及最重要的manifest.json,manifest.json文件定义了Chrome扩展的一些基本信息,展示页面等,写完后工程结构如下:

▲ 项目结构,红色部分是Chrome扩展

 

本例中manifest.json的内容如下:

{
  "name": "Pi MonitR Client",
  "version": "1.0",
  "description": "Real time Streaming from Raspnerry PI using SignalR",
  "browser_action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "images/16.png",
      "32": "images/32.png",
      "48": "images/48.png",
      "128": "images/128.png"
    }
  },
  "icons": {
    "16": "images/16.png",
    "32": "images/32.png",
    "48": "images/48.png",
    "128": "images/128.png"
  },
  "permissions": [
    "tabs",
    "notifications",
    "http://*/*"
  ],
  "manifest_version": 2,
  "web_accessible_resources": [
    "images/*.png"
  ]
}

    上面的manifest.json中,browser_action节点定义了点击后展示的主页面popup.html以及图标等信息,本例中移除了原例中不工作的background中的js定义,将这些js移到了popup.html页面中,在页面中引用了signalr.js和自定义的popup.js。popup.html内容为:

<!doctype html>
<html>

<head>
    <title>Pi MonitR Dashboard</title>
</head>

<body>
    <h1>Pi MonitR - Stream Dashboard</h1>
    <div>
        <input type="button" id="streamStartButton" value="Start Streaming" />
        <input type="button" id="streamStopButton" value="Stop Streaming" disabled />
    </div>
    <ul id="logContent"></ul>
    <img id="streamContent" width="700" height="400" src="" />
</body>
</html>
<script src="signalr.js" type="text/javascript"></script>
<script src="popup.js" type="text/javascript"></script>

   很简单,跟前文的页面基本一样,定义了两个按钮,一个开始,一个结束,以及一个image标签,在末尾已用了两个js,这两个js直接放在项目中新建的ChromeExtension文件夹下,signalr.js就是从ms的js库拷贝过来的,popup.js库的内容如下:

var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};

const hubUrl = "https://localhost:44303/CameraR"
//const hubUrl = "/CameraR"
var connection = new signalR.HubConnectionBuilder()
    .withUrl(hubUrl, { logger: signalR.LogLevel.Information })
    .build();

chrome.runtime.onConnect.addListener(function (externalPort) {
    externalPort.onDisconnect.addListener(function () {
        connection.invoke("StopStream").catch(err => console.error(err.toString()));
    });
});

// We need an async function in order to use await, but we want this code to run immediately,
// so we use an "immediately-executed async function"
(() => __awaiter(this, void 0, void 0, function* () {
    try {
        yield connection.start();
    }
    catch (e) {
        console.error(e.toString());
    }
}))();

const streamStartButton = document.getElementById('streamStartButton');
const streamStopButton = document.getElementById('streamStopButton');
const streamContent = document.getElementById('streamContent');
const logContent = document.getElementById('logContent');
connection.start();
streamStartButton.addEventListener("click", (event) => __awaiter(this, void 0, void 0, function* () {
    streamStartButton.setAttribute("disabled", "disabled");
    streamStopButton.removeAttribute("disabled");
    try {
        connection.stream("StartStream")
            .subscribe({
                next: (item) => {
                    streamContent.src = "data:image/jpg;base64," + item;
                },
                complete: () => {
                    var li = document.createElement("li");
                    li.textContent = "Stream completed";
                    logContent.appendChild(li);
                },
                error: (err) => {
                    var li = document.createElement("li");
                    li.textContent = err;
                    logContent.appendChild(li);
                },
            });
    }
    catch (e) {
        console.error(e.toString());
    }
    event.preventDefault();
}));

streamStopButton.addEventListener("click", function () {
    streamStopButton.setAttribute("disabled", "disabled");
    streamStartButton.removeAttribute("disabled");
    connection.invoke("StopStream").catch(err => console.error(err.toString()));
    event.preventDefault();
});

connection.on("ReceiveNotification", (message) => {
    new Notification(message, {
        icon: '48.png',
        body: message
    });
});
connection.on("StopStream", () => {
    var li = document.createElement("li");
    li.textContent = "stream closed";
    logContent.appendChild(li);
    streamStopButton.setAttribute("disabled", "disabled");
    streamStartButton.removeAttribute("disabled");
});

   是不跟跟上文里很相似,streamStartButton注册了点击后通过connection.stream订阅了StartStream事件,当服务端有数据写入时,这里就能接收到数据,并且更新image标签予以展示。这里connection.on订阅了“ReceiveNotification”事件,新建了一个Notification对象,这个事件是在SignalR的后台服务中触发的,当后台服务每隔10秒判断从摄像头读取的数据里小孩如果在哭泣(也可以是其他逻辑),则发出“ReceiveNotification”事件,这样Chrome浏览器就会接到通知,在Win10系统上效果如下:

▲ Chrome 通知(Notification)

    然后在Chrome的插件管理器里面,点击图1中的“加载已解压的扩展程序”,在弹出的文件选择器中,指向ChromeExtension文件夹即可安装插件。

▲ 自定义插件的样子,点击右边刷新按钮可以重新加载修改的内容

  还有一点需要注意的是,由于是在Chrome里扩展里通过JavaScript调用SignalR服务端代码,所以会提示CORS同源问题,需要在ASP.NET Core里支持CORS,解决方法参考这个issure,具体如下,新建CorsMiddleware.cs类,内容如下:

public class CorsMiddleware
{
    private readonly RequestDelegate _next;

    public CorsMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public Task Invoke(HttpContext httpContext)
    {
        if (httpContext.Request.Headers.TryGetValue("Origin", out var originValue))
        {
            httpContext.Response.Headers.Add("Access-Control-Allow-Credentials", "true");
            httpContext.Response.Headers.Add("Access-Control-Allow-Headers", "x-requested-with");
            httpContext.Response.Headers.Add("Access-Control-Allow-Methods", "POST,GET,OPTIONS");
            httpContext.Response.Headers.Add("Access-Control-Allow-Origin", originValue);

            if (httpContext.Request.Method == "OPTIONS")
            {
                httpContext.Response.StatusCode = 204;
                return httpContext.Response.WriteAsync(string.Empty);
            }
        }
        return _next(httpContext);
    }
}

public static class CorsMiddlewareExtensions
{
    public static IApplicationBuilder UseCorsMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<CorsMiddleware>();
    }
}

   然后在Startup里配置,即可支持CORS功能。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...
    app.UseCorsMiddleware();
    ...
}
运行以及效果

   现在,将ASP.NET Core承载的SignalR Hub和Background service的项目运行起来,可以看到,在Chrome浏览器的右上角点击之后,可以看到开发的插件,点击该插件后,就能弹出popup.html中定义的内容。

点击Pi MonitorR Client 即可运行起来,同时每个一段时间Chrome提醒会在Win10的右侧栏提醒,整个效果如下图:

▲Chrome扩展作为SignalR客户端显示的效果。左下,原SignalR JS效果,左上Chrome扩展点击后弹出页面效果,右边是虚拟摄像头内容