using Application.Domain.Entities; using Application.Models; using Infrastructure.Data; using Infrastructure.Extensions; using IoT.Shared.Services; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; 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; namespace IoTNode.DeviceServices.Onvif { public class OnvifService : BaseDeviceService, IDisposable { private readonly ConcurrentDictionary _list = new ConcurrentDictionary(); private readonly IWebHostEnvironment _env; private readonly IHttpClientFactory _httpClientFactory; private readonly IOnvifDeviceManagement _onvifDeviceManagement; public OnvifService(IServiceProvider applicationServices, IConfiguration configuration, IWebHostEnvironment env, IHttpClientFactory httpClientFactory) : base(applicationServices, configuration) { this._env = env; this._httpClientFactory = httpClientFactory; this._onvifDeviceManagement = new OnvifDeviceManagement(httpClientFactory); } 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() { Console.WriteLine("OnvifService>Onvif Service Dispose"); try { foreach (var item in this._list.Keys.ToList()) { try { this.StopPushToServer(item); } catch (Exception ex) { ex.PrintStack(); } } } catch (Exception ex) { ex.PrintStack(); } finally { Console.WriteLine($"{this.GetType().Name} Service Stopd"); } } public void Search() { try { var list = this._onvifDeviceManagement.Discovery(); foreach (var ipCamera in list) { if (string.IsNullOrEmpty(ipCamera.Id) || string.IsNullOrEmpty(ipCamera.DeviceUrl)) { continue; } var writeList = this._cfg["camera.writelist"]; if (!string.IsNullOrEmpty(writeList)) { var snList = writeList.Split(','); if (!snList.Contains(ipCamera.Id)) { Console.WriteLine($"skip {ipCamera.Id} because it isn't contains by writelist"); continue; } } Console.WriteLine(ipCamera.DeviceUrl); try { using (var scope = _applicationServices.CreateScope()) { var iotNodeClient = scope.ServiceProvider.GetService(); var productNumber = "onvifcamera"; var categoryRepo = scope.ServiceProvider.GetService>(); var category = categoryRepo.ReadOnlyTable().FirstOrDefault(o => o.Number == "10"); var productRepo = scope.ServiceProvider.GetService>(); var product = this.UpdateProduct("摄像头", productNumber, "/Onvif/", "10", "camera"); var deviceNodeRepo = scope.ServiceProvider.GetService>(); var node = deviceNodeRepo.ReadOnlyTable().FirstOrDefault(); var deviceRepo = scope.ServiceProvider.GetService>(); var device = deviceRepo.Table().Include(o => o.Data).FirstOrDefault(o => o.Number == ipCamera.Id); if (device == null) { device = new Device { Name = "摄像头", DisplayName = "摄像头", Number = ipCamera.Id, Ip = ipCamera.Ip, Icon = "camera", ProductId = product.Id, NodeId = node.Id }; deviceRepo.Add(device); this.UpdateDevice(deviceRepo, device); var fileName = $"ffmpeg-{Helper.Instance.GetRunTime()}{(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : "")}"; var file = Path.Combine(this._env.WebRootPath, fileName); var pushValue = device.Data.FirstOrDefault(o => o.Name == "推流")?.Value ?? "是"; this.UpdateData(deviceRepo, device, device.CreateData("Push", pushValue, DeviceDataType.String, "推流")); this.UpdateData(deviceRepo, device, device.CreateData("Record", "否", DeviceDataType.String, "录像")); this.UpdateData(deviceRepo, device, device.CreateData("ffmpeg.file", file, DeviceDataType.String, "ffmpeg路径", hidden: true)); this.UpdateData(deviceRepo, device, device.CreateData("ffmpeg.args", this._cfg["ffmpeg.args"], DeviceDataType.String, "ffmpeg.args", hidden: true)); this.UpdateData(deviceRepo, device, device.CreateData("mainrtmp", $"rtmp://{this._cfg["stream.rtmp"]}/live/main{ipCamera.Id}", DeviceDataType.String, "主码流rtmp")); this.UpdateData(deviceRepo, device, device.CreateData("mainflv", $"http://{this._cfg["stream.flv"]}/live/main{ipCamera.Id}.flv", DeviceDataType.String, "主码流flv")); this.UpdateData(deviceRepo, device, device.CreateData("mainhls", $"http://{this._cfg["stream.hls"]}/live/main{ipCamera.Id}.m3u8", DeviceDataType.String, "主码流hls")); this.UpdateData(deviceRepo, device, device.CreateData("subrtmp", $"rtmp://{this._cfg["stream.rtmp"]}/live/sub{ipCamera.Id}", DeviceDataType.String, "子码流rtmp")); this.UpdateData(deviceRepo, device, device.CreateData("subflv", $"http://{this._cfg["stream.flv"]}/live/sub{ipCamera.Id}.flv", DeviceDataType.String, "子码流flv")); this.UpdateData(deviceRepo, device, device.CreateData("subhls", $"http://{this._cfg["stream.hls"]}/live/sub{ipCamera.Id}.m3u8", DeviceDataType.String, "子码流hls")); } var profiles = this._onvifDeviceManagement.GetProfiles(ipCamera.DeviceUrl, ipCamera.MediaUrl); var needAuth = false; var hasAuth = false; if (profiles == "") { needAuth = true; device.UserName = device.UserName ?? this._cfg["camera.usr"]; device.Password = device.Password ?? this._cfg["camera.pwd"]; this.UpdateDevice(deviceRepo, device); profiles = this._onvifDeviceManagement.GetProfiles(ipCamera.DeviceUrl, ipCamera.MediaUrl, device.UserName, device.Password); if (profiles == "") { hasAuth = false; } else { hasAuth = true; ipCamera.GetProfilesXml = profiles; } } else { ipCamera.GetProfilesXml = profiles; } ipCamera.ParseProfiles(); try { if (ipCamera.Profiles.Count > 0) { if (needAuth) { ipCamera.MainStreamUriXml = this._onvifDeviceManagement.GetStreamUri(ipCamera.DeviceUrl, ipCamera.MediaUrl, device.UserName, device.Password, ipCamera.Profiles.First().Token); ipCamera.MainSnapshotUriXml = this._onvifDeviceManagement.GetSnapshotUri(ipCamera.DeviceUrl, ipCamera.MediaUrl, device.UserName, device.Password, ipCamera.Profiles.First().Token); ipCamera.SubStreamUriXml = this._onvifDeviceManagement.GetStreamUri(ipCamera.DeviceUrl, ipCamera.MediaUrl, device.UserName, device.Password, ipCamera.Profiles.Last().Token); ipCamera.SubSnapshotUriXml = this._onvifDeviceManagement.GetSnapshotUri(ipCamera.DeviceUrl, ipCamera.MediaUrl, device.UserName, device.Password, ipCamera.Profiles.Last().Token); } else { ipCamera.MainStreamUriXml = this._onvifDeviceManagement.GetStreamUri(ipCamera.DeviceUrl, ipCamera.MediaUrl, ipCamera.Profiles.First().Token); ipCamera.MainSnapshotUriXml = this._onvifDeviceManagement.GetSnapshotUri(ipCamera.DeviceUrl, ipCamera.MediaUrl, ipCamera.Profiles.First().Token); ipCamera.SubStreamUriXml = this._onvifDeviceManagement.GetStreamUri(ipCamera.DeviceUrl, ipCamera.MediaUrl, ipCamera.Profiles.Last().Token); ipCamera.SubSnapshotUriXml = this._onvifDeviceManagement.GetSnapshotUri(ipCamera.DeviceUrl, ipCamera.MediaUrl, ipCamera.Profiles.Last().Token); } ipCamera.ParseStreamUri(); ipCamera.ParseSnapshotUri(); this.UpdateData(deviceRepo, device, device.CreateData("MainToken", ipCamera.Profiles.First().Token, DeviceDataType.String, "主码流Token")); this.UpdateData(deviceRepo, device, device.CreateData("SubToken", ipCamera.Profiles.Last().Token, DeviceDataType.String, "子码流Token")); this.UpdateData(deviceRepo, device, device.CreateData("MainStreamUri", ipCamera.MainStreamUri, DeviceDataType.String, "主码流地址")); this.UpdateData(deviceRepo, device, device.CreateData("MainSnapshotUri", ipCamera.MainSnapshotUri, DeviceDataType.String, "主码流截图地址")); this.UpdateData(deviceRepo, device, device.CreateData("SubStreamUri", ipCamera.SubStreamUri, DeviceDataType.String, "子码流地址")); this.UpdateData(deviceRepo, device, device.CreateData("SubSnapshotUri", ipCamera.SubSnapshotUri, DeviceDataType.String, "子码流截图地址")); } } catch (Exception ex) { ex.PrintStack(ex.Message); } this.UpdateData(deviceRepo, device, device.CreateData("NeedAuth", needAuth ? "是" : "否", DeviceDataType.String, "需认证")); this.UpdateData(deviceRepo, device, device.CreateData("HasAuth", hasAuth ? "是" : "否", DeviceDataType.String, "已认证")); this.UpdateData(deviceRepo, device, device.CreateData("DeviceUrl", ipCamera.DeviceUrl, DeviceDataType.String, "设备地址")); this.UpdateData(deviceRepo, device, device.CreateData("PtzAddress", ipCamera.PTZAddress, DeviceDataType.String, "云台地址")); this.UpdateData(deviceRepo, device, device.CreateData("Ptz3DZoomSupport", ipCamera.Ptz3DZoomSupport ? "是" : "否", DeviceDataType.String, "缩放支持")); //deviceRepo.SaveChanges(); //var deviceDto = device.To(); //this.SendToServer(Methods.EditDevice, deviceDto); //foreach (var item in device.Data.Where(o => !o.Hidden)) //{ // this.SendDataToServer(item); //} } } catch (Exception ex) { ex.PrintStack(); } } } catch (Exception ex) { ex.PrintStack(); } } public void Notify() { using (var scope = _applicationServices.CreateScope()) { var repo = scope.ServiceProvider.GetService>(); var cameras = repo.ReadOnlyTable().Include(o => o.Data).Where(o => o.Name == "摄像头").ToList(); foreach (var key in this._list.Keys) { var camera = cameras.FirstOrDefault(o => o.Number == key); var remove = false; if (camera.Data.FirstOrDefault(o => o.Key == "Push").Value != "是") { remove = true; } if (camera.Data.FirstOrDefault(o => o.Key == "NeedAuth").Value == "是" && camera.Data.FirstOrDefault(o => o.Key == "HasAuth").Value == "否") { remove = true; } if (remove) { this.StopPushToServer(key); } } foreach (var camera in cameras) { try { if (camera.Data.Any(o => o.Key == "Push" && o.Value == "是")) { if (camera.Data.Any(o => o.Key == "NeedAuth" && o.Value == "否") || camera.Data.Any(o => o.Key == "HasAuth" && o.Value == "是")) { this.StartPushToServer(camera); } } } catch (Exception ex) { ex.PrintStack(); } } } } private void StartPushToServer(Device 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>(); var camera = repo.ReadOnlyTable().Include(o => o.Data).FirstOrDefault(o => o.Number == key); if (camera != null && camera.Data.FirstOrDefault(o => o.Key == "Push").Value == "是")// && camera.Publish && (!camera.NeedAuth || (camera.NeedAuth && camera.HasAuth))) { if (camera.Data.FirstOrDefault(o => o.Key == "NeedAuth").Value == "否" || camera.Data.FirstOrDefault(o => o.Key == "HasAuth").Value == "是") { this.Add(camera); } } } } public void Add(Device camera) { if (this._list.Any(o => o.Key == camera.Number)) { return; } var needAuth = camera.Data.FirstOrDefault(o => o.Key == "NeedAuth").Value == "是"; var hasAuth = camera.Data.FirstOrDefault(o => o.Key == "HasAuth").Value == "是"; var mainStreamUri = camera.Data.FirstOrDefault(o => o.Key == "MainStreamUri").Value; var mainRtspUrl = $"rtsp://{(needAuth ? $"{camera.UserName}:{camera.Password}@" : "")}{mainStreamUri.Substring(7)}"; var subStreamUri = camera.Data.FirstOrDefault(o => o.Key == "SubStreamUri").Value; var subRtspUrl = $"rtsp://{(needAuth ? $"{camera.UserName}:{camera.Password}@" : "")}{subStreamUri.Substring(7)}"; var fileName = $"ffmpeg-{Helper.Instance.GetRunTime()}{(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : "")}"; var file = camera.GetData("ffmpeg.file")?.Value; var mainRtmp = camera.Data.FirstOrDefault(o => o.Key == "mainrtmp").Value; var subRtmp = camera.Data.FirstOrDefault(o => o.Key == "subrtmp").Value; Console.WriteLine(file); var arguments = camera.Data.FirstOrDefault(o => o.Key == "ffmpeg.args").Value; Process main = null, sub = null; if (!string.IsNullOrEmpty(mainStreamUri)) { if (!string.IsNullOrEmpty(mainRtmp)) { main = this.SetProcess(file, string.Format(arguments, Environment.ProcessorCount, mainRtspUrl, mainRtmp)); } if (!string.IsNullOrEmpty(subRtmp)) { sub = this.SetProcess(file, string.Format(arguments, Environment.ProcessorCount, subRtspUrl, subRtmp)); } } if (this._list.TryAdd(camera.Number, (camera, main, sub))) { Console.WriteLine($"add {camera.Number} to list"); } else { if (main != null) { this.CloseProcess(main); this.CloseProcess(sub); } } } public void StopPushToServer(string key) { (Device camera, Process local, Process remote) item; if (this._list.TryRemove(key, out item)) { Console.WriteLine($"OnvifService>remove {key} from list"); this.CloseProcess(item.local); this.CloseProcess(item.remote); } } private Process SetProcess(string file, string arguments) { Console.WriteLine(file); Console.WriteLine(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; Console.WriteLine($"ffmpeg processes 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)) { if (this._cfg.GetValue("debug", false)) { Console.WriteLine(e.Data); } if (e.Data.IndexOf("forcing output") > -1) { process.Kill(); } } }; Console.WriteLine(arguments); try { process.Start(); process.BeginErrorReadLine(); } catch (Exception ex) { ex.PrintStack($"start ffmpeg error:{ex.Message}"); } return process; } private void CloseProcess(Process process) { try { if (process != null) { process.EnableRaisingEvents = false; process.Kill(); //process.WaitForExit(); process.Dispose(); } } catch (Exception ex) { ex.PrintStack($"error when close ffmpeg process {ex.Message}"); } } 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 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 camera = this.GetCamera(id); if (camera != null) { var ptzAddress = camera.GetDataValue("PtzAddress"); if (!string.IsNullOrEmpty(ptzAddress)) { var deviceUrl = camera.GetDataValue("DeviceUrl"); RequestXml(ptzAddress, MessageTemplate.StopAction, String.Format(MessageTemplate.StopMessage, camera.GetDataValue("MainToken"), true, true), camera.UserName, camera.Password, GetOnoce(deviceUrl)); } } } public void Move(string id, float zx, float px, float py) { var camera = this.GetCamera(id); if (camera != null) { var ptzAddress = camera.GetDataValue("PtzAddress"); if (!string.IsNullOrEmpty(ptzAddress)) { var deviceUrl = camera.GetDataValue("DeviceUrl"); RequestXml(ptzAddress, MessageTemplate.ContinuousMoveAction, String.Format(MessageTemplate.ContinuousMoveMessage, camera.GetDataValue("MainToken"), zx, px, py), camera.UserName, camera.Password, GetOnoce(deviceUrl)); } } } public byte[] MainScreenShot(string id) { var camera = this.GetCamera(id); var url = camera.GetDataValue("MainSubSnapshotUri"); if (string.IsNullOrEmpty(url)) { return new byte[] { }; } var hc = this._httpClientFactory.CreateClient(); if (camera.GetDataValue("NeedAuth") == "否") { return hc.GetByteArrayAsync(url).Result; } return hc.GetByteDigest(new Uri(url), camera.UserName, camera.Password); } public byte[] SubScreenShot(string id) { var camera = this.GetCamera(id); var url = camera.GetDataValue("SubSnapshotUri"); if (string.IsNullOrEmpty(url)) { return new byte[] { }; } var hc = this._httpClientFactory.CreateClient(); if (camera.GetDataValue("NeedAuth") == "否") { return hc.GetByteArrayAsync(url).Result; } return hc.GetByteDigest(new Uri(url), camera.UserName, camera.Password); } public void SetPush(string number, bool state) { using (var scope = _applicationServices.CreateScope()) { var repo = scope.ServiceProvider.GetService>(); var device = repo.Table().Include(o => o.Data).FirstOrDefault(o => o.Number == number); if (device != null) { var data = device.Data.FirstOrDefault(o => o.Key == "Push"); if (data != null) { data.Value = state ? "是" : "否"; this.UpdateData(repo, device, data); 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.Table().Include(o => o.Data).FirstOrDefault(o => o.Number == number); if (device != null) { var data = device.Data.FirstOrDefault(o => o.Key == "Record"); if (data != null) { data.Value = state ? "是" : "否"; //this.UpdateData(repo, device, data); this.SendToServer(Methods.UpdateDvr, data); } } } } private byte[] ScreenShot(Device camera) { var url = camera.GetDataValue("SubSnapshotUri"); if (string.IsNullOrEmpty(url)) { return new byte[] { }; } var hc = this._httpClientFactory.CreateClient(); if (camera.GetDataValue("NeedAuth") == "否") { return hc.GetByteArrayAsync(url).Result; } return hc.GetByteDigest(new Uri(url), camera.UserName, camera.Password); } private Device GetCamera(string number) { using (var scope = _applicationServices.CreateScope()) { var repo = scope.ServiceProvider.GetService>(); return repo.ReadOnlyTable().Include(o => o.Data).FirstOrDefault(o => o.Number == number); } } public void Dispose() { this.Stop(); } } }