using Application.Domain.Entities; using Infrastructure.Data; using Infrastructure.Extensions; using IoT.Shared.Infrastructure; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Concurrent; using System.Diagnostics; using System.Drawing; 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; namespace IoT.Shared.DeviceServices.Onvif { public class OnvifService : BaseDeviceService { private readonly ConcurrentDictionary _list = new ConcurrentDictionary(); private readonly IHostingEnvironment _env; private readonly IHttpClientFactory _httpClientFactory; private readonly IOnvifDeviceManagement _onvifDeviceManagement; public OnvifService(IServiceProvider applicationServices, IConfiguration configuration, IHostingEnvironment env, IHttpClientFactory httpClientFactory) : base(applicationServices, configuration) { this._env = env; this._httpClientFactory = httpClientFactory; this._onvifDeviceManagement = new OnvifDeviceManagement(httpClientFactory); } #region impl interface public override void Execute() { Refresh(); } public override void Stop() { try { this._tokenSource.Cancel(); foreach (var item in this._list.Keys.ToList()) { this.Remove(item); } } catch (Exception ex) { ex.PrintStack(); } } #endregion impl interface private void Refresh() { try { Search(); Notify(); } catch (Exception ex) { ex.PrintStack(); } } 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 deviceInfoNumber = "onvifcamera"; var deviceInfoRepo = scope.ServiceProvider.GetService>(); var deviceInfo = deviceInfoRepo.Table().FirstOrDefault(o => o.Number == deviceInfoNumber); if (deviceInfo == null) { deviceInfo = new DeviceInfo { Number = deviceInfoNumber, Name = "ONVIF摄像头", DeviceType = DeviceType.Device, ApiJson = this.GetApiJson("/Camera/") }; deviceInfoRepo.Add(deviceInfo); deviceInfoRepo.SaveChanges(); this.SendDeviceInfo(deviceInfo); } 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 = "摄像头", Number = ipCamera.Id, Icon = "camera", CategoryNumber = "10", InfoNumber = deviceInfoNumber, InfoId = deviceInfo.Id, NodeId = node.Id }; var fileName = $"ffmpeg-{Helper.Instance.GetRunTime()}{(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : "")}"; var file = Path.Combine(this._env.WebRootPath, fileName); device.AddorUpdateData("Push", "否", DeviceDataType.String, "推流"); device.AddorUpdateData("Record", "否", DeviceDataType.String, "录像"); device.AddorUpdateData("ffmpeg.file", file, DeviceDataType.String, "ffmpeg路径", hidden: true); device.AddorUpdateData("ffmpeg.args", this._configuration["ffmpeg.args"], DeviceDataType.String, "ffmpeg.args", hidden: true); device.AddorUpdateData("mainrtmp", $"rtmp://{this._configuration["stream.rtmp"]}/live/main{ipCamera.Id}", DeviceDataType.String, "主码流rtmp"); device.AddorUpdateData("mainflv", $"http://{this._configuration["stream.flv"]}/live/main{ipCamera.Id}.flv", DeviceDataType.String, "主码流flv"); device.AddorUpdateData("mainhls", $"http://{this._configuration["stream.hls"]}/live/main{ipCamera.Id}.m3u8", DeviceDataType.String, "主码流hls"); device.AddorUpdateData("subrtmp", $"rtmp://{this._configuration["stream.rtmp"]}/live/sub{ipCamera.Id}", DeviceDataType.String, "子码流rtmp"); device.AddorUpdateData("subflv", $"http://{this._configuration["stream.flv"]}/live/sub{ipCamera.Id}.flv", DeviceDataType.String, "子码流flv"); device.AddorUpdateData("subhls", $"http://{this._configuration["stream.hls"]}/live/sub{ipCamera.Id}.m3u8", DeviceDataType.String, "子码流hls"); device.ConnectId = this._configuration["connectId"]; device.NodeNumber = this._configuration["node.number"]; deviceRepo.Add(device); } var profiles = this._onvifDeviceManagement.GetProfiles(ipCamera.DeviceUrl, ipCamera.MediaUrl); var needAuth = false; var hasAuth = false; if (profiles == "") { needAuth = true; device.UserName = device.UserName ?? this._configuration["camera.usr"]; device.Password = device.Password ?? this._configuration["camera.pwd"]; 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(); 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(); device.AddorUpdateData("NeedAuth", needAuth ? "是" : "否", DeviceDataType.String, "需认证"); device.AddorUpdateData("HasAuth", hasAuth ? "是" : "否", DeviceDataType.String, "已认证"); device.AddorUpdateData("DeviceUrl", ipCamera.DeviceUrl, DeviceDataType.String, "设备地址"); device.AddorUpdateData("PtzAddress", ipCamera.PTZAddress, DeviceDataType.String, "云台地址"); device.AddorUpdateData("Ptz3DZoomSupport", ipCamera.Ptz3DZoomSupport ? "是" : "否", DeviceDataType.String, "缩放支持"); device.AddorUpdateData("MainToken", ipCamera.Profiles.First().Token, DeviceDataType.String, "主码流Token"); device.AddorUpdateData("SubToken", ipCamera.Profiles.Last().Token, DeviceDataType.String, "子码流Token"); device.AddorUpdateData("MainStreamUri", ipCamera.MainStreamUri, DeviceDataType.String, "主码流地址"); device.AddorUpdateData("MainSnapshotUri", ipCamera.MainSnapshotUri, DeviceDataType.String, "主码流截图地址"); device.AddorUpdateData("SubStreamUri", ipCamera.SubStreamUri, DeviceDataType.String, "子码流地址"); device.AddorUpdateData("SubSnapshotUri", ipCamera.SubSnapshotUri, DeviceDataType.String, "子码流截图地址"); var jpgBytes = ScreenShot(device); using (var stream = new MemoryStream(jpgBytes)) { using (var bigImage = Image.FromStream(stream)) { var width = 640; var height = 480; using (var bitmap = new Bitmap(width, height)) { using (Graphics g = Graphics.FromImage((Image)bitmap)) { g.DrawImage(bigImage, 0, 0, width, height); device.AddorUpdateData("Snapshot", Convert.ToBase64String(bitmap.ToJpeg(80)), DeviceDataType.String, "截图"); } } } } deviceRepo.SaveChanges(); this.SendDevice(device); } } } 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.Remove(key); } } foreach (var camera in cameras) { try { if (camera.Data.FirstOrDefault(o => o.Key == "Push").Value == "是") { if (camera.Data.FirstOrDefault(o => o.Key == "NeedAuth").Value == "否" || camera.Data.FirstOrDefault(o => o.Key == "HasAuth").Value == "是") { this.Publish(camera); } } } catch (Exception ex) { ex.PrintStack(); } } } } private void Publish(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 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(); } } } private string GetOnoce(string deviceUrl) { var message = @" "; 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; } 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, Template.StopAction, String.Format(Template.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, Template.ContinuousMoveAction, String.Format(Template.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(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(url, camera.UserName, camera.Password); } public void StartPush(string number) { using (var scope = _applicationServices.CreateScope()) { var repo = scope.ServiceProvider.GetService>(); var device = repo.Table().Include(o => o.Data).FirstOrDefault(o => o.Number == number); var data = device.Data.FirstOrDefault(o => o.Key == "Push"); data.Value = "是"; } } public void StopPush(string number) { using (var scope = _applicationServices.CreateScope()) { var repo = scope.ServiceProvider.GetService>(); var device = repo.Table().Include(o => o.Data).FirstOrDefault(o => o.Number == number); var data = device.Data.FirstOrDefault(o => o.Key == "Push"); data.Value = "否"; } } 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(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); } } } }