using Application.Domain.Entities; using Infrastructure.Data; using Infrastructure.Extensions; using IoT.Shared.Application.Models; using IoT.Shared.Services; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; using System.Collections.Concurrent; using System.Collections.Generic; 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; namespace IoTNode.DeviceServices.Onvif { public class OnvifService : BaseDeviceService, IDisposable { private bool isDisposed; private readonly ConcurrentDictionary _list = new ConcurrentDictionary(); private readonly ILogger _logger; private readonly IHttpClientFactory _httpClientFactory; private readonly IOnvifDeviceManagement _onvifDeviceManagement; public OnvifService(IServiceProvider applicationServices, ILogger logger, IWebHostEnvironment env, IHttpClientFactory httpClientFactory, IOnvifDeviceManagement onvifDeviceManagement) : base(applicationServices, env) { this._logger = logger; this._httpClientFactory = httpClientFactory; this._onvifDeviceManagement = onvifDeviceManagement; } public override void Execute() { try { Search(); Notify(); } catch (Exception ex) { ex.PrintStack(); } } public override Task StopAsync(CancellationToken cancellationToken) { Stop(); return Task.CompletedTask; } private void Stop() { this._logger.LogDebug("OnvifService>Onvif Service Dispose"); try { foreach (var item in this._list.Keys.ToList()) { try { this.StopPushToServer(item); } catch (Exception ex) { ex.PrintStack(); this._logger.LogError(ex.ToString()); } } } catch (Exception ex) { ex.PrintStack(); this._logger.LogError(ex.ToString()); } finally { this._logger.LogDebug($"{this.GetType().Name} Service Stopd"); } } public void Search() { try { var list = this._onvifDeviceManagement.Discovery(); this.UpdateOnlineStatus(list.Select(o=>o.Id).ToList()); foreach (var ipCamera in list) { if (string.IsNullOrEmpty(ipCamera.Id) || string.IsNullOrEmpty(ipCamera.DeviceUrl)) { continue; } var writeList = GetSetting("camera.writelist"); if (!string.IsNullOrEmpty(writeList)) { var snList = writeList.Split(','); if (!snList.Contains(ipCamera.Id)) { this._logger.LogDebug($"skip {ipCamera.Id} because it isn't contains by writelist"); continue; } } this._logger.LogDebug(ipCamera.DeviceUrl); try { using var scope = _applicationServices.CreateScope(); var iotNodeClient = scope.ServiceProvider.GetService(); var productNumber = "onvifcamera"; var productRepo = scope.ServiceProvider.GetService>(); var product = this.UpdateProduct("摄像头", productNumber, "/Onvif/", "camera"); var deviceNodeRepo = scope.ServiceProvider.GetService>(); var node = deviceNodeRepo.ReadOnlyTable().FirstOrDefault(); var deviceRepo = scope.ServiceProvider.GetService>(); var device = deviceRepo.Table().FirstOrDefault(o => o.Number == ipCamera.Id); var timestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(); if (device == null) { device = new IoTDevice { Name = "摄像头", DisplayName = "摄像头", Number = ipCamera.Id, Ip = ipCamera.Ip, Icon = "camera", IsOnline = true, IoTProductId = product.Id, IoTGatewayId = node.Id }; deviceRepo.Add(device); } else { device.Ip = ipCamera.Ip; } deviceRepo.SaveChanges(); this.UpdateIoTData(device.Id, DataKeys.DeviceUrl, ipCamera.DeviceUrl); this.UpdateIoTData(device.Id, DataKeys.MediaUrl, ipCamera.MediaUrl); this.UpdateIoTData(device.Id, DataKeys.PtzAddress, ipCamera.PTZAddress); this.UpdateIoTData(device.Id, DataKeys.Ptz3DZoomSupport, ipCamera.Ptz3DZoomSupport ? "1" : "0"); var fileName = $"ffmpeg-{Helper.Instance.GetRunTime()}{(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : "")}"; var file = Path.Combine(this._env.WebRootPath, fileName); this.AddIoTData(device.Id, DataKeys.Push, "是"); this.AddIoTData(device.Id, DataKeys.Record, "否"); this.UpdateIoTData(device.Id, DataKeys.FFmpegFile, file); this.UpdateIoTData(device.Id, DataKeys.FFmpegArgs, GetSetting("ffmpeg.args")); var profiles = this._onvifDeviceManagement.GetProfiles(ipCamera.DeviceUrl, ipCamera.MediaUrl); var needAuth = false; var hasAuth = false; if (string.IsNullOrEmpty(profiles)) { needAuth = true; device.UserName ??= GetSetting("camera.usr"); device.Password ??= GetSetting("camera.pwd"); deviceRepo.SaveChanges(); profiles = this._onvifDeviceManagement.GetProfiles(ipCamera.DeviceUrl, ipCamera.MediaUrl, device.UserName, device.Password); if (string.IsNullOrEmpty(profiles)) { hasAuth = false; } else { hasAuth = true; ipCamera.GetProfilesXml = profiles; } } else { ipCamera.GetProfilesXml = profiles; } ipCamera.ParseProfiles(); try { if (ipCamera.Profiles.Count > 0) { var tokens = ipCamera.Profiles.ToJson(); this.UpdateIoTData(device.Id, DataKeys.Profiles, tokens); var token = this.GetIoTDataValue(device.Id, DataKeys.ProfileToken); if (string.IsNullOrEmpty(token)|| !ipCamera.Profiles.Any(o => o.Token == token)) { token = (ipCamera.Profiles.FirstOrDefault(o=>o.Width==1280)??ipCamera.Profiles.First()).Token; } if (needAuth) { ipCamera.StreamUriXml = this._onvifDeviceManagement.GetStreamUri(ipCamera.DeviceUrl, ipCamera.MediaUrl, device.UserName, device.Password, token); ipCamera.SnapshotUriXml = this._onvifDeviceManagement.GetSnapshotUri(ipCamera.DeviceUrl, ipCamera.MediaUrl, device.UserName, device.Password, token); } else { ipCamera.StreamUriXml = this._onvifDeviceManagement.GetStreamUri(ipCamera.DeviceUrl, ipCamera.MediaUrl, token); ipCamera.SnapshotUriXml = this._onvifDeviceManagement.GetSnapshotUri(ipCamera.DeviceUrl, ipCamera.MediaUrl, token); } ipCamera.ParseStreamUri(); ipCamera.ParseSnapshotUri(); this.UpdateIoTData(device.Id, DataKeys.ProfileToken, token); this.UpdateIoTData(device.Id, DataKeys.StreamUri, ipCamera.StreamUri); this.UpdateIoTData(device.Id, DataKeys.SnapshotUri, ipCamera.SnapshotUri); } } catch (Exception ex) { ex.PrintStack(ex.Message); } this.UpdateIoTData(device.Id, DataKeys.NeedAuth, needAuth ? "是" : "否"); this.UpdateIoTData(device.Id, DataKeys.HasAuth, hasAuth ? "是" : "否"); } catch (Exception ex) { ex.PrintStack(); } } } catch (Exception ex) { ex.PrintStack(); } } private void UpdateOnlineStatus(List list) { using var scope = _applicationServices.CreateScope(); var repo = scope.ServiceProvider.GetService>(); var devicesList = repo.Table().Where(o => o.Name == "摄像头").ToList(); foreach (var item in devicesList) { item.IsOnline = list.Any(o=>o==item.Number); } repo.SaveChanges(); } public void Notify() { using var scope = _applicationServices.CreateScope(); var repo = scope.ServiceProvider.GetService>(); var devicesList = repo.ReadOnlyTable().Where(o => o.Name == "摄像头").ToList(); foreach (var number in this._list.Keys) { var device = devicesList.FirstOrDefault(o => o.Number == number); var remove = false; if (this.GetIoTDataValue(device.Id, DataKeys.Push) == "否") { remove = true; } if (this.GetIoTDataValue(device.Id, DataKeys.NeedAuth) == "是" && this.GetIoTDataValue(device.Id, DataKeys.HasAuth) == "否") { remove = true; } if (remove) { this.StopPushToServer(number); } } foreach (var device in devicesList) { try { if (this.GetIoTDataValue(device.Id, DataKeys.Push) == "是") { if (this.GetIoTDataValue(device.Id, DataKeys.NeedAuth) == "否" || this.GetIoTDataValue(device.Id, DataKeys.HasAuth) == "是") { this.StartPushToServer(device); } } } catch (Exception ex) { ex.PrintStack(); } } } public void StartPushToServer() { using var scope = _applicationServices.CreateScope(); var repo = scope.ServiceProvider.GetService>(); var devices = repo.ReadOnlyTable().Where(o => o.Name == "摄像头").ToList(); foreach (var device in devices) { try { if (this.GetIoTDataValue(device.Id, DataKeys.Push) == "是") { if (this.GetIoTDataValue(device.Id, DataKeys.NeedAuth) == "否" && this.GetIoTDataValue(device.Id, DataKeys.HasAuth) == "是") { this.StartPushToServer(device); } } } catch (Exception ex) { ex.PrintStack(); } } } public void StartPushToServer(IoTDevice device) { try { if (!this._list.Any(o => o.Key == device.Number)) { this.Add(device); } } catch (Exception ex) { ex.PrintStack(); } } public void Add(string key) { var device = this.GetIoTDevice(key); if (device != null && this.GetIoTDataValue(device.Id,DataKeys.Push) == "是") { if (this.GetIoTDataValue(device.Id, DataKeys.NeedAuth) == "否" || this.GetIoTDataValue(device.Id, DataKeys.HasAuth) == "是") { this.Add(device); } } } public void Add(IoTDevice device) { if (this._list.Any(o => o.Key == device.Number)) { return; } var needAuth = this.GetIoTDataValue(device.Id, DataKeys.NeedAuth) == "是"; var hasAuth = this.GetIoTDataValue(device.Id, DataKeys.HasAuth) == "是"; var streamUri = this.GetIoTDataValue(device.Id, DataKeys.StreamUri); var rtspUrl = $"rtsp://{(needAuth ? $"{device.UserName}:{device.Password}@" : "")}{streamUri.Substring(7)}"; var fileName = $"ffmpeg-{Helper.Instance.GetRunTime()}{(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : "")}"; var file = this.GetIoTDataValue(device.Id, DataKeys.FFmpegFile); var rtmp = $"rtmp://{GetSetting("stream.rtmp")}/live/{device.Number}"; //camera.Data.FirstOrDefault(o => o.Key == "rtmp").Value; this._logger.LogDebug(file); var arguments = this.GetIoTDataValue(device.Id, DataKeys.FFmpegArgs); Process process = null; if (!string.IsNullOrEmpty(streamUri)) { if (!string.IsNullOrEmpty(rtmp)) { process = this.SetProcess(file, string.Format(arguments, Environment.ProcessorCount, rtspUrl, rtmp)); } } if (this._list.TryAdd(device.Number, process)) { this._logger.LogDebug($"add {device.Number} to list"); } else { if (process != null) { this.CloseProcess(process); } } } public void StopPushToServer() { foreach (var item in this._list.Keys) { try { this.StopPushToServer(item); } catch (Exception ex) { ex.PrintStack(); } } } public void StopPushToServer(string key) { if (this._list.TryRemove(key, out Process process)) { this._logger.LogDebug($"OnvifService>remove {key} from list"); this.CloseProcess(process); } } private Process SetProcess(string file, string arguments) { this._logger.LogDebug(file); this._logger.LogDebug(arguments); 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; this._logger.LogDebug($"ffmpeg processes list:{_list.Count},exit:{_process?.ExitCode},args:{arguments}"); if (_process != null) { Thread.Sleep(3000); _process.CancelErrorRead(); _process.Start(); _process.BeginErrorReadLine(); } }; process.ErrorDataReceived += (s, e) => { if (!string.IsNullOrEmpty(e.Data)) { if (GetSetting("debug") == "true") { this._logger.LogDebug(e.Data); } if (e.Data.IndexOf("forcing output") > -1) { process.Kill(); } } }; this._logger.LogDebug(arguments); try { process.Start(); process.BeginErrorReadLine(); } catch (Exception ex) { ex.PrintStack($"start ffmpeg error:{ex.Message}"); } return process; } private void CloseProcess(Process process) { this._logger.LogInformation($"close process:{process?.StartInfo?.FileName} with {process?.StartInfo?.Arguments}"); try { if (process != null) { process.EnableRaisingEvents = false; process?.Kill(); } } catch (Exception ex) { ex.PrintStack($"error when close ffmpeg process {ex.Message}"); } finally { process?.Dispose(); } } private string GetOnoce(string deviceUrl) { var message = @" "; var hc = this._httpClientFactory.CreateClient(); hc.DefaultRequestHeaders.Add("ContentType", $"application/soap+xml; charset=utf-8; action=\"{MessageTemplate.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(MessageTemplate.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; } public void ChangeToken(string id,string token) { var device = this.GetIoTDevice(id); if (device != null) { using var scope = _applicationServices.CreateScope(); var repo = scope.ServiceProvider.GetRequiredService>(); var profiles = this.GetIoTDataValue(device.Id,DataKeys.Profiles); if (!string.IsNullOrEmpty(profiles)) { var tokens = profiles.FromJson>(); if(tokens.Any(o=>o.Token==token)) { var currentToken = this.GetIoTDataValue(device.Id, DataKeys.ProfileToken); if (!string.IsNullOrEmpty(currentToken) && currentToken != token) { this.UpdateIoTData(device.Id, DataKeys.ProfileToken, token); var deviceUrl = this.GetIoTDataValue(device.Id, DataKeys.DeviceUrl); var mediaUrl = this.GetIoTDataValue(device.Id, DataKeys.MediaUrl); if (!string.IsNullOrEmpty(deviceUrl) && !string.IsNullOrEmpty(mediaUrl)) { var streamUriXml = this._onvifDeviceManagement.GetStreamUri(deviceUrl, mediaUrl, device.UserName, device.Password, token); var snapshotUri = this._onvifDeviceManagement.GetSnapshotUri(deviceUrl, mediaUrl, device.UserName, device.Password, token); var ipCamera = new IPCamera { StreamUriXml = streamUriXml, SnapshotUriXml = snapshotUri }; ipCamera.ParseStreamUri(); ipCamera.ParseSnapshotUri(); this.UpdateIoTData(device.Id, DataKeys.StreamUri, ipCamera.StreamUri); this.UpdateIoTData(device.Id, DataKeys.SnapshotUri, ipCamera.SnapshotUri); this.SetPush(device.Number, false); this.SetPush(device.Number, true); } } } } } } 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 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 Stop(string id) { var device = this.GetIoTDevice(id); if (device != null) { var ptzAddress = this.GetIoTDataValue(device.Id, DataKeys.PtzAddress); if (!string.IsNullOrEmpty(ptzAddress)) { var deviceUrl = this.GetIoTDataValue(device.Id, DataKeys.DeviceUrl); var onoce = (this._onvifDeviceManagement as OnvifDeviceManagement).GetOnoce(deviceUrl); var token = this.GetIoTDataValue(device.Id, DataKeys.ProfileToken); RequestXml(ptzAddress, MessageTemplate.StopAction, string.Format(MessageTemplate.StopMessage, token, true, true), device.UserName, device.Password, onoce); } } } public void Move(string id, float zx, float px, float py) { var device = this.GetIoTDevice(id); if (device != null) { var ptzAddress = this.GetIoTDataValue(device.Id, DataKeys.PtzAddress); if (!string.IsNullOrEmpty(ptzAddress)) { var deviceUrl = this.GetIoTDataValue(device.Id, DataKeys.DeviceUrl); var onoce = (this._onvifDeviceManagement as OnvifDeviceManagement).GetOnoce(deviceUrl); var token = this.GetIoTDataValue(device.Id, DataKeys.ProfileToken); RequestXml(ptzAddress, MessageTemplate.ContinuousMoveAction, String.Format(MessageTemplate.ContinuousMoveMessage, token, zx, px, py), device.UserName, device.Password, onoce); } } } public byte[] ScreenShot(string id) { var device = this.GetIoTDevice(id); var url = this.GetIoTDataValue(device.Id, DataKeys.SnapshotUri); if (!string.IsNullOrEmpty(url)) { var hc = this._httpClientFactory.CreateClient(); if (this.GetIoTDataValue(device.Id, DataKeys.NeedAuth) == "否") { return hc.GetByteArrayAsync(url).Result; } else { if (this.GetIoTDataValue(device.Id, DataKeys.HasAuth) == "是") { return hc.GetByteDigest(new Uri(url), device.UserName, device.Password); } } } return Array.Empty(); } public void SetPush(string number, bool state) { var device = this.GetIoTDevice(number); if (device != null) { this.UpdateIoTData(device.Id, DataKeys.Push, state ? "是" : "否"); if (state) { this.StartPushToServer(device); } else { this.StopPushToServer(device.Number); } } } public void SetRecord(string number, bool state) { using var scope = _applicationServices.CreateScope(); var repo = scope.ServiceProvider.GetService>(); var device = repo.ReadOnlyTable().FirstOrDefault(o => o.Number == number); if (device != null) { var data = this.UpdateIoTData(device.Id, DataKeys.Record, state ? "是" : "否"); var iotNodeClient = scope.ServiceProvider.GetService(); iotNodeClient.ClientToServer(Methods.UpdateDvr, data,null); } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (isDisposed) return; if (disposing) { this.Stop(); } isDisposed = true; } } }