新增 Mjpegplayer 用来播放 Web 流
This commit is contained in:
@@ -31,16 +31,14 @@ public static class Bootstrapper
|
||||
"--appid", "CameraApp_01",
|
||||
|
||||
// 视频流地址 (格式: IP,Port,Type,Desc)
|
||||
"--uris", "localhost,9001,video,调试PC;",
|
||||
"--uris", "localhost,9002,video,调试PC-2;",
|
||||
|
||||
// 指令通道
|
||||
"--uris", "localhost,9001,command,调试PC;",
|
||||
|
||||
"--uris", "localhost,9001,调试PC;",
|
||||
"--uris", "localhost,9002,调试PC;",
|
||||
|
||||
// 日志中心配置 (格式: IP,Port,Desc)
|
||||
"--sequris", "58.216.225.5,20026,日志处置中心;",
|
||||
"--seqkey", "Shine899195994250;",
|
||||
|
||||
"--seqkey", "Shine978697953780;",
|
||||
//"--seqkey", "Shine899195994250;",
|
||||
|
||||
// 端口策略
|
||||
"--mode", "1",
|
||||
"--ports", "5000,100"
|
||||
@@ -245,31 +243,71 @@ public static class Bootstrapper
|
||||
if (!config.CommandEndpoints.Any()) return;
|
||||
var gRpcLog = Log.ForContext("SourceContext", LogModules.gRpc);
|
||||
|
||||
try
|
||||
// // Optimized: 并发任务集合,实现多目标同时注册
|
||||
var registrationTasks = config.CommandEndpoints.Select(async endpoint =>
|
||||
{
|
||||
// 将 tcp:// 转换为 http:// 以适配 gRpc
|
||||
string targetUrl = config.CommandEndpoints.First().Uri.Replace("tcp://", "http://");
|
||||
string targetUrl = endpoint.Uri.Replace("tcp://", "http://");
|
||||
|
||||
using var channel = GrpcChannel.ForAddress(targetUrl);
|
||||
var client = new GatewayProvider.GatewayProviderClient(channel);
|
||||
|
||||
gRpcLog.Information($"[gRpc] 正在执行预注册: {targetUrl}");
|
||||
var resp = await client.RegisterInstanceAsync(new RegisterRequest
|
||||
// // Modified: 将 try-catch 移入内部,确保单个端点失败不影响其他端点
|
||||
try
|
||||
{
|
||||
InstanceId = config.AppId,
|
||||
Version = "2.0.0-grpc",
|
||||
ServerIp = "127.0.0.1",
|
||||
WebapiPort = config.BasePort, // 使用扫描后的新端口
|
||||
StartTimeTicks = DateTime.Now.Ticks,
|
||||
ProcessId = Environment.ProcessId,
|
||||
Description = ""
|
||||
});
|
||||
gRpcLog.Information($"[gRpc] 💡预注册成功: {resp.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
gRpcLog.Error($"[gRpc] ⚠️ 预注册尝试失败: {ex.Message}");
|
||||
}
|
||||
using var channel = GrpcChannel.ForAddress(targetUrl);
|
||||
var client = new GatewayProvider.GatewayProviderClient(channel);
|
||||
|
||||
gRpcLog.Information($"[gRpc] 正在执行预注册: {targetUrl}");
|
||||
|
||||
var resp = await client.RegisterInstanceAsync(new RegisterRequest
|
||||
{
|
||||
InstanceId = config.AppId,
|
||||
Version = "2.0.0-grpc",
|
||||
ServerIp = "127.0.0.1",
|
||||
WebapiPort = config.BasePort,
|
||||
StartTimeTicks = DateTime.Now.Ticks,
|
||||
ProcessId = Environment.ProcessId,
|
||||
Description = endpoint.Description // 携带备注信息
|
||||
});
|
||||
|
||||
gRpcLog.Information($"[gRpc] 💡预注册成功: {targetUrl} -> {resp.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// // Optimized: 记录具体哪个端点失败,但不阻断流程
|
||||
gRpcLog.Error($"[gRpc] ⚠️ 预注册尝试失败 ({targetUrl}): {ex.Message}");
|
||||
}
|
||||
});
|
||||
|
||||
// 等待所有注册任务完成
|
||||
await Task.WhenAll(registrationTasks);
|
||||
|
||||
//try
|
||||
//{
|
||||
// var cfgEndpoints = config.CommandEndpoints;
|
||||
// for(var i=0; i<cfgEndpoints.Count; i++)
|
||||
// {
|
||||
// // 将 tcp:// 转换为 http:// 以适配 gRpc
|
||||
// string targetUrl = cfgEndpoints[i].Uri.Replace("tcp://", "http://");
|
||||
|
||||
// using var channel = GrpcChannel.ForAddress(targetUrl);
|
||||
// var client = new GatewayProvider.GatewayProviderClient(channel);
|
||||
|
||||
// gRpcLog.Information($"[gRpc] 正在执行预注册: {targetUrl}");
|
||||
// var resp = await client.RegisterInstanceAsync(new RegisterRequest
|
||||
// {
|
||||
// InstanceId = config.AppId,
|
||||
// Version = "2.0.0-grpc",
|
||||
// ServerIp = "127.0.0.1",
|
||||
// WebapiPort = config.BasePort, // 使用扫描后的新端口
|
||||
// StartTimeTicks = DateTime.Now.Ticks,
|
||||
// ProcessId = Environment.ProcessId,
|
||||
// Description = ""
|
||||
// });
|
||||
// gRpcLog.Information($"[gRpc] 💡预注册成功: {resp.Message}");
|
||||
// }
|
||||
//}
|
||||
//catch (Exception ex)
|
||||
//{
|
||||
// gRpcLog.Error($"[gRpc] ⚠️ 预注册尝试失败: {ex.Message}");
|
||||
//}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -18,7 +18,7 @@ public class DeviceConfigHandler : ICommandHandler
|
||||
/// <summary>
|
||||
/// 命令名称
|
||||
/// </summary>
|
||||
public string ActionName => ProtocolHeaders.Sync_Camera;
|
||||
public string ActionName => ProtocolCodes.Sync_Camera;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
|
||||
@@ -24,6 +24,9 @@ public class DeviceStatusHandler : BackgroundService
|
||||
// 状态存储:CameraId -> 状态载荷
|
||||
private readonly ConcurrentDictionary<string, StatusEventPayload> _stateStore = new();
|
||||
|
||||
// 记录上一次成功发送的状态快照,用于增量日志对比
|
||||
private readonly Dictionary<string, bool> _lastPublishedStates = new();
|
||||
|
||||
private volatile bool _isDirty = false;
|
||||
private long _lastSendTick = 0;
|
||||
|
||||
@@ -40,7 +43,7 @@ public class DeviceStatusHandler : BackgroundService
|
||||
// 1. 初始化本地状态缓存
|
||||
foreach (var dev in _manager.GetAllDevices())
|
||||
{
|
||||
UpdateLocalState(dev.Id, false, "Service Init");
|
||||
UpdateLocalState(dev.Id, dev.Config.IpAddress, false, "Service Init");
|
||||
}
|
||||
|
||||
// 2. 订阅 SDK 状态变更事件
|
||||
@@ -71,17 +74,18 @@ public class DeviceStatusHandler : BackgroundService
|
||||
/// <summary>
|
||||
/// SDK 状态变更回调
|
||||
/// </summary>
|
||||
private void OnSdkStatusChanged(long deviceId, bool isOnline, string reason)
|
||||
private void OnSdkStatusChanged(long deviceId, string ipAddress, bool isOnline, string reason)
|
||||
{
|
||||
UpdateLocalState(deviceId, isOnline, reason);
|
||||
UpdateLocalState(deviceId, ipAddress, isOnline, reason);
|
||||
_isDirty = true;
|
||||
}
|
||||
|
||||
private void UpdateLocalState(long deviceId, bool isOnline, string reason)
|
||||
private void UpdateLocalState(long deviceId, string ipAddress, bool isOnline, string reason)
|
||||
{
|
||||
var evt = new StatusEventPayload
|
||||
{
|
||||
CameraId = deviceId.ToString(),
|
||||
IpAddress = ipAddress,
|
||||
IsOnline = isOnline,
|
||||
Reason = reason,
|
||||
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
|
||||
@@ -133,8 +137,7 @@ public class DeviceStatusHandler : BackgroundService
|
||||
// 这就是客户端尝试调用的真实路径:/包名.服务名/方法名
|
||||
var serviceName = client.GetType().DeclaringType?.Name ?? "Unknown";
|
||||
|
||||
_gRpcLog.Debug("[gRpc] 准备调用端点: {Url}", grpcUrl);
|
||||
_gRpcLog.Debug("[gRpc] 客户端契约服务名: {Service}", serviceName);
|
||||
_gRpcLog.Debug("[gRpc] 准备调用端点: {Url}, 客户端契约服务名: {Service}", grpcUrl, serviceName);
|
||||
|
||||
// 执行调用
|
||||
var response = await client.ReportStatusBatchAsync(request,
|
||||
@@ -142,8 +145,69 @@ public class DeviceStatusHandler : BackgroundService
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
_gRpcLog.Information("[gRpc] 设备状态上报成功, 共计: {Count} 个, Url: {Url}", request.Items.Count, grpcUrl);
|
||||
_gRpcLog.Debug("[gRpc] 设备状态上报成功: {Url} Items:{Items}", grpcUrl, request.Items);
|
||||
// 1. 处理变更日志 (Information)
|
||||
var diffList = new List<string>();
|
||||
foreach (var item in request.Items)
|
||||
{
|
||||
// 只有状态翻转时才记录变更
|
||||
if (!_lastPublishedStates.TryGetValue(item.CameraId, out bool lastStatus) || lastStatus != item.IsOnline)
|
||||
{
|
||||
// 从内存 Store 中抓取带有 IP 的原始对象
|
||||
_stateStore.TryGetValue(item.CameraId, out var payload);
|
||||
string ip = payload?.IpAddress ?? "Unknown IP";
|
||||
|
||||
string statusText = item.IsOnline ? "上线" : "离线";
|
||||
diffList.Add($"[{item.CameraId}({ip})] {statusText}");
|
||||
|
||||
// // Modified: 记录当前状态供下次对比
|
||||
_lastPublishedStates[item.CameraId] = item.IsOnline;
|
||||
}
|
||||
}
|
||||
|
||||
if (diffList.Any())
|
||||
{
|
||||
_gRpcLog.Information("[gRpc] 设备状态变更: {DiffDetails}, Url: {Url}",
|
||||
string.Join(", ", diffList), grpcUrl);
|
||||
}
|
||||
|
||||
// 2. 处理详细统计日志 (Debug)
|
||||
// Optimized: 通过映射获取 IP,不修改 StatusEventItem 契约
|
||||
var onlineDetails = request.Items
|
||||
.Where(x => x.IsOnline)
|
||||
.Select(x => {
|
||||
_stateStore.TryGetValue(x.CameraId, out var p);
|
||||
return $"{x.CameraId}({p?.IpAddress ?? "N/A"})";
|
||||
}).ToList();
|
||||
|
||||
var offlineDetails = request.Items
|
||||
.Where(x => !x.IsOnline)
|
||||
.Select(x => {
|
||||
_stateStore.TryGetValue(x.CameraId, out var p);
|
||||
return $"{x.CameraId}({p?.IpAddress ?? "N/A"})";
|
||||
}).ToList();
|
||||
|
||||
var detailParts = new List<string>();
|
||||
detailParts.Add($"其中在线 {onlineDetails.Count} 个");
|
||||
detailParts.Add($"离线 {offlineDetails.Count} 个");
|
||||
|
||||
if (offlineDetails.Any())
|
||||
{
|
||||
detailParts.Add($"离线设备【{string.Join(",", offlineDetails)}】");
|
||||
}
|
||||
|
||||
if (onlineDetails.Any())
|
||||
{
|
||||
detailParts.Add($"在线设备【{string.Join(",", onlineDetails)}】");
|
||||
}
|
||||
|
||||
string detailMsg = string.Join(",", detailParts);
|
||||
|
||||
// // Optimized: 最终输出格式化的详细日志
|
||||
_gRpcLog.Debug("[gRpc] 设备状态上报详细: {Url} 总数:{Count} {Detail}",
|
||||
grpcUrl,
|
||||
request.Items.Count,
|
||||
detailMsg);
|
||||
|
||||
_isDirty = false;
|
||||
_lastSendTick = Environment.TickCount64;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace SHH.CameraService
|
||||
/// <summary>
|
||||
/// 指令名称
|
||||
/// </summary>
|
||||
public string ActionName => ProtocolHeaders.Remove_Camera;
|
||||
public string ActionName => ProtocolCodes.Remove_Camera;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
|
||||
@@ -107,6 +107,19 @@ public class Program
|
||||
|
||||
await manager.StartAsync();
|
||||
|
||||
// 2. Optimized: 主动拉起所有已加载设备的物理连接
|
||||
// 理由:当本地配置了 video 推送目标时,不再等待远端 command 下发启动指令
|
||||
var allDevices = manager.GetAllDevices();
|
||||
foreach (var device in allDevices)
|
||||
{
|
||||
if (device.IsRunning && !device.IsActived)
|
||||
{
|
||||
logger.Information($"[Core] 🚀 自动激活设备流: ID:{device.Id} IP:{device.Config.IpAddress}");
|
||||
// 使用 Fire-and-forget 启动,避免阻塞主线程
|
||||
_ = device.StartAsync();
|
||||
}
|
||||
}
|
||||
|
||||
var sysLog = Log.ForContext("SourceContext", LogModules.Core);
|
||||
sysLog.Information($"[Core] 🚀 核心业务逻辑已激活, 设备管理器已就绪.");
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<ApplicationIcon>notifyIcon.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -14,6 +15,10 @@
|
||||
<None Remove="Dels\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="notifyIcon.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Grpc.Net.Client" Version="2.76.0" />
|
||||
<PackageReference Include="Grpc.Tools" Version="2.76.0">
|
||||
@@ -26,7 +31,6 @@
|
||||
<ProjectReference Include="..\Ayay.SerilogLogs\Ayay.SerilogLogs.csproj" />
|
||||
<ProjectReference Include="..\SHH.CameraSdk\SHH.CameraSdk.csproj" />
|
||||
<ProjectReference Include="..\SHH.Contracts.Grpc\SHH.Contracts.Grpc.csproj" />
|
||||
<ProjectReference Include="..\SHH.Contracts\SHH.Contracts.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using SHH.CameraSdk;
|
||||
|
||||
@@ -93,12 +94,24 @@ public static class ServiceCollectionExtensions
|
||||
services.AddSingleton<IEnumerable<StreamTarget>>(netTargets);
|
||||
services.AddHostedService<ImageMonitorController>();
|
||||
|
||||
// 动态注册 Sender Worker
|
||||
//// 动态注册 Sender Worker
|
||||
//foreach (var target in netTargets)
|
||||
//{
|
||||
// // 注意:这里需要使用 Microsoft.Extensions.Logging.ILogger 来适配构造函数
|
||||
// services.AddHostedService(sp =>
|
||||
// new GrpcSenderWorker(target));
|
||||
//}
|
||||
|
||||
foreach (var target in netTargets)
|
||||
{
|
||||
// 注意:这里需要使用 Microsoft.Extensions.Logging.ILogger 来适配构造函数
|
||||
services.AddHostedService(sp =>
|
||||
new GrpcSenderWorker(target));
|
||||
// Modified: 显式声明局部变量,防止 Lambda 捕获循环变量导致的引用重复
|
||||
var currentTarget = target;
|
||||
|
||||
logger.Information("[DI] 准备启动 Worker 实例: {Name} -> {Url}",
|
||||
currentTarget.Config.Name, currentTarget.Config.Endpoint);
|
||||
|
||||
// 使用工厂模式注册,确保传入的是当前的 currentTarget
|
||||
services.AddSingleton<IHostedService>(sp => new GrpcSenderWorker(currentTarget));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BIN
SHH.CameraService/notifyIcon.ico
Normal file
BIN
SHH.CameraService/notifyIcon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 133 KiB |
Reference in New Issue
Block a user