You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

527 lines
22 KiB

using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Application.Domain.Entities;
using Infrastructure.Data;
using Infrastructure.Extensions;
using Infrastructure.Models;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace ONVIFService
{
public class OnvifService : IDisposable
{
private readonly IHostingEnvironment _env;
private readonly IConfiguration _configuration;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServiceProvider _applicationServices;
private CancellationTokenSource _tokenSource;
private readonly ConcurrentDictionary<string, (Camera camera, Process local, Process remote)> _list = new ConcurrentDictionary<string, (Camera camera, Process local, Process remote)>();
private readonly IOnvifDeviceManagement _onvifDeviceManagement;
public OnvifService(IHostingEnvironment env, IServiceProvider applicationServices, IConfiguration configuration, IHttpClientFactory httpClientFactory)
{
this._env = env;
this._applicationServices = applicationServices;
this._configuration = configuration;
this._httpClientFactory = httpClientFactory;
this._tokenSource = new CancellationTokenSource();
this._onvifDeviceManagement = new OnvifDeviceManagement(httpClientFactory);
}
public void Start()
{
Task.Run(async () =>
{
while (!_tokenSource.IsCancellationRequested)
{
try
{
Search();
Notify();
}
catch (Exception ex)
{
ex.PrintStack();
}
await Task.Delay(this._configuration.GetValue<int>("timer.seconds") * 1000);
}
});
}
public void Search()
{
try
{
var list = this._onvifDeviceManagement.Discovery();
foreach (var ipCamera in list)
{
if (string.IsNullOrEmpty(ipCamera.Id) || string.IsNullOrEmpty(ipCamera.DeviceUrl))
{
continue;
}
Console.WriteLine(ipCamera.DeviceUrl);
using (var scope = _applicationServices.CreateScope())
{
var repo = scope.ServiceProvider.GetService<IRepository<Camera>>();
var camera = repo.Table().FirstOrDefault(o => o.Number == ipCamera.Id);
if (camera == null)
{
camera = new Camera
{
Name = "摄像头",
Number = ipCamera.Id,
Rtmp1 = $"rtmp://{this._configuration["stream.local.rtmp"]}/live/{ipCamera.Id}",
Flv1 = $"http://{this._configuration["stream.local.flv"]}/live/{ipCamera.Id}.flv",
Hls1 = $"http://{this._configuration["stream.local.hls"]}/live/{ipCamera.Id}.m3u8",
Rtmp2 = $"rtmp://{this._configuration["stream.remote.rtmp"]}/live/{ipCamera.Id}",
Flv2 = $"http://{this._configuration["stream.remote.flv"]}/live/{ipCamera.Id}.flv",
Hls2 = $"http://{this._configuration["stream.remote.hls"]}/live/{ipCamera.Id}.m3u8",
Arguments = this._configuration["ffmpeg.args"],
File = this._configuration["ffmpeg.file"]
};
repo.Add(camera);
}
var profiles = this._onvifDeviceManagement.GetProfiles(ipCamera.DeviceUrl, ipCamera.MediaUrl);
if (profiles == "")
{
camera.NeedAuth = true;
camera.UserName = camera.UserName ?? this._configuration["camera.usr"];
camera.Password = camera.Password ?? this._configuration["camera.pwd"];
profiles = this._onvifDeviceManagement.GetProfiles(ipCamera.DeviceUrl, ipCamera.MediaUrl, camera.UserName, camera.Password);
if (profiles == "")
{
camera.HasAuth = false;
}
else
{
camera.HasAuth = true;
ipCamera.GetProfilesXml = profiles;
}
}
else
{
ipCamera.GetProfilesXml = profiles;
}
if (!camera.NeedAuth || camera.HasAuth)
{
ipCamera.ParseProfiles();
ipCamera.StreamUriXml = camera.NeedAuth
? this._onvifDeviceManagement.GetStreamUri(ipCamera.DeviceUrl, ipCamera.MediaUrl, camera.UserName, camera.Password, ipCamera.Tokens.FirstOrDefault())
: this._onvifDeviceManagement.GetStreamUri(ipCamera.DeviceUrl, ipCamera.MediaUrl, ipCamera.Tokens.FirstOrDefault());
ipCamera.ParseStreamUri();
ipCamera.SnapshotUriXml = camera.NeedAuth
? this._onvifDeviceManagement.GetSnapshotUri(ipCamera.DeviceUrl, ipCamera.MediaUrl, camera.UserName, camera.Password, ipCamera.Tokens.FirstOrDefault())
: this._onvifDeviceManagement.GetSnapshotUri(ipCamera.DeviceUrl, ipCamera.MediaUrl, ipCamera.Tokens.FirstOrDefault());
ipCamera.ParseSnapshotUri();
}
camera.DeviceUrl = ipCamera.DeviceUrl;
camera.StreamUri = ipCamera.StreamUri;
camera.SnapshotUri = ipCamera.SnapshotUri;
camera.PtzAddress = ipCamera.PTZAddress;
camera.Ptz3DZoomSupport = ipCamera.Ptz3DZoomSupport;
camera.UpdatedOn = DateTime.Now;
repo.SaveChanges();
}
}
}
catch (Exception ex)
{
ex.PrintStack();
}
}
public void Notify()
{
var host = Helper.Instance.GetLocalIP().ToString();
var prot = Convert.ToInt32(Regex.Match(this._configuration["server.urls"], @"(?<=:)\d+").Value);
using (var scope = _applicationServices.CreateScope())
{
var repo = scope.ServiceProvider.GetService<IRepository<Camera>>();
var cameras = repo.ReadOnlyTable().Where(o => o.Enabled).ToList();
foreach (var key in this._list.Keys)
{
if (!cameras.Where(o => o.Publish).Where(o => !o.NeedAuth || (o.NeedAuth && o.HasAuth)).Any(o => o.Number == key))
{
this.Remove(key);
}
}
foreach (var camera in cameras)
{
try
{
var model = new NotifyModel
{
CategoryName = "安防",
CategoryNumber = "10",
Name = "摄像头",
Number = camera.Number,
Icon = "camera",
IsOnline = true,
BaseUrl = $"http://{host}:{prot}/camera",
ApiPath = "/api"
};
model.Data.Add(new DataModel { Type = DataValueType.Text.ToString(), Key = nameof(camera.Ptz3DZoomSupport), Name = "缩放支持", Value = camera.Ptz3DZoomSupport ? "是" : "否", DisplayOrder = 1 });
model.Data.Add(new DataModel { Type = DataValueType.Text.ToString(), Key = "rtsp", Name = "rtsp", Value = camera.StreamUri, DisplayOrder = 2 });
model.Data.Add(new DataModel { Type = DataValueType.Text.ToString(), Key = "rtmp1", Name = "rtmp1", Value = camera.Rtmp1, DisplayOrder = 3 });
model.Data.Add(new DataModel { Type = DataValueType.Text.ToString(), Key = "flv1", Name = "flv1", Value = camera.Flv1, DisplayOrder = 4 });
model.Data.Add(new DataModel { Type = DataValueType.Text.ToString(), Key = "hls1", Name = "hls1", Value = camera.Hls1, DisplayOrder = 5 });
model.Data.Add(new DataModel { Type = DataValueType.Text.ToString(), Key = "rtmp2", Name = "rtmp2", Value = camera.Rtmp2, DisplayOrder = 6 });
model.Data.Add(new DataModel { Type = DataValueType.Text.ToString(), Key = "flv2", Name = "flv2", Value = camera.Flv2, DisplayOrder = 7 });
model.Data.Add(new DataModel { Type = DataValueType.Text.ToString(), Key = "hls2", Name = "hls2", Value = camera.Hls2, DisplayOrder = 8 });
this.Update(model);
if (camera.Publish && (!camera.NeedAuth || camera.HasAuth))
{
this.Publish(camera);
}
}
catch (Exception ex)
{
ex.PrintStack();
}
}
}
}
private void Update(IPCamera ipCamera)
{
}
private void Publish(Camera camera)
{
try
{
if (!this._list.Any(o => o.Key == camera.Number))
{
this.Add(camera);
}
}
catch (Exception ex)
{
ex.PrintStack();
}
}
public void Add(string key)
{
using (var scope = _applicationServices.CreateScope())
{
var repo = scope.ServiceProvider.GetService<IRepository<Camera>>();
var camera = repo.ReadOnlyTable().FirstOrDefault(o => o.Number == key);
if (camera != null && camera.Enabled && camera.Publish && (!camera.NeedAuth || (camera.NeedAuth && camera.HasAuth)))
{
this.Add(camera);
}
}
}
public void Add(Camera camera)
{
if (this._list.Any(o => o.Key == camera.Number))
{
return;
}
var rtspUrl = $"rtsp://{(camera.NeedAuth ? $"{camera.UserName}:{camera.Password}@" : "")}{camera.StreamUri.Substring(7)}";
var fileName = $"ffmpeg-{Helper.Instance.GetRunTime()}{(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : "")}";
var file = Path.Combine(this._env.WebRootPath, fileName);
if (camera.UseCustomFile)
{
file = camera.File;
}
Console.WriteLine(file);
var arguments = camera.UseCustomArguments ? camera.Arguments : this._configuration["ffmpeg.args"];
Process local = null, remote = null;
if (!string.IsNullOrEmpty(camera.StreamUri))
{
if (!string.IsNullOrEmpty(camera.Rtmp1))
{
local = this.SetProcess(file, string.Format(arguments, Environment.ProcessorCount, rtspUrl, camera.Rtmp1));
}
if (!string.IsNullOrEmpty(camera.Rtmp2) && camera.Rtmp2 != camera.Rtmp1)
{
remote = this.SetProcess(file, string.Format(arguments, Environment.ProcessorCount, rtspUrl, camera.Rtmp2));
}
}
if (this._list.TryAdd(camera.Number, (camera, local, remote)))
{
Console.WriteLine($"add {camera.Number} to list");
}
else
{
if (local != null)
{
this.CloseProcess(local);
this.CloseProcess(remote);
}
}
}
public void Remove(string key)
{
var item = this._list[key];
this.CloseProcess(item.local);
this.CloseProcess(item.remote);
if (this._list.TryRemove(key, out item))
{
Console.WriteLine($"remove {key} from list");
}
}
private Process SetProcess(string file, string arguments)
{
Console.WriteLine(file);
var process = new Process
{
StartInfo = new ProcessStartInfo
{
WorkingDirectory = this._env.WebRootPath,
FileName = file,
Arguments = arguments,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardError = true
},
EnableRaisingEvents = true
};
process.Exited += (s, e) =>
{
var _process = s as Process;
Console.WriteLine($"list:{_list.Count},exit:{_process.ExitCode},args:{arguments}");
if (_process != null)
{
Thread.Sleep(10 * 1000);
_process.CancelErrorRead();
_process.Start();
_process.BeginErrorReadLine();
}
};
process.ErrorDataReceived += (s, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
{
Console.WriteLine(e.Data);
if (e.Data.IndexOf("forcing output") > -1)
{
process.Kill();
}
}
};
Console.WriteLine(arguments);
process.Start();
process.BeginErrorReadLine();
return process;
}
private void CloseProcess(Process process)
{
if (process != null)
{
process.EnableRaisingEvents = false;
try
{
process.Kill();
process.WaitForExit();
}
catch (Exception ex)
{
ex.PrintStack();
}
}
}
public void Update(NotifyModel model)
{
try
{
var url = $"http://{this._configuration["node.url"]}/Notify";
Console.WriteLine(url);
var hc = this._httpClientFactory.CreateClient();
var task = this._httpClientFactory.CreateClient().PostAsync(url, new FormUrlEncodedContent(model.ToList()));
task.Wait();
using (var response = task.Result)
{
using (var content = response.Content)
{
var value = content.ReadAsStringAsync().Result;
Console.WriteLine($"end:{url}:{value}");
}
}
}
catch (Exception ex)
{
ex.PrintStack();
}
}
private string GetOnoce(string deviceUrl)
{
var message = @"<s:Envelope xmlns:s=""http://www.w3.org/2003/05/soap-envelope"">
<s:Body xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema"">
<GetDeviceInformation xmlns=""http://www.onvif.org/ver10/device/wsdl""></GetDeviceInformation>
</s:Body>
</s:Envelope>";
var hc = this._httpClientFactory.CreateClient();
hc.DefaultRequestHeaders.Add("ContentType", $"application/soap+xml; charset=utf-8; action=\"{Template.GetDeviceInformationAction}\"");
var task = hc.PostAsync(deviceUrl, new StringContent(message));
var result = task.Result.Headers.WwwAuthenticate;
return Regex.Match(result.ToString(), "nonce=\"([^\"]+)\"").Groups[1].Value;
}
private string RequestXml(string url, string action, string body, string userName, string password, string onoce)
{
var nonce_b = Convert.FromBase64String(onoce);
var now = DateTime.Now.ToUniversalTime().ToString("yyyy-MM-ddThh:mm:ss.fffZ");
var creationtime_b = Encoding.ASCII.GetBytes(now);
var password_b = Encoding.ASCII.GetBytes(password);
var concatenation_b = new byte[nonce_b.Length + creationtime_b.Length + password_b.Length];
Buffer.BlockCopy(nonce_b, 0, concatenation_b, 0, nonce_b.Length);
Buffer.BlockCopy(creationtime_b, 0, concatenation_b, nonce_b.Length, creationtime_b.Length);
Buffer.BlockCopy(password_b, 0, concatenation_b, nonce_b.Length + creationtime_b.Length, password_b.Length);
var sha = new SHA1CryptoServiceProvider();
var pdresult = sha.ComputeHash(concatenation_b);
var passworddigest = Convert.ToBase64String(pdresult);
var message = string.Format(Template.AuthTemplate, userName, passworddigest, Convert.ToBase64String(nonce_b), now, body);
var result = SoapRequest(url, action, message);
return result;
}
private string SoapRequest(string url, string action, string message)
{
var hc = this._httpClientFactory.CreateClient();
hc.DefaultRequestHeaders.Add("ContentType", $"application/soap+xml; charset=utf-8; action=\"{action}\"");
var task = hc.PostAsync(url, new StringContent(message));
var result = task.Result.Content.ReadAsStringAsync().Result;
return result;
}
private string RegexMatch(string input, string pattern, string prefix, string suffix)
{
return Regex.Match(input, $"(?<={prefix}){pattern}(?={suffix})").Value;
}
public void ZoomIn(string id, float speed)
{
this.Move(id, speed, 0, 0);
}
public void ZoomOut(string id, float speed)
{
this.Move(id, -speed, 0, 0);
}
public void StopZoom(string id)
{
this.Stop(id, false, true);
}
public void Up(string id, float speed)
{
this.Move(id, 0, 0, speed);
}
public void Right(string id, float speed)
{
this.Move(id, 0, speed, 0);
}
public void Down(string id, float speed)
{
this.Move(id, 0, 0, -speed);
}
public void Left(string id, float speed)
{
this.Move(id, 0, -speed, 0);
}
public void StopTurn(string id)
{
this.Stop(id, true, false);
}
public void Move(string id, float zx, float px, float py)
{
var camera = this.GetCamera(id);
if (camera != null)
{
var ptzAddress = camera.PtzAddress;
if (!string.IsNullOrEmpty(ptzAddress))
{
var deviceUrl = camera.DeviceUrl;
RequestXml(ptzAddress, Template.ContinuousMoveAction, String.Format(Template.ContinuousMoveMessage, zx, px, py), camera.UserName, camera.Password, GetOnoce(deviceUrl));
}
}
}
public void Stop(string id, bool panTilt, bool zoom)
{
var camera = this.GetCamera(id);
if (camera != null)
{
var ptzAddress = camera.PtzAddress;
if (!string.IsNullOrEmpty(ptzAddress))
{
var deviceUrl = camera.DeviceUrl;
RequestXml(ptzAddress, Template.StopAction, String.Format(Template.StopMessage, panTilt, zoom), camera.UserName, camera.Password, GetOnoce(deviceUrl));
}
}
}
public byte[] ScreenShot(string id)
{
var camera = this.GetCamera(id);
var url = camera.SnapshotUri;
var hc = this._httpClientFactory.CreateClient();
if (!camera.NeedAuth)
{
return hc.GetByteArrayAsync(url).Result;
}
return hc.GetByteDigest(url, camera.UserName, camera.Password);
}
private Camera GetCamera(string number)
{
using (var scope = _applicationServices.CreateScope())
{
var repo = scope.ServiceProvider.GetService<IRepository<Camera>>();
return repo.ReadOnlyTable().FirstOrDefault(o => o.Number == number);
}
}
public void Clear()
{
foreach (var item in this._list.Keys.ToList())
{
this.Remove(item);
}
}
public void Dispose()
{
Console.WriteLine("OnvifService dispose...");
try
{
this._tokenSource.Cancel();
foreach (var item in this._list.Keys.ToList())
{
this.Remove(item);
}
}
catch (Exception ex)
{
ex.PrintStack();
}
}
}
}