前文介绍了如何通过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扩展点击后弹出页面效果,右边是虚拟摄像头内容