ShengShengBuXi/ShengShengBuXi/Hubs/AudioHub.cs
2025-03-29 10:16:44 +08:00

1783 lines
72 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using System.Text.Json;
using Newtonsoft.Json;
using ShengShengBuXi.Models;
using ShengShengBuXi.Services;
using System.Xml.Linq;
using static System.Net.Mime.MediaTypeNames;
namespace ShengShengBuXi.Hubs
{
/// <summary>
/// 音频处理Hub处理WebSocket连接和音频数据传输
/// </summary>
public class AudioHub : Hub, IDisposable
{
/// <summary>
///
/// </summary>
private readonly IAudioProcessingService _audioProcessingService;
private readonly IConfigurationService _configurationService;
private readonly ISpeechToTextService _speechToTextService;
private static readonly ConcurrentDictionary<string, ClientInfo> _clients = new ConcurrentDictionary<string, ClientInfo>();
private readonly ILogger<AudioHub> _logger;
private readonly IHubContext<AudioHub> _hubContext;
private bool _disposed = false;
private readonly HashSet<string> _processedRecordingPaths = new HashSet<string>();
/// <summary>
/// 使用静态集合进行全局去重,防止服务重启或热重载时丢失去重状态
/// </summary>
private static readonly ConcurrentDictionary<string, DateTime> _globalProcessedPaths = new ConcurrentDictionary<string, DateTime>();
/// <summary>
/// 用于清理过期记录的计时器
/// </summary>
private static Timer _cleanupTimer;
/// <summary>
/// 显示文本队列
/// </summary>
private static ConcurrentDictionary<Guid, DisplayText> _displayTextQueue { get; set; } = new ConcurrentDictionary<Guid, DisplayText>();
/// <summary>
/// 控制端拦截的文本队列
/// </summary>
private static ConcurrentDictionary<Guid, DisplayText> _monitorTextQueue { get; set; } = new ConcurrentDictionary<Guid, DisplayText>();
/// <summary>
/// ai美化文本队列
/// </summary>
private static ConcurrentDictionary<Guid, DisplayText> _aiTextQueue { get; set; } = new ConcurrentDictionary<Guid, DisplayText>();
/// <summary>
/// 显示文本定时器
/// </summary>
private static Timer _displayTextTimer { get; set; }
/// <summary>
/// 上次有真实用户说话的时间
/// </summary>
private static DateTime _lastRealUserSpeakTime = DateTime.MinValue;
/// <summary>
/// 是否已初始化显示文本定时器
/// </summary>
private static bool _isDisplayTimerInitialized = false;
/// <summary>
/// 显示文本定时器锁
/// </summary>
private static readonly object _displayTimerLock = new object();
/// <summary>
/// 监控文本队列的持久化文件路径
/// </summary>
private static readonly string _monitorTextQueueFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config/monitor_text_queue.json");
/// <summary>
/// 预设句子文件路径
/// </summary>
private static readonly string _sentencesFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config/sentences.txt");
/// <summary>
/// 真实用户显示记录的持久化文件路径
/// </summary>
private static readonly string _realUserDisplayLogsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config/real_user_displays.log");
/// <summary>
/// 预设句子列表
/// </summary>
private static List<string> _presetSentences = new List<string>();
// 配置信息
private static string _displayConfig = "{}";
private static object _displayConfigLock = new object();
// 控屏开关设置
private static bool _manualScreenControlEnabled = false;
private static readonly string _screenControlSettingPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config/screen_control.json");
// 管理员配置保存路径
private static readonly string ConfigDirectory = Path.Combine(Directory.GetCurrentDirectory(), "config");
private static readonly string DisplayConfigPath = Path.Combine(ConfigDirectory, "display.json");
private static DateTime _lastRealTimeResultSentTime = DateTime.MinValue;
private static int _realTimeResultCounter = 0;
private static readonly TimeSpan _realTimeResultThrottleWindow = TimeSpan.FromSeconds(0.3);
private static readonly int _maxRealTimeResultsPerSecond = 1;
// 增加对最终结果的限流变量
private static DateTime _lastFinalResultSentTime = DateTime.MinValue;
private static int _finalResultCounter = 0;
private static TimeSpan _finalResultThrottleWindow = TimeSpan.FromSeconds(0.3);
private static readonly int _maxFinalResultsPerSecond = 1;
// 用于初始化配置
static AudioHub()
{
// 确保配置目录存在
if (!Directory.Exists(ConfigDirectory))
{
Directory.CreateDirectory(ConfigDirectory);
}
// 加载显示配置
LoadDisplayConfig();
// 加载控屏设置
LoadScreenControlSetting();
}
// 加载显示配置
private static void LoadDisplayConfig()
{
try
{
if (File.Exists(DisplayConfigPath))
{
string configJson = File.ReadAllText(DisplayConfigPath);
lock (_displayConfigLock)
{
_displayConfig = configJson;
}
Console.WriteLine("加载显示配置成功");
}
else
{
// 创建默认配置
string defaultConfig = @"{
""leftContainer"": {
""turnPageHeight"": 0.8,
""fontSize"": ""16px"",
""typewriterSpeed"": 50
},
""rightContainer"": {
""fontSize"": ""24px"",
""fontWeight"": ""normal"",
""fontStyle"": ""normal"",
""typewriterSpeed"": 50
},
""waterEffect"": {
""enabled"": true,
""minInterval"": 800,
""maxInterval"": 2000,
""simultaneousDrops"": 3,
""fadeOutSpeed"": 0.1,
""centerBias"": 0.5,
""largeDrop"": {
""probability"": 0.2,
""size"": 9
}
}
}";
lock (_displayConfigLock)
{
_displayConfig = defaultConfig;
}
// 保存默认配置
File.WriteAllText(DisplayConfigPath, defaultConfig);
Console.WriteLine("创建默认显示配置成功");
}
}
catch (Exception ex)
{
Console.WriteLine($"加载显示配置出错: {ex.Message}");
}
}
// 加载控屏设置
private static void LoadScreenControlSetting()
{
try
{
if (File.Exists(_screenControlSettingPath))
{
string json = File.ReadAllText(_screenControlSettingPath);
var setting = JsonConvert.DeserializeObject<dynamic>(json);
_manualScreenControlEnabled = setting?.IsManual ?? false;
Console.WriteLine($"加载控屏设置成功,当前模式: {(_manualScreenControlEnabled ? "" : "")}");
}
else
{
// 默认设置为自动
_manualScreenControlEnabled = false;
// 保存默认设置
SaveScreenControlSetting();
Console.WriteLine("创建默认控屏设置成功");
}
}
catch (Exception ex)
{
Console.WriteLine($"加载控屏设置出错: {ex.Message}");
_manualScreenControlEnabled = false;
}
}
// 保存控屏设置
private static bool SaveScreenControlSetting()
{
try
{
string json = JsonConvert.SerializeObject(new { IsManual = _manualScreenControlEnabled });
File.WriteAllText(_screenControlSettingPath, json);
return true;
}
catch (Exception ex)
{
Console.WriteLine($"保存控屏设置出错: {ex.Message}");
return false;
}
}
/// <summary>
/// 初始化音频Hub
/// </summary>
/// <param name="audioProcessingService">音频处理服务</param>
/// <param name="configurationService">配置服务</param>
/// <param name="speechToTextService">语音识别服务</param>
/// <param name="logger">日志记录器</param>
/// <param name="hubContext">Hub上下文</param>
public AudioHub(
IAudioProcessingService audioProcessingService,
IConfigurationService configurationService,
ISpeechToTextService speechToTextService,
ILogger<AudioHub> logger,
IHubContext<AudioHub> hubContext)
{
_audioProcessingService = audioProcessingService ?? throw new ArgumentNullException(nameof(audioProcessingService));
_configurationService = configurationService ?? throw new ArgumentNullException(nameof(configurationService));
_speechToTextService = speechToTextService ?? throw new ArgumentNullException(nameof(speechToTextService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_hubContext = hubContext ?? throw new ArgumentNullException(nameof(hubContext));
// 订阅语音识别结果事件
_speechToTextService.ResultReceived += OnSpeechToTextResultReceived;
_audioProcessingService.SpeechToTextResultReceived += OnSpeechToTextResultReceived;
_audioProcessingService.AudioSavedToFile += OnAudioSavedToFile;
_configurationService.ConfigurationChanged += OnConfigurationChanged;
// 初始化清理计时器(如果尚未初始化)
if (_cleanupTimer == null)
{
_cleanupTimer = new Timer(CleanupOldProcessedPaths, null, TimeSpan.FromMinutes(30), TimeSpan.FromMinutes(30));
}
// 从文件加载预设句子
LoadPresetSentencesFromFile();
// 从文件加载监控文本队列
LoadMonitorTextQueueFromFile();
// 初始化显示文本定时器
InitializeDisplayTextTimer();
// 注册应用程序域卸载事件,以便在应用关闭时保存数据
AppDomain.CurrentDomain.ProcessExit += (sender, e) => SaveMonitorTextQueueToFile();
AppDomain.CurrentDomain.DomainUnload += (sender, e) => SaveMonitorTextQueueToFile();
}
/// <summary>
/// 清理过期的处理记录
/// </summary>
private static void CleanupOldProcessedPaths(object state)
{
try
{
// 删除超过24小时的记录
var expireTime = DateTime.Now.AddHours(-24);
var keysToRemove = _globalProcessedPaths.Where(kvp => kvp.Value < expireTime)
.Select(kvp => kvp.Key)
.ToList();
foreach (var key in keysToRemove)
{
_globalProcessedPaths.TryRemove(key, out _);
}
}
catch
{
// 忽略清理过程中的错误
}
}
/// <summary>
/// 客户端连接时触发
/// </summary>
/// <returns>连接处理任务</returns>
public override async Task OnConnectedAsync()
{
var httpContext = Context.GetHttpContext();
var clientIp = httpContext?.Connection?.RemoteIpAddress?.ToString() ?? "Unknown";
var userAgent = httpContext?.Request.Headers["User-Agent"].ToString() ?? "Unknown";
_logger.LogInformation($"客户端连接: {Context.ConnectionId}, IP: {clientIp}");
// 等待客户端发送身份信息
await base.OnConnectedAsync();
}
/// <summary>
/// 客户端断开连接时触发
/// </summary>
/// <param name="exception">异常信息</param>
/// <returns>断开连接处理任务</returns>
public override async Task OnDisconnectedAsync(Exception exception)
{
if (_clients.TryRemove(Context.ConnectionId, out var clientInfo))
{
_logger.LogInformation($"客户端断开连接: {Context.ConnectionId}, 类型: {clientInfo.ClientType}, 名称: {clientInfo.Name}");
// 如果是控制器客户端,结束当前音频流
if (clientInfo.ClientType == ClientType.Controller)
{
await _audioProcessingService.EndAudioStreamAsync(Context.ConnectionId);
await _speechToTextService.EndSessionAsync(Context.ConnectionId);
}
// 通知所有管理端客户端断开连接
await Clients.Group("webadmin").SendAsync("ClientDisconnected", clientInfo);
}
await base.OnDisconnectedAsync(exception);
}
/// <summary>
/// 注册客户端
/// </summary>
/// <param name="clientType">客户端类型</param>
/// <param name="name">客户端名称</param>
/// <returns>注册处理任务</returns>
public async Task RegisterClient(ClientType clientType, string name)
{
var httpContext = Context.GetHttpContext();
var clientIp = httpContext?.Connection?.RemoteIpAddress?.ToString() ?? "Unknown";
var userAgent = httpContext?.Request.Headers["User-Agent"].ToString() ?? "Unknown";
var clientInfo = new ClientInfo
{
ClientId = Context.ConnectionId,
ClientType = clientType,
Name = name,
IpAddress = clientIp,
UserAgent = userAgent
};
if (_clients.TryAdd(Context.ConnectionId, clientInfo))
{
_logger.LogInformation($"客户端注册成功: {Context.ConnectionId}, 类型: {clientType}, 名称: {name}");
// 根据客户端类型加入不同的组
switch (clientType)
{
case ClientType.Controller:
await Groups.AddToGroupAsync(Context.ConnectionId, "controllers");
break;
case ClientType.WebAdmin:
await Groups.AddToGroupAsync(Context.ConnectionId, "webadmin");
// 发送当前配置给管理端
await Clients.Caller.SendAsync("ReceiveConfiguration", _configurationService.CurrentConfig);
// 发送客户端列表给管理端
await Clients.Caller.SendAsync("ClientList", _clients.Values);
break;
case ClientType.Monitor:
await Groups.AddToGroupAsync(Context.ConnectionId, "monitor");
break;
case ClientType.Display:
await Groups.AddToGroupAsync(Context.ConnectionId, "displays");
break;
}
// 通知所有管理端有新客户端连接
await Clients.Group("webadmin").SendAsync("NewClientConnected", clientInfo);
// 给客户端发送确认消息
await Clients.Caller.SendAsync("RegistrationConfirmed", clientInfo.ClientId);
}
else
{
_logger.LogWarning($"客户端注册失败: {Context.ConnectionId}");
await Clients.Caller.SendAsync("RegistrationFailed", "注册失败连接ID已存在");
}
}
/// <summary>
/// 处理开始音频流请求
/// </summary>
/// <param name="sampleRate">采样率</param>
/// <param name="channels">声道数</param>
/// <returns>处理任务</returns>
public async Task StartAudioStream(int sampleRate, int channels)
{
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
{
_logger.LogWarning($"未注册的客户端尝试开始音频流: {Context.ConnectionId}");
await Clients.Caller.SendAsync("Error", "请先注册客户端");
return;
}
if (clientInfo.ClientType != ClientType.Controller)
{
_logger.LogWarning($"非控制器客户端尝试开始音频流: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
await Clients.Caller.SendAsync("Error", "只有控制器客户端可以发送音频流");
return;
}
_logger.LogInformation($"开始音频流: {Context.ConnectionId}, 采样率: {sampleRate}, 声道: {channels}");
// 开始音频处理
await _audioProcessingService.StartAudioStreamAsync(Context.ConnectionId, sampleRate, channels);
// 开始语音识别会话
await _speechToTextService.StartSessionAsync(Context.ConnectionId, sampleRate);
// 更新客户端状态
clientInfo.IsAudioStreaming = true;
_clients[Context.ConnectionId] = clientInfo;
// 通知所有管理端开始接收音频流
await Clients.Group("webadmin").SendAsync("AudioStreamStarted", Context.ConnectionId, sampleRate, channels);
// 通知所有监听中的显示端有通话开始
await Clients.Group("monitor").SendAsync("CallStateChanged", true);
}
/// <summary>
/// 处理结束音频流请求
/// </summary>
/// <returns>处理任务</returns>
public async Task EndAudioStream()
{
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
{
_logger.LogWarning($"未注册的客户端尝试结束音频流: {Context.ConnectionId}");
return;
}
_logger.LogInformation($"结束音频流: {Context.ConnectionId}");
// 结束音频处理
await _audioProcessingService.EndAudioStreamAsync(Context.ConnectionId);
// 结束语音识别会话
await _speechToTextService.EndSessionAsync(Context.ConnectionId);
var _speechsession = _speechToTextService.GetSessionStatus(Context.ConnectionId);
if (_speechsession.HasSavedText && !_speechsession.IsSentToDisplay)
{
AddRecognizedTextToDisplay(_speechsession.FinalText, true, Context.ConnectionId, _speechsession);
_speechsession.IsSentToDisplay = true;
_logger.LogInformation($"保存文件流: {JsonConvert.SerializeObject(_speechsession)}");
// 将结果发送给所有监听中的客户端
await _hubContext.Clients.Groups(new[] { "webadmin", "monitor" })
.SendAsync("ReceiveSpeechToEndTextResult", _speechsession.FinalText);
}
// 更新客户端状态
clientInfo.IsAudioStreaming = false;
_clients[Context.ConnectionId] = clientInfo;
//
// 通知所有管理端结束接收音频流
await Clients.Group("webadmin").SendAsync("AudioStreamEnded", Context.ConnectionId);
// 通知所有监听中的显示端通话结束
await Clients.Group("monitor").SendAsync("CallStateChanged", false);
}
/// <summary>
/// 处理接收到的音频数据
/// </summary>
/// <param name="audioData">音频数据</param>
/// <returns>处理任务</returns>
public async Task ReceiveAudioData(byte[] audioData)
{
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
{
_logger.LogWarning($"未注册的客户端尝试发送音频数据: {Context.ConnectionId}");
var httpContext = Context.GetHttpContext();
var clientIp = httpContext?.Connection?.RemoteIpAddress?.ToString() ?? "Unknown";
var userAgent = httpContext?.Request.Headers["User-Agent"].ToString() ?? "Unknown";
clientInfo = new ClientInfo
{
ClientId = Context.ConnectionId,
ClientType = ClientType.Controller,
Name = "自动注册客户端",
IpAddress = clientIp,
UserAgent = userAgent
};
if (_clients.TryAdd(Context.ConnectionId, clientInfo))
{
}
//return;
}
if (clientInfo.ClientType != ClientType.Controller)
{
_logger.LogWarning($"非控制器客户端尝试发送音频数据: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
return;
}
// 处理音频数据
var config = _configurationService.CurrentConfig.Recording;
await _audioProcessingService.ProcessAudioDataAsync(audioData, config.SampleRate, config.Channels, Context.ConnectionId);
// 处理语音识别
if (_configurationService.CurrentConfig.Network.EnableSpeechToText)
{
await _speechToTextService.ProcessAudioAsync(audioData, Context.ConnectionId);
}
// 转发音频数据到管理端
if (_configurationService.CurrentConfig.Network.EnableAudioStreaming)
{
// await Clients.Group("webadmin").SendAsync("ReceiveAudioData", Context.ConnectionId, audioData);
// 转发音频到正在监听的显示端
var monitoringClients = _clients.Values
.Where(c => (c.ClientType == ClientType.Monitor || c.ClientType == ClientType.WebAdmin))
.Select(c => c.ClientId)
.ToList();
if (monitoringClients.Any())
{
try
{
byte[] dataToSend;
if (_configurationService.CurrentConfig.Network.EnableAudioNoiseReduction)
{
dataToSend = _audioProcessingService.ApplyNoiseReduction(audioData, config.SampleRate, config.Channels);
_logger.LogDebug($"转发音频数据到{monitoringClients.Count}个监听客户端,数据长度: {audioData.Length},降噪后长度:{dataToSend.Length}");
}
else
{
dataToSend = audioData;
_logger.LogDebug($"转发音频数据到{monitoringClients.Count}个监听客户端,数据长度: {audioData.Length}");
}
// 始终使用二进制格式发送数据,避免字符串转换
await Clients.Clients(monitoringClients).SendAsync("ReceiveAudioData", dataToSend);
}
catch (Exception ex)
{
_logger.LogError($"转发音频数据到监听端失败: {ex.Message}");
}
}
}
}
/// <summary>
/// 处理更新配置请求
/// </summary>
/// <param name="configJson">配置JSON字符串</param>
/// <returns>处理任务</returns>
public async Task UpdateConfiguration(string configJson)
{
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
{
_logger.LogWarning($"未注册的客户端尝试更新配置: {Context.ConnectionId}");
await Clients.Caller.SendAsync("Error", "请先注册客户端");
return;
}
if (clientInfo.ClientType != ClientType.WebAdmin)
{
_logger.LogWarning($"非管理端客户端尝试更新配置: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
await Clients.Caller.SendAsync("Error", "只有管理端客户端可以更新配置");
return;
}
_logger.LogInformation($"更新配置: {Context.ConnectionId}");
// 更新配置
var success = _configurationService.UpdateConfigurationFromJson(configJson);
if (success)
{
// 配置更新成功事件会广播新配置
await Clients.Caller.SendAsync("ConfigurationUpdated", true, "配置更新成功");
}
else
{
await Clients.Caller.SendAsync("ConfigurationUpdated", false, "配置更新失败");
}
}
/// <summary>
/// 处理获取录音列表请求
/// </summary>
/// <param name="count">获取数量</param>
/// <returns>处理任务</returns>
public async Task GetRecentRecordings(int count = 100)
{
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
{
_logger.LogWarning($"未注册的客户端尝试获取录音列表: {Context.ConnectionId}");
await Clients.Caller.SendAsync("Error", "请先注册客户端");
return;
}
if (clientInfo.ClientType != ClientType.WebAdmin && clientInfo.ClientType != ClientType.Monitor)
{
_logger.LogWarning($"非管理端或显示端客户端尝试获取录音列表: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
await Clients.Caller.SendAsync("Error", "只有管理端或显示端客户端可以获取录音列表");
return;
}
_logger.LogInformation($"获取录音列表: {Context.ConnectionId}, 数量: {count}");
// 获取录音列表
var recordings = _audioProcessingService.GetRecentRecordings(count);
// 发送录音列表
await Clients.Caller.SendAsync("RecentRecordings", recordings);
}
/// <summary>
/// 获取最新配置
/// </summary>
/// <returns>配置JSON字符串</returns>
public async Task<string> GetLatestConfig()
{
try
{
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
{
_logger.LogWarning($"未注册的客户端尝试获取配置: {Context.ConnectionId}");
return string.Empty;
}
_logger.LogInformation($"客户端请求获取最新配置: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
// 读取config.json文件
string configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.json");
if (!File.Exists(configPath))
{
_logger.LogWarning($"配置文件不存在: {configPath}");
return JsonConvert.SerializeObject(_configurationService.CurrentConfig);
}
string configJson = await File.ReadAllTextAsync(configPath);
_logger.LogInformation($"已读取配置文件: {configPath}");
return configJson;
}
catch (Exception ex)
{
_logger.LogError($"获取配置时出错: {ex.Message}");
// 出错时返回当前内存中的配置
return JsonConvert.SerializeObject(_configurationService.CurrentConfig);
}
}
/// <summary>
/// 获取监控文本列表
/// </summary>
/// <returns>监控文本列表</returns>
public async Task<List<DisplayText>> GetMonitorTextList()
{
var result = _monitorTextQueue.Select(kvp => kvp.Value).OrderBy(it => it.Timestamp).ToList();
// 转换文件路径为URL路径
foreach (var item in result)
{
ConvertFilePathsToUrls(item);
}
return result;
}
/// <summary>
/// 获取显示文本列表
/// </summary>
/// <returns>显示文本列表</returns>
public async Task<List<DisplayText>> GetDisplayList()
{
var result = _displayTextQueue.Select(kvp => kvp.Value).Where(it => it.IsRealUser).OrderBy(it => it.Timestamp).ToList();
// 转换文件路径为URL路径
foreach (var item in result)
{
ConvertFilePathsToUrls(item);
}
return result;
}
/// <summary>
/// 将文件路径转换为URL路径
/// </summary>
/// <param name="displayText">要转换的显示文本对象</param>
private void ConvertFilePathsToUrls(DisplayText displayText)
{
// 转换录音文件路径
if (!string.IsNullOrEmpty(displayText.RecordingPath))
{
// 从完整路径中提取文件名
string fileName = Path.GetFileName(displayText.RecordingPath);
// 从路径中识别/recordings/目录
if (displayText.RecordingPath.Contains("recordings"))
{
int recordingsIndex = displayText.RecordingPath.IndexOf("recordings", StringComparison.OrdinalIgnoreCase);
string relativePath = displayText.RecordingPath.Substring(recordingsIndex);
displayText.RecordingPath = $"/{relativePath.Replace('\\', '/')}";
}
else
{
// 如果路径不包含recordings目录则直接使用文件名
displayText.RecordingPath = $"/recordings/{fileName}";
}
}
// 转换文本文件路径
if (!string.IsNullOrEmpty(displayText.TextFilePath))
{
// 从完整路径中提取文件名
string fileName = Path.GetFileName(displayText.TextFilePath);
// 从路径中识别/texts/目录
if (displayText.TextFilePath.Contains("texts"))
{
int textsIndex = displayText.TextFilePath.IndexOf("texts", StringComparison.OrdinalIgnoreCase);
string relativePath = displayText.TextFilePath.Substring(textsIndex);
displayText.TextFilePath = $"/{relativePath.Replace('\\', '/')}";
}
else
{
// 如果路径不包含texts目录则直接使用文件名
displayText.TextFilePath = $"/texts/{fileName}";
}
}
}
/// <summary>
/// 删除
/// </summary>
/// <returns></returns>
public async Task<bool> DelMonitorText(Guid id)
{
if (_monitorTextQueue.TryRemove(id, out _))
{
// 删除后保存到文件
SaveMonitorTextQueueToFile();
return true;
}
return false;
}
/// <summary>
/// 添加文本
/// </summary>
/// <returns></returns>
public async Task<bool> AddDisplayList(string text)
{
var id = Guid.NewGuid();
_displayTextQueue.TryAdd(id, new DisplayText()
{
CompletedText = text,
IsProcessed = true,
Id = id,
IsRealUser = true,
RecognitionId = "",
Text = text,
Timestamp = DateTime.Now
});
return false;
}
/// <summary>
/// 语音识别结果事件处理
/// </summary>
/// <param name="sender">发送者</param>
/// <param name="e">事件参数</param>
private async void OnSpeechToTextResultReceived(object sender, SpeechToTextResult e)
{
try
{
// 只处理最终结果,且不是仅用于显示的中间结果
if (e.IsFinal && !e.IsForDisplayOnly)
{
_logger.LogInformation($"接收到语音识别最终结果: {e.Text}");
// 发送频率限制每秒最多发送3次最终识别结果
DateTime now = DateTime.Now;
TimeSpan elapsed = now - _lastFinalResultSentTime;
// 检查是否已进入新的时间窗口
if (elapsed >= _finalResultThrottleWindow)
{
// 重置计数器和时间窗口
_finalResultCounter = 0;
_lastFinalResultSentTime = now;
}
// 检查当前时间窗口内是否已达到发送上限
if (_finalResultCounter < _maxFinalResultsPerSecond)
{
// 将最终结果发送给所有监听中的客户端
await _hubContext.Clients.Groups(new[] { "webadmin", "monitor" })
.SendAsync("ReceiveSpeechToEndTextResult", e.Text);
// 增加计数器
_finalResultCounter++;
}
else
{
_logger.LogDebug($"已达到最终结果发送频率限制,跳过发送: {e.Text}");
}
}
else
{
// 发送频率限制每秒最多发送3次实时识别结果
DateTime now = DateTime.Now;
TimeSpan elapsed = now - _lastRealTimeResultSentTime;
// 检查是否已进入新的时间窗口
if (elapsed >= _realTimeResultThrottleWindow)
{
// 重置计数器和时间窗口
_realTimeResultCounter = 0;
_lastRealTimeResultSentTime = now;
}
// 检查当前时间窗口内是否已达到发送上限
if (_realTimeResultCounter < _maxRealTimeResultsPerSecond)
{
// 发送实时识别结果给客户端
_logger.LogInformation($"接收到语音识别实时结果: {e.Text}");
await _hubContext.Clients.Groups(new[] { "webadmin", "monitor" })
.SendAsync("ReceiveSpeechToTextResult", e.Text);
// 增加计数器
_realTimeResultCounter++;
}
else
{
_logger.LogDebug($"已达到实时结果发送频率限制,跳过发送: {e.Text}");
}
}
}
catch (Exception ex)
{
_logger.LogError($"处理语音识别结果事件时出错: {ex.Message}");
}
}
/// <summary>
/// 添加识别文本到显示队列
/// </summary>
/// <param name="text">文本内容</param>
/// <param name="isRealUser">是否来自真实用户</param>
/// <param name="recognitionId">识别结果ID</param>
public void AddRecognizedTextToDisplay(string text, bool isRealUser = true, string recognitionId = null, SpeechRecognitionSession session = null)
{
if (string.IsNullOrWhiteSpace(text))
return;
var displayText = new DisplayText
{
Id = Guid.NewGuid(),
Text = text,
Timestamp = DateTime.Now,
IsRealUser = isRealUser,
RecognitionId = recognitionId
};
if (session != null)
{
displayText.TextFilePath = session.TextFilePath;
displayText.RecordingPath = session.RecordingPath;
}
if (isRealUser)
{
if (_configurationService.CurrentConfig.DisplayType == 1)
{
_monitorTextQueue.TryAdd(displayText.Id, displayText);
// 添加到监控队列后保存文件
SaveMonitorTextQueueToFile();
}
else if (_configurationService.CurrentConfig.DisplayType == 2)
{
_aiTextQueue.TryAdd(displayText.Id, displayText);
}
else
{
_displayTextQueue.TryAdd(displayText.Id, displayText);
}
_lastRealUserSpeakTime = DateTime.Now;
_logger.LogInformation($"添加真实用户文本到显示队列: {text}");
}
else
{
_displayTextQueue.TryAdd(displayText.Id, displayText);
_logger.LogInformation($"添加预设文本到显示队列: {text}");
}
// 确保定时器已初始化
InitializeDisplayTextTimer();
}
/// <summary>
/// 初始化显示文本定时器
/// </summary>
private void InitializeDisplayTextTimer()
{
// 注意:不再需要实际启动定时器,因为已改为客户端主动请求模式
// 但保留此方法以保持代码结构兼容性
if (!_isDisplayTimerInitialized)
{
lock (_displayTimerLock)
{
if (!_isDisplayTimerInitialized)
{
// 不再需要实际启动定时器
// _displayTextTimer = new Timer(SendNextDisplayText, null, 0, Timeout.Infinite);
_isDisplayTimerInitialized = true;
_logger.LogInformation("显示文本系统已初始化(客户端主动获取模式)");
}
}
}
}
// 注意:以下方法不再使用,由客户端主动获取替代,保留代码仅作参考
/// <summary>
/// 发送下一条显示文本(不再使用,由客户端主动获取替代)
/// </summary>
/// <param name="state">状态对象</param>
private async void SendNextDisplayText(object state)
{
// 这个方法不再使用,由客户端主动获取替代
// 保留原有逻辑仅作参考
try
{
// 获取随机等待时间(10-30秒)
int nextInterval = new Random().Next(10000, 30000);
// 设置下一次触发时间,此处设置为较长时间,避免频繁触发未使用的方法
_displayTextTimer?.Change(nextInterval, Timeout.Infinite);
_logger.LogWarning("使用了已废弃的发送显示文本方法,应改为客户端主动获取模式");
}
catch (Exception ex)
{
_logger.LogError($"发送显示文本失败: {ex.Message}");
}
}
/// <summary>
/// 开始接收显示文本兼容旧客户端新客户端应使用GetNextDisplayText
/// </summary>
public async Task StartReceivingDisplayText()
{
// 兼容旧版客户端的方法
_logger.LogWarning($"客户端 {Context.ConnectionId} 使用了已废弃的StartReceivingDisplayText方法应改为GetNextDisplayText");
// 转发到新方法
await GetNextDisplayText();
}
/// <summary>
/// 获取下一条要显示的文本(由客户端主动请求)
/// </summary>
/// <returns>处理任务</returns>
public async Task GetNextDisplayText()
{
try
{
// 检查客户端是否已注册
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
{
_logger.LogWarning($"未注册的客户端尝试获取显示文本: {Context.ConnectionId}");
await Clients.Caller.SendAsync("Error", "请先注册客户端");
return;
}
// 检查客户端是否为Display类型
if (clientInfo.ClientType != ClientType.Display)
{
_logger.LogWarning($"非显示端客户端尝试获取显示文本: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
await Clients.Caller.SendAsync("Error", "只有显示端客户端可以获取显示文本");
return;
}
_logger.LogInformation($"显示端请求获取下一条显示文本: {Context.ConnectionId}");
// 检查队列是否为空
if (_displayTextQueue.IsEmpty)
{
// 检查是否需要添加预设文本
if (DateTime.Now.Subtract(_lastRealUserSpeakTime).TotalSeconds > 30)
{
AddFakeTextToQueue();
}
else
{
_logger.LogInformation("显示文本队列为空,且不需要添加预设文本");
// 如果队列为空且不需要添加预设文本,返回空
return;
}
}
// 从队列中选择最高优先级的消息
var highestPriority = _displayTextQueue.OrderByDescending(kvp => kvp.Value.Priority)
.ThenBy(it => it.Value.Timestamp)
.FirstOrDefault();
// 检查是否获取到有效的消息
if (highestPriority.Key == Guid.Empty || highestPriority.Value == null)
{
_logger.LogWarning("无法从队列中获取有效的显示文本");
return;
}
// 在手动控屏模式下,只处理真实用户的消息
if (_manualScreenControlEnabled && !highestPriority.Value.IsRealUser)
{
_logger.LogInformation("当前为手动控屏模式,跳过非真实用户消息");
// 从队列中移除该消息,但不发送
if (_displayTextQueue.TryRemove(highestPriority.Key, out _))
{
// 返回空表示没有要显示的文本
await Clients.Caller.SendAsync("ReceiveDisplayText", "");
}
return;
}
// 从队列中移除该消息
if (_displayTextQueue.TryRemove(highestPriority.Key, out var textToDisplay))
{
// 只发送给请求的客户端
await Clients.Caller.SendAsync("ReceiveDisplayText", textToDisplay.Text);
_logger.LogInformation($"已发送显示文本到客户端: {textToDisplay.Text} (来源: {(textToDisplay.IsRealUser ? "" : "")})");
// 如果是真实用户的发言,持久化保存到文件
if (textToDisplay.IsRealUser)
{
SaveRealUserDisplayToFile(textToDisplay);
}
}
else
{
_logger.LogWarning($"无法从队列中移除显示文本: {highestPriority.Key}");
}
}
catch (Exception ex)
{
_logger.LogError($"获取显示文本失败: {ex.Message}");
}
}
/// <summary>
/// 添加预设文本到队列
/// </summary>
private void AddFakeTextToQueue()
{
try
{
// 确保预设句子列表不为空
if (_presetSentences.Count == 0)
{
LoadPresetSentencesFromFile();
// 如果加载后仍然为空,使用默认句子
if (_presetSentences.Count == 0)
{
_presetSentences.Add("记得每到夏天傍晚,您就摇着蒲扇坐在藤椅里,把切好的西瓜最甜那块硬塞给我。");
_presetSentences.Add("时光匆匆流逝,思念却越来越深。");
}
}
_presetSentences.OrderBy(it => Guid.NewGuid()).ToList().ForEach(item =>
{
var displayText = new DisplayText
{
Id = Guid.NewGuid(),
Text = item,
Timestamp = DateTime.Now,
IsRealUser = false,
RecognitionId = ""
};
_displayTextQueue.TryAdd(displayText.Id, displayText);
});
}
catch (Exception ex)
{
_logger.LogError($"添加预设文本失败: {ex.Message}");
// 使用一个默认句子防止程序崩溃
AddRecognizedTextToDisplay("时光匆匆流逝,思念却越来越深。", false);
}
}
/// <summary>
/// 从文件加载预设句子
/// </summary>
private void LoadPresetSentencesFromFile()
{
try
{
_presetSentences.Clear();
// 检查文件是否存在
if (!File.Exists(_sentencesFilePath))
{
_logger.LogWarning($"预设句子文件不存在: {_sentencesFilePath},将创建默认文件");
// 创建目录(如果不存在)
Directory.CreateDirectory(Path.GetDirectoryName(_sentencesFilePath));
// 写入默认的预设句子
File.WriteAllLines(_sentencesFilePath, new string[] {
});
}
// 读取文件中的每一行作为一个预设句子
string[] lines = File.ReadAllLines(_sentencesFilePath);
foreach (string line in lines)
{
// 忽略空行
if (!string.IsNullOrWhiteSpace(line))
{
_presetSentences.Add(line.Trim());
}
}
_presetSentences = _presetSentences.OrderBy(x => Guid.NewGuid()).ToList();
_logger.LogInformation($"成功从文件加载预设句子: {_presetSentences.Count} 条");
}
catch (Exception ex)
{
_logger.LogError($"加载预设句子失败: {ex.Message}");
}
}
/// <summary>
/// 从文件加载监控文本队列
/// </summary>
private void LoadMonitorTextQueueFromFile()
{
try
{
if (!File.Exists(_monitorTextQueueFilePath))
{
File.Create(_monitorTextQueueFilePath).Close();
_logger.LogInformation($"监控文本队列文件不存在: {_monitorTextQueueFilePath},手动创建");
//return;
}
string json = File.ReadAllText(_monitorTextQueueFilePath);
if (string.IsNullOrEmpty(json))
{
_logger.LogWarning("监控文本队列文件内容为空");
return;
}
var items = JsonConvert.DeserializeObject<List<DisplayText>>(json);
if (items == null || !items.Any())
{
_logger.LogWarning("没有从文件中读取到监控文本队列项");
return;
}
// 清空现有队列,并添加从文件加载的项
_monitorTextQueue.Clear();
foreach (var item in items)
{
_monitorTextQueue.TryAdd(item.Id, item);
}
_logger.LogInformation($"成功从文件加载监控文本队列: {items.Count} 项");
}
catch (Exception ex)
{
_logger.LogError($"加载监控文本队列失败: {ex.Message}");
}
}
/// <summary>
/// 保存监控文本队列到文件
/// </summary>
private void SaveMonitorTextQueueToFile()
{
try
{
if (_monitorTextQueue.IsEmpty)
{
_logger.LogInformation("监控文本队列为空,无需保存");
// 如果队列为空但文件存在,删除文件
if (File.Exists(_monitorTextQueueFilePath))
{
File.Delete(_monitorTextQueueFilePath);
_logger.LogInformation($"已删除空的监控文本队列文件: {_monitorTextQueueFilePath}");
}
return;
}
var items = _monitorTextQueue.Values.ToList();
string json = JsonConvert.SerializeObject(items, Formatting.Indented);
File.WriteAllText(_monitorTextQueueFilePath, json);
_logger.LogInformation($"成功保存监控文本队列到文件: {items.Count} 项");
}
catch (Exception ex)
{
_logger.LogError($"保存监控文本队列失败: {ex.Message}");
}
}
/// <summary>
/// 获取当前音频传输设置
/// </summary>
/// <returns>是否启用音频传输</returns>
public async Task<bool> GetAudioStreamingSetting()
{
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
{
_logger.LogWarning($"未注册的客户端尝试获取音频传输设置: {Context.ConnectionId}");
throw new HubException("请先注册客户端");
}
_logger.LogInformation($"客户端获取当前音频传输设置: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
return _configurationService.CurrentConfig.Network.EnableAudioStreaming;
}
/// <summary>
/// 更新音频传输设置
/// </summary>
/// <param name="enabled">是否启用音频传输</param>
/// <returns>处理任务</returns>
public async Task UpdateAudioStreaming(bool enabled)
{
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
{
_logger.LogWarning($"未注册的客户端尝试更新音频传输设置: {Context.ConnectionId}");
await Clients.Caller.SendAsync("Error", "请先注册客户端");
return;
}
if (clientInfo.ClientType != ClientType.Monitor && clientInfo.ClientType != ClientType.WebAdmin)
{
_logger.LogWarning($"非监控或管理端客户端尝试更新音频传输设置: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
await Clients.Caller.SendAsync("Error", "只有监控或管理端客户端可以更新音频传输设置");
return;
}
_logger.LogInformation($"更新音频传输设置: {Context.ConnectionId}, 新设置: {(enabled ? "" : "")}");
// 更新配置
_configurationService.CurrentConfig.Network.EnableAudioStreaming = enabled;
// 通知其他客户端音频传输设置已更改
await Clients.Groups(new[] { "monitor", "webadmin" }).SendAsync("AudioStreamingChanged", enabled);
// 保存配置到文件
try
{
var success = _configurationService.SaveConfiguration();
if (success)
{
await Clients.Caller.SendAsync("AudioStreamingUpdated", true, $"音频传输设置已更新为: {(enabled ? "" : "")}");
}
else
{
_logger.LogError("保存配置失败");
await Clients.Caller.SendAsync("AudioStreamingUpdated", false, "更新音频传输设置成功,但保存配置失败");
}
}
catch (Exception ex)
{
_logger.LogError($"保存配置到文件时出错: {ex.Message}");
await Clients.Caller.SendAsync("AudioStreamingUpdated", false, "更新音频传输设置成功,但保存配置失败");
}
}
/// <summary>
/// 释放资源
/// </summary>
/// <param name="disposing">是否正在处理Dispose</param>
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// 保存监控文本队列到文件
SaveMonitorTextQueueToFile();
// 取消订阅所有事件
_speechToTextService.ResultReceived -= OnSpeechToTextResultReceived;
_audioProcessingService.SpeechToTextResultReceived -= OnSpeechToTextResultReceived;
_audioProcessingService.AudioSavedToFile -= OnAudioSavedToFile;
_configurationService.ConfigurationChanged -= OnConfigurationChanged;
_logger.LogDebug("已取消订阅所有事件");
}
_disposed = true;
}
}
/// <summary>
/// 释放资源
/// </summary>
public new void Dispose()
{
Dispose(true);
base.Dispose();
GC.SuppressFinalize(this);
}
/// <summary>
/// 配置变更事件处理
/// </summary>
/// <param name="sender">发送者</param>
/// <param name="e">配置</param>
private async void OnConfigurationChanged(object sender, PhoneBoothConfig e)
{
try
{
_logger.LogInformation("配置已更新");
// 使用_hubContext替代Clients
await _hubContext.Clients.Group("controllers").SendAsync("ReceiveConfiguration", e);
await _hubContext.Clients.Group("webadmin").SendAsync("ReceiveConfiguration", e);
}
catch (Exception ex)
{
_logger.LogError($"处理配置变更事件时出错: {ex.Message}");
}
}
/// <summary>
/// 音频文件保存事件处理
/// </summary>
/// <param name="sender">发送者</param>
/// <param name="e">文件路径</param>
private async void OnAudioSavedToFile(object sender, string e)
{
try
{
// 快速检查路径是否已处理过,如果已处理过则直接返回
if (string.IsNullOrEmpty(e) || _globalProcessedPaths.ContainsKey(e))
{
return;
}
// 记录处理时间
_globalProcessedPaths[e] = DateTime.Now;
_logger.LogInformation($"音频保存到文件: {e}");
// 设置语音识别服务的录音文件路径
if (sender is IAudioProcessingService audioService)
{
// 查找对应的会话ID
var clientId = _clients.Keys.FirstOrDefault(id => audioService.GetRecordingFilePath(id) == e);
if (!string.IsNullOrEmpty(clientId))
{
// 生成会话文件路径的唯一标识符
string key = $"{clientId}:{e}";
// 添加到本地实例缓存 - 为双重保险
if (!_processedRecordingPaths.Contains(key))
{
_processedRecordingPaths.Add(key);
// 设置语音识别服务的录音文件路径
if (_speechToTextService is SpeechToTextService speechService)
{
_logger.LogInformation($"设置会话 {clientId} 的录音文件路径: {e}");
speechService.SetSessionRecordingPath(clientId, e);
}
}
}
}
// 使用_hubContext替代Clients
await _hubContext.Clients.Group("webadmin").SendAsync("AudioSavedToFile", e);
}
catch (Exception ex)
{
_logger.LogError($"处理音频保存事件时出错: {ex.Message}");
}
}
/// <summary>
/// 开始监听音频
/// </summary>
/// <returns>处理任务</returns>
public async Task StartMonitoringAudio()
{
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
{
_logger.LogWarning($"未注册的客户端尝试开始监听音频: {Context.ConnectionId}");
await Clients.Caller.SendAsync("Error", "请先注册客户端");
return;
}
if (clientInfo.ClientType != ClientType.Monitor && clientInfo.ClientType != ClientType.WebAdmin)
{
_logger.LogWarning($"非显示端或管理端客户端尝试开始监听音频: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
await Clients.Caller.SendAsync("Error", "只有显示端或管理端客户端可以监听音频");
return;
}
_logger.LogInformation($"开始监听音频: {Context.ConnectionId}");
// 将客户端标记为监听状态
clientInfo.IsMonitoring = true;
_clients[Context.ConnectionId] = clientInfo;
// 如果有正在进行的通话,通知客户端
var activeControllers = _clients.Values.Where(c => c.ClientType == ClientType.Controller && c.IsAudioStreaming).ToList();
if (activeControllers.Any())
{
await Clients.Caller.SendAsync("CallStateChanged", true);
}
else
{
await Clients.Caller.SendAsync("CallStateChanged", false);
}
}
/// <summary>
/// 停止监听音频
/// </summary>
/// <returns>处理任务</returns>
public async Task StopMonitoringAudio()
{
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
{
_logger.LogWarning($"未注册的客户端尝试停止监听音频: {Context.ConnectionId}");
return;
}
_logger.LogInformation($"停止监听音频: {Context.ConnectionId}");
// 将客户端标记为非监听状态
clientInfo.IsMonitoring = false;
_clients[Context.ConnectionId] = clientInfo;
await Clients.Caller.SendAsync("CallStateChanged", false);
}
/// <summary>
/// 从监控端发送消息
/// </summary>
/// <param name="sender">发送者姓名</param>
/// <param name="message">消息内容</param>
/// <returns>处理任务</returns>
public async Task SendMonitorMessage(string sender, string message)
{
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
{
_logger.LogWarning($"未注册的客户端尝试发送消息: {Context.ConnectionId}");
await Clients.Caller.SendAsync("Error", "请先注册客户端");
return;
}
if (clientInfo.ClientType != ClientType.Monitor && clientInfo.ClientType != ClientType.WebAdmin)
{
_logger.LogWarning($"非显示端或管理端客户端尝试发送消息: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
await Clients.Caller.SendAsync("Error", "只有显示端或管理端可以发送消息");
return;
}
_logger.LogInformation($"发送消息: {Context.ConnectionId}, 发送者: {sender}, 内容: {message}");
// 广播消息给所有显示端和管理端
await Clients.Groups(new[] { "displays", "webadmin", "monitor" }).SendAsync("ReceiveMessage", sender, message);
}
/// <summary>
/// 更新显示模式类型
/// </summary>
/// <param name="displayType">显示类型0表示识别立即显示1表示手动显示</param>
/// <returns>处理任务</returns>
public async Task UpdateDisplayType(int displayType)
{
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
{
_logger.LogWarning($"未注册的客户端尝试更新显示模式: {Context.ConnectionId}");
await Clients.Caller.SendAsync("Error", "请先注册客户端");
return;
}
if (clientInfo.ClientType != ClientType.Monitor && clientInfo.ClientType != ClientType.WebAdmin)
{
_logger.LogWarning($"非监控或管理端客户端尝试更新显示模式: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
await Clients.Caller.SendAsync("Error", "只有监控或管理端客户端可以更新显示模式");
return;
}
if (displayType < 0 || displayType > 2)
{
_logger.LogWarning($"客户端尝试设置无效的显示模式: {Context.ConnectionId}, 模式: {displayType}");
await Clients.Caller.SendAsync("Error", "无效的显示模式值");
return;
}
_logger.LogInformation($"更新显示模式: {Context.ConnectionId}, 新模式: {displayType}");
// 更新配置
_configurationService.CurrentConfig.DisplayType = displayType;
// 通知其他客户端显示模式已更改
await Clients.Groups(new[] { "monitor", "webadmin" }).SendAsync("DisplayTypeChanged", displayType);
// 保存配置到文件
try
{
var success = _configurationService.SaveConfiguration();
if (success)
{
await Clients.Caller.SendAsync("DisplayTypeUpdated", true, $"显示模式已更新为: {displayType}");
}
else
{
_logger.LogError("保存配置失败");
await Clients.Caller.SendAsync("DisplayTypeUpdated", false, "更新显示模式成功,但保存配置失败");
}
}
catch (Exception ex)
{
_logger.LogError($"保存配置到文件时出错: {ex.Message}");
await Clients.Caller.SendAsync("DisplayTypeUpdated", false, "更新显示模式成功,但保存配置失败");
}
}
/// <summary>
/// 获取当前显示模式
/// </summary>
/// <returns>当前显示模式</returns>
public async Task<int> GetDisplayType()
{
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
{
_logger.LogWarning($"未注册的客户端尝试获取显示模式: {Context.ConnectionId}");
throw new HubException("请先注册客户端");
}
_logger.LogInformation($"客户端获取当前显示模式: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
return _configurationService.CurrentConfig.DisplayType;
}
/// <summary>
/// 获取显示配置
/// </summary>
/// <returns>显示配置的JSON字符串</returns>
public string GetDisplayConfig()
{
lock (_displayConfigLock)
{
return _displayConfig;
}
}
/// <summary>
/// 保存显示配置
/// </summary>
/// <param name="configJson">配置的JSON字符串</param>
/// <returns>是否保存成功</returns>
public async Task<bool> SaveDisplayConfig(string configJson)
{
try
{
// 尝试解析JSON以验证格式
JsonDocument.Parse(configJson);
// 保存到文件
await File.WriteAllTextAsync(DisplayConfigPath, configJson);
// 更新内存中的配置
lock (_displayConfigLock)
{
_displayConfig = configJson;
}
// 通知所有显示客户端
await NotifyDisplayConfig();
return true;
}
catch (Exception ex)
{
Console.WriteLine($"保存显示配置出错: {ex.Message}");
return false;
}
}
/// <summary>
/// 通知所有显示客户端更新配置
/// </summary>
private async Task NotifyDisplayConfig()
{
string configJson;
lock (_displayConfigLock)
{
configJson = _displayConfig;
}
List<string> displayClientIds = new List<string>();
lock (_clients)
{
displayClientIds = _clients
.Where(c => c.Value.ClientType.ToString() == "Display")
.Select(c => c.Key)
.ToList();
}
foreach (var clientId in displayClientIds)
{
await Clients.Client(clientId).SendAsync("ReceiveDisplayConfig", configJson);
}
}
/// <summary>
/// 保存真实用户显示文本到文件
/// </summary>
/// <param name="displayText">要保存的显示文本对象</param>
private void SaveRealUserDisplayToFile(DisplayText displayText)
{
try
{
// 确保目录存在
string directory = Path.GetDirectoryName(_realUserDisplayLogsPath);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
// 使用简单的序列化方式,将对象转换为单行文本
string entry = JsonConvert.SerializeObject(new RealUserDisplayRecord
{
Text = displayText.Text,
Timestamp = displayText.Timestamp.ToString("yyyy-MM-dd HH:mm:ss"),
});
// 使用AppendAllText确保追加到文件末尾不覆盖原有内容
File.AppendAllText(_realUserDisplayLogsPath, entry + Environment.NewLine);
_logger.LogInformation($"已保存真实用户显示文本到文件: {displayText.Text}");
}
catch (Exception ex)
{
_logger.LogError($"保存真实用户显示文本失败: {ex.Message}");
}
}
/// <summary>
/// 获取当前控屏设置
/// </summary>
/// <returns>是否为手动控屏模式</returns>
public async Task<bool> GetScreenControlSetting()
{
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
{
_logger.LogWarning($"未注册的客户端尝试获取控屏设置: {Context.ConnectionId}");
throw new HubException("请先注册客户端");
}
_logger.LogInformation($"客户端获取当前控屏设置: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
return _manualScreenControlEnabled;
}
/// <summary>
/// 更新控屏设置
/// </summary>
/// <param name="isManual">是否为手动控屏模式</param>
/// <returns>更新结果</returns>
public async Task<bool> UpdateScreenControlSetting(bool isManual)
{
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
{
_logger.LogWarning($"未注册的客户端尝试更新控屏设置: {Context.ConnectionId}");
throw new HubException("请先注册客户端");
}
if (clientInfo.ClientType != ClientType.Monitor && clientInfo.ClientType != ClientType.WebAdmin)
{
_logger.LogWarning($"非监控端或管理端尝试更新控屏设置: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
throw new HubException("只有监控端或管理端可以更新控屏设置");
}
// 设置模式
_manualScreenControlEnabled = isManual;
// 保存设置
bool success = SaveScreenControlSetting();
if (success)
{
// 通知所有监控客户端更新设置
await Clients.Group("monitor").SendAsync("ScreenControlSettingChanged", isManual);
_logger.LogInformation($"控屏设置已更新为: {(isManual ? "" : "")}");
}
return success;
}
/// <summary>
/// 获取真实用户聊天记录
/// </summary>
/// <returns>用户聊天记录列表</returns>
public async Task<List<RealUserDisplayRecord>> GetRealUserChatRecords()
{
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
{
_logger.LogWarning($"未注册的客户端尝试获取真实用户聊天记录: {Context.ConnectionId}");
throw new HubException("请先注册客户端");
}
if (clientInfo.ClientType != ClientType.WebAdmin)
{
_logger.LogWarning($"非管理端尝试获取真实用户聊天记录: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
throw new HubException("只有管理端可以获取真实用户聊天记录");
}
var records = new List<RealUserDisplayRecord>();
try
{
if (File.Exists(_realUserDisplayLogsPath))
{
var lines = await File.ReadAllLinesAsync(_realUserDisplayLogsPath);
foreach (var line in lines)
{
try
{
if (!string.IsNullOrWhiteSpace(line))
{
var record = JsonConvert.DeserializeObject<RealUserDisplayRecord>(line);
records.Add(record);
}
}
catch (Exception ex)
{
_logger.LogError($"解析真实用户聊天记录行失败: {ex.Message}, 行内容: {line}");
}
}
}
else
{
_logger.LogWarning($"真实用户聊天记录文件不存在: {_realUserDisplayLogsPath}");
}
}
catch (Exception ex)
{
_logger.LogError($"获取真实用户聊天记录失败: {ex.Message}");
throw new HubException($"获取真实用户聊天记录失败: {ex.Message}");
}
return records;
}
}
}