ShengShengBuXi/ShengShengBuXi/Pages/Monitor.cshtml
2025-03-28 01:38:44 +08:00

1317 lines
56 KiB
Plaintext
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.

@page
@model ShengShengBuXi.Pages.MonitorModel
@{
ViewData["Title"] = "清竹园-中控页面";
}
<div class="container-fluid">
<!-- 头部区域 (100%, 20%) -->
<div class="row mb-3">
<div class="col-12">
<div class="card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<h2 class="my-2">清竹园-中控页面 <span id="connection-status" class="badge bg-warning">连接中...</span>
</h2>
<!-- 通话状态指示器 -->
<div class="d-flex align-items-center">
<div id="status-indicator" class="rounded-circle me-2"
style="width: 24px; height: 24px; background-color: red;"></div>
<span id="status-text" class="me-3">未检测到通话</span>
</div>
<!-- 单选按钮组 - 隐藏自动识别显示选项 -->
<div class="btn-group" role="group" style="display: none;">
<input type="radio" class="btn-check" name="displayMode" id="displayMode0" value="0"
checked>
<label class="btn btn-outline-primary" for="displayMode0">自动识别显示</label>
<input type="radio" class="btn-check" name="displayMode" id="displayMode1" value="1">
<label class="btn btn-outline-primary" for="displayMode1">手动处理显示</label>
</div>
<!-- 音频传输开关 - 隐藏关闭音频传输选项 -->
<div class="btn-group ms-3" role="group">
<input type="radio" class="btn-check" name="audioStreaming" id="audioStreaming1" value="1" checked>
<label class="btn btn-outline-success" for="audioStreaming1">开启音频传输</label>
<input type="radio" class="btn-check" name="audioStreaming" id="audioStreaming0" value="0" style="display: none;">
<label class="btn btn-outline-danger" for="audioStreaming0" style="display: none;">关闭音频传输</label>
<!-- 音量控制滑块 -->
<div class="ms-3 d-flex align-items-center">
<label for="volumeControl" class="me-2"><i class="bi bi-volume-up"></i></label>
<input type="range" class="form-range" min="0" max="1" step="0.1" value="1.0" id="volumeControl" style="width: 100px;">
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 主体区域 (20%+60%+20%, 80%) -->
<div class="row">
<!-- 左侧文字列表 (20%, 80%) -->
<div class="col-3">
<div class="card h-100">
<div class="card-header">
<h5 class="mb-0">监控文本列表</h5>
</div>
<div class="card-body p-0">
<div id="monitor-text-list" class="list-group list-group-flush"
style="max-height: 75vh; overflow-y: auto;">
<!-- 文本列表内容将通过JS动态填充 -->
<div class="text-center text-muted p-3">加载中...</div>
</div>
</div>
</div>
</div>
<!-- 中间区域 (60%, 80%) -->
<div class="col-6">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<button class="btn btn-sm btn-outline-primary me-2" onclick="copyDisplayedText()">
<i class="bi bi-clipboard"></i> 复制文本
</button>
</div>
<button class="btn btn-sm btn-outline-secondary" onclick="closeTextDisplay()">关闭</button>
</div>
<div class="card-body">
<div id="content-area" style="height: 75vh;">
<!-- 文本显示区域 -->
<div id="text-display-area" class="mb-4" style="display: none; height: 20%;">
<div class="card">
<div class="card-body">
<p id="displayed-text" class="mb-0"></p>
</div>
</div>
</div>
<!-- 文本输入区域 -->
<div id="input-text-area" style="height: 40%;">
<div class="card h-100">
<div class="card-header">
<h5 class="mb-0">文本编辑</h5>
</div>
<div class="card-body">
<textarea id="text-input" class="form-control h-100"
placeholder="请输入要显示的文本..."></textarea>
</div>
</div>
</div>
<!-- 按钮组区域 - 隐藏添加并移除按钮 -->
<div id="action-buttons-area" class="mt-3" style="height: 20%;">
<div class="card h-100">
<div class="card-body d-flex justify-content-center align-items-center">
<button id="add-text-btn" class="btn btn-primary me-3" data-bs-toggle="tooltip"
title="将文本添加到显示队列,并显示在大屏上" onclick="addDisplayText()">
<i class="bi bi-plus-circle"></i> 添加到显示
</button>
<button id="add-and-remove-btn" class="btn btn-warning" data-bs-toggle="tooltip"
title="将文本添加到显示队列,并从监控列表中移除当前选中项" onclick="addDisplayTextAndRemoveMonitor()" style="display: none;">
<i class="bi bi-arrow-right-circle"></i> 添加并移除
</button>
</div>
</div>
</div>
<!-- 其他中间内容 -->
<div id="other-content">
<!-- 中间的空div -->
</div>
</div>
</div>
</div>
</div>
<!-- 右侧文字列表 (20%, 80%) -->
<div class="col-3">
<div class="card h-100">
<div class="card-header">
<h5 class="mb-0">显示文本列表</h5>
</div>
<div class="card-body p-0">
<div id="display-text-list" class="list-group list-group-flush"
style="max-height: 75vh; overflow-y: auto;">
<!-- 文本列表内容将通过JS动态填充 -->
<div class="text-center text-muted p-3">加载中...</div>
</div>
</div>
</div>
</div>
</div>
<!-- 调试区域 (底部) -->
<div class="row mt-3">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>调试信息</span>
<div>
<button class="btn btn-sm btn-outline-danger me-2" onclick="clearDebug()">清空</button>
<button class="btn btn-sm btn-outline-secondary" onclick="toggleDebug()">隐藏</button>
</div>
</div>
<div class="card-body">
<pre id="debug-log" style="max-height: 150px; overflow-y: auto;"></pre>
</div>
</div>
</div>
</div>
</div>
<!-- 消息区域 -->
<div id="message-area" class="position-fixed bottom-0 end-0 p-3" style="z-index: 1050;"></div>
@section Scripts {
<script src="~/lib/microsoft-signalr/signalr.min.js"></script>
<script>
let connection = null;
let refreshDisplayInterval = null;
let callInProgress = false;
// 当前播放的音频元素和按钮引用
let currentAudio = null;
let currentPlayButton = null;
// 实时音频相关变量
let audioContext = null;
let audioStreamSource = null;
let isAudioStreamEnabled = false;
let audioGainNode = null;
let currentVolume = 1.0; // 默认音量为1.0 (100%)
// 调试日志
function log(message) {
const timestamp = new Date().toLocaleTimeString();
const logMsg = `[${timestamp}] ${message}`;
console.log(logMsg);
const logElem = document.getElementById("debug-log");
if (logElem) {
const logLine = document.createElement("div");
logLine.textContent = logMsg;
logElem.insertBefore(logLine, logElem.firstChild);
}
}
// 显示/隐藏调试区域
function toggleDebug() {
const debugCard = document.querySelector("#debug-log").closest(".card");
const isVisible = debugCard.style.display !== "none";
debugCard.style.display = isVisible ? "none" : "";
}
// 清空调试日志
function clearDebug() {
document.getElementById("debug-log").innerHTML = "";
}
// 显示消息提示
function showMessage(message, type = 'info', duration = 5000) {
const messageArea = document.getElementById("message-area");
const alert = document.createElement("div");
alert.className = `alert alert-${type} alert-dismissible fade show`;
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`;
messageArea.appendChild(alert);
// 自动移除
setTimeout(() => {
alert.classList.remove('show');
setTimeout(() => alert.remove(), 150);
}, duration);
}
// 更新连接状态
function updateConnectionStatus(status, type = 'warning') {
const statusElem = document.getElementById("connection-status");
statusElem.className = `badge bg-${type}`;
statusElem.textContent = status;
}
// 初始化SignalR连接
function initSignalR() {
log("初始化SignalR连接...");
updateConnectionStatus("连接中...", "warning");
// 创建连接
connection = new signalR.HubConnectionBuilder()
.withUrl("/audiohub")
.configureLogging(signalR.LogLevel.Debug)
.withAutomaticReconnect()
.build();
// 注册连接事件处理程序
connection.onreconnecting(error => {
log("重新连接中: " + (error ? error.message : "未知错误"));
updateConnectionStatus("重连中...", "warning");
});
connection.onreconnected(connectionId => {
log("已重新连接ID: " + connectionId);
updateConnectionStatus("已连接", "success");
// 重新注册
registerAsMonitor();
});
connection.onclose(error => {
log("连接已关闭: " + (error ? error.message : ""));
updateConnectionStatus("已断开", "danger");
// 5秒后重连
setTimeout(() => startConnection(), 5000);
});
// 注册服务器消息处理程序
setupSignalRHandlers();
// 建立连接
startConnection();
}
// 开始连接
function startConnection() {
log("正在连接到服务器...");
connection.start()
.then(() => {
log("已连接到服务器连接ID: " + connection.connectionId);
updateConnectionStatus("已连接", "success");
registerAsMonitor();
})
.catch(err => {
log("连接失败: " + err);
updateConnectionStatus("连接失败", "danger");
setTimeout(() => startConnection(), 5000);
});
}
// 注册为中控客户端
function registerAsMonitor() {
if (!connection || connection.state !== signalR.HubConnectionState.Connected) {
log("无法注册:未连接");
return;
}
log("正在注册为中控客户端...");
connection.invoke("RegisterClient", 2, "中控监视页面")
.then(() => {
log("已注册为中控客户端");
// 加载初始数据
loadMonitorTextList();
loadDisplayTextList();
// 获取当前显示模式
getServerDisplayType();
// 获取当前音频流设置
getServerAudioStreamingSetting();
// 设置定时刷新显示列表
if (refreshDisplayInterval) {
clearInterval(refreshDisplayInterval);
}
refreshDisplayInterval = setInterval(loadDisplayTextList, 10000); // 每10秒刷新一次
})
.catch(err => {
log("注册失败: " + err);
showMessage("注册失败: " + err, "danger");
});
}
// 设置SignalR事件处理程序
function setupSignalRHandlers() {
// 注册确认
connection.on("RegistrationConfirmed", (clientId) => {
log("注册确认客户端ID: " + clientId);
showMessage("已注册为中控客户端", "success");
});
// 注册失败
connection.on("RegistrationFailed", (reason) => {
log("注册失败: " + reason);
showMessage("注册失败: " + reason, "danger");
});
// 通话状态改变
connection.on("CallStateChanged", (isActive) => {
updateCallStatus(isActive);
});
// 错误消息
connection.on("Error", (message) => {
log("错误: " + message);
showMessage("错误: " + message, "danger");
});
// 显示模式更新消息
connection.on("DisplayTypeChanged", (displayType) => {
log(`服务器显示模式已更改为: ${displayType}`);
updateDisplayModeUI(displayType);
});
// 显示模式更新结果
connection.on("DisplayTypeUpdated", (success, message) => {
if (success) {
log(`显示模式更新成功: ${message}`);
showMessage(message, "success");
} else {
log(`显示模式更新失败: ${message}`);
showMessage(message, "danger");
}
});
// 接收实时音频数据
connection.on("ReceiveAudioData", (audioData) => {
if (isAudioStreamEnabled && callInProgress) {
// 确保音频流开启且通话进行中
log("接收到实时音频数据...");
// 直接传递原始数据让playRealTimeAudio函数内部处理
playRealTimeAudio(audioData);
}
});
// 音频流设置更新消息
connection.on("AudioStreamingChanged", (enabled) => {
log(`服务器音频流设置已更改为: ${enabled ? "开启" : "关闭"}`);
updateAudioStreamingUI(enabled);
});
// 音频流设置更新结果
connection.on("AudioStreamingUpdated", (success, message) => {
if (success) {
log(`音频流设置更新成功: ${message}`);
showMessage(message, "success");
} else {
log(`音频流设置更新失败: ${message}`);
showMessage(message, "danger");
}
});
}
// 更新通话状态
function updateCallStatus(isActive) {
callInProgress = isActive;
const indicator = document.getElementById("status-indicator");
const statusText = document.getElementById("status-text");
if (isActive) {
indicator.style.backgroundColor = "green";
statusText.textContent = "正在通话中";
// 初始化音频上下文(如果需要且启用了音频流)
if (isAudioStreamEnabled && !audioContext) {
initAudioContext();
}
} else {
indicator.style.backgroundColor = "red";
statusText.textContent = "未检测到通话";
}
// 有新通话时刷新监控列表
loadMonitorTextList();
}
// 加载监控文本列表
function loadMonitorTextList() {
if (!connection || connection.state !== signalR.HubConnectionState.Connected) {
log("无法加载监控文本列表:未连接");
return;
}
log("加载监控文本列表...");
connection.invoke("GetMonitorTextList")
.then(data => {
log(`获取到${data.length}条监控文本`);
renderMonitorTextList(data);
})
.catch(err => {
log("获取监控文本列表失败: " + err);
showMessage("获取监控文本列表失败", "danger");
});
}
// 渲染监控文本列表
function renderMonitorTextList(data) {
const container = document.getElementById("monitor-text-list");
container.innerHTML = ""; // 清空现有内容
if (!data || data.length === 0) {
container.innerHTML = '<div class="text-center text-muted p-3">暂无监控文本</div>';
return;
}
// 按时间倒序排列
data.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
// 渲染每一项
data.forEach(item => {
const listItem = document.createElement("div");
listItem.className = "list-group-item";
listItem.dataset.id = item.id;
// 存储原始文本,用于点击后显示
listItem.dataset.fullText = item.text || item.completedText || "无文本内容";
// 添加点击事件
listItem.addEventListener('click', function (e) {
// 如果点击的是按钮,不触发选择
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'I' ||
e.target.closest('button')) {
return;
}
// 移除其他项的active类
document.querySelectorAll("#monitor-text-list .list-group-item").forEach(item => {
item.classList.remove("active");
});
// 添加active类到当前项
this.classList.add("active");
// 显示文本
displayTextInCenter(this.dataset.fullText);
});
// 转换日期格式
const date = new Date(item.timestamp);
const formattedDate = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`;
const text = item.text || item.completedText || "无文本内容";
// 截取前10个字符若不足10个则全部显示
const shortText = text.length > 10 ? text.substring(0, 10) : text;
listItem.innerHTML = `
<div class="d-flex justify-content-between align-items-start mb-1">
<small class="text-muted">【${shortText}】</small>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary btn-sm" onclick="playAudio('${item.recordingPath || ''}', this)">
<i class="bi bi-play-fill"></i>
</button>
<button class="btn btn-outline-secondary btn-sm" onclick="downloadRecording('${item.recordingPath || ''}')">
<i class="bi bi-download"></i>
</button>
<button class="btn btn-outline-danger btn-sm" onclick="deleteMonitorText('${item.id}')">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
<div>${formattedDate}</div>
`;
container.appendChild(listItem);
});
}
// 删除监控文本
function deleteMonitorText(id) {
if (!id || !connection || connection.state !== signalR.HubConnectionState.Connected) {
log("无法删除监控文本未连接或ID无效");
return;
}
log(`正在删除监控文本: ${id}`);
connection.invoke("DelMonitorText", id)
.then(result => {
log(`删除监控文本${result ? '成功' : '失败'}`);
if (result) {
// 从UI中移除该项
const item = document.querySelector(`#monitor-text-list .list-group-item[data-id="${id}"]`);
if (item) {
item.remove();
}
showMessage("已删除文本", "success");
} else {
showMessage("删除文本失败", "warning");
}
})
.catch(err => {
log("删除监控文本失败: " + err);
showMessage("删除监控文本失败: " + err, "danger");
});
}
// 播放或暂停音频
function playAudio(path, buttonElement) {
// 获取按钮元素
const button = buttonElement || event.currentTarget;
const buttonIcon = button.querySelector('i');
// 如果没有路径
if (!path) {
showMessage("无可播放的录音", "warning");
return;
}
// 如果是当前正在播放的音频
if (currentAudio && currentPlayButton === button) {
if (currentAudio.paused) {
// 如果当前是暂停状态,则继续播放
log(`继续播放音频: ${path}`);
currentAudio.play()
.then(() => {
// 更改按钮图标为暂停
buttonIcon.className = "bi bi-pause-fill";
showMessage("继续播放音频", "info");
})
.catch(err => {
showMessage(`播放失败: ${err}`, "danger");
log(`继续播放失败: ${err}`);
});
} else {
// 如果当前是播放状态,则暂停
log(`暂停播放音频: ${path}`);
currentAudio.pause();
// 更改按钮图标为播放
buttonIcon.className = "bi bi-play-fill";
showMessage("已暂停播放", "info");
}
return;
}
// 如果有其他正在播放的音频,先停止它
if (currentAudio) {
currentAudio.pause();
// 恢复之前的播放按钮图标
if (currentPlayButton && currentPlayButton.querySelector('i')) {
currentPlayButton.querySelector('i').className = "bi bi-play-fill";
}
}
log(`播放音频: ${path}`);
// 创建新的音频元素
const audioElement = new Audio(path);
// 应用当前音量设置
audioElement.volume = currentVolume;
// 设置当前播放的音频和按钮
currentAudio = audioElement;
currentPlayButton = button;
// 添加事件处理
audioElement.addEventListener('error', () => {
showMessage("无法播放音频文件,可能不存在或格式不支持", "warning");
log(`音频播放失败: ${path}`);
// 重置当前播放状态
currentAudio = null;
currentPlayButton = null;
buttonIcon.className = "bi bi-play-fill";
});
audioElement.addEventListener('loadeddata', () => {
log(`音频已加载,开始播放: ${path}, 音量: ${currentVolume * 100}%`);
});
audioElement.addEventListener('ended', () => {
log(`音频播放结束: ${path}`);
// 重置按钮状态
buttonIcon.className = "bi bi-play-fill";
// 重置当前播放状态
currentAudio = null;
currentPlayButton = null;
});
// 播放音频
audioElement.play()
.then(() => {
// 更改按钮图标为暂停
buttonIcon.className = "bi bi-pause-fill";
showMessage("正在播放音频", "info");
})
.catch(err => {
showMessage(`播放失败: ${err}`, "danger");
log(`播放失败: ${err}`);
// 重置当前播放状态
currentAudio = null;
currentPlayButton = null;
});
}
// 下载录音
function downloadRecording(path) {
if (!path) {
showMessage("无可下载的录音", "warning");
return;
}
log(`下载录音: ${path}`);
// 创建一个链接元素用于下载
const a = document.createElement('a');
a.href = path;
a.download = path.split('/').pop(); // 从路径中提取文件名
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
showMessage("正在下载录音文件", "info");
}
// 加载显示文本列表
function loadDisplayTextList() {
if (!connection || connection.state !== signalR.HubConnectionState.Connected) {
log("无法加载显示文本列表:未连接");
return;
}
log("加载显示文本列表...");
connection.invoke("GetDisplayList")
.then(data => {
log(`获取到${data.length}条显示文本`);
renderDisplayTextList(data);
})
.catch(err => {
log("获取显示文本列表失败: " + err);
});
}
// 渲染显示文本列表
function renderDisplayTextList(data) {
const container = document.getElementById("display-text-list");
container.innerHTML = ""; // 清空现有内容
if (!data || data.length === 0) {
container.innerHTML = '<div class="text-center text-muted p-3">暂无显示文本</div>';
return;
}
// 按时间倒序排列
data.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
// 渲染每一项
data.forEach(item => {
const listItem = document.createElement("div");
listItem.className = "list-group-item";
// 存储原始文本,用于点击后显示
listItem.dataset.fullText = item.text || item.completedText || "无文本内容";
// 添加点击事件
listItem.addEventListener('click', function () {
onlyDisplayTextInCenter(this.dataset.fullText);
});
// 转换日期格式
const date = new Date(item.timestamp);
const formattedDate = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`;
const text = item.text || item.completedText || "无文本内容";
// 截取前5个字符若不足5个则全部显示
const shortText = text.length > 5 ? text.substring(0, 5) : text;
listItem.innerHTML = `
<div class="mb-1">
<small class="text-muted">${formattedDate}</small>
</div>
<div>【${shortText}】</div>
`;
container.appendChild(listItem);
});
}
// 监听显示模式单选按钮的变化
function setupDisplayModeListeners() {
const displayModeButtons = document.querySelectorAll('input[name="displayMode"]');
displayModeButtons.forEach(radio => {
radio.addEventListener('change', function () {
const mode = parseInt(this.value);
log(`显示模式已切换为: ${mode === 0 ? "识别立即显示" : "手动显示"}`);
// 调用服务器端方法更新显示模式
if (connection && connection.state === signalR.HubConnectionState.Connected) {
connection.invoke("UpdateDisplayType", mode)
.then(() => {
showMessage(`已切换为${mode === 0 ? "识别立即显示" : "手动显示"}模式`, "success");
})
.catch(err => {
log(`更新显示模式失败: ${err}`);
showMessage("切换显示模式失败", "danger");
// 恢复原来的选择
const currentMode = mode === 0 ? 1 : 0;
document.getElementById(`displayMode${currentMode}`).checked = true;
});
} else {
log("无法更新显示模式:未连接到服务器");
showMessage("无法更新显示模式:未连接到服务器", "warning");
}
});
});
// 监听音频传输单选按钮的变化
const audioStreamingButtons = document.querySelectorAll('input[name="audioStreaming"]');
audioStreamingButtons.forEach(radio => {
radio.addEventListener('change', function () {
const enabled = parseInt(this.value) === 1;
log(`音频传输已切换为: ${enabled ? "开启" : "关闭"}`);
// 设置本地音频流状态
isAudioStreamEnabled = enabled;
// 如果开启了音频流,初始化音频上下文
if (enabled && callInProgress) {
initAudioContext();
}
// 调用服务器端方法更新音频流设置
if (connection && connection.state === signalR.HubConnectionState.Connected) {
connection.invoke("UpdateAudioStreaming", enabled)
.then(() => {
showMessage(`已${enabled ? "开启" : "关闭"}音频传输`, "success");
})
.catch(err => {
log(`更新音频流设置失败: ${err}`);
showMessage("切换音频传输失败", "danger");
// 恢复原来的选择
const currentValue = enabled ? 0 : 1;
document.getElementById(`audioStreaming${currentValue}`).checked = true;
isAudioStreamEnabled = !enabled;
});
} else {
log("无法更新音频流设置:未连接到服务器");
showMessage("无法更新音频流设置:未连接到服务器", "warning");
}
});
});
}
// 页面加载完成后初始化
document.addEventListener("DOMContentLoaded", function () {
log("页面已加载");
// 添加Bootstrap Icons样式
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css';
document.head.appendChild(link);
// 初始化SignalR连接
initSignalR();
// 设置显示模式和音频流监听器
setupDisplayModeListeners();
// 设置音量控制监听器
setupVolumeControl();
// 初始化工具提示
setTimeout(initTooltips, 1000);
// 默认设置为手动显示模式
document.getElementById("displayMode1").checked = true;
// 默认开启音频传输
document.getElementById("audioStreaming1").checked = true;
isAudioStreamEnabled = true;
// 显示当前数据处理模式的提示消息
setTimeout(() => {
showMessage("当前为手动处理数据模式,需要您手动审核并添加文本到显示队列", "info", 10000);
}, 1500);
});
// 页面卸载前清理资源
window.addEventListener('beforeunload', function () {
if (refreshDisplayInterval) {
clearInterval(refreshDisplayInterval);
}
// 关闭音频上下文
if (audioContext) {
audioContext.close().catch(e => console.log("关闭音频上下文失败: " + e));
}
});
// 更新显示模式UI
function updateDisplayModeUI(displayType) {
// 将displayType转换为整数确保类型匹配
displayType = parseInt(displayType);
// 确保值在有效范围内(0或1)
if (displayType === 0 || displayType === 1) {
// 更新单选按钮状态
document.getElementById(`displayMode${displayType}`).checked = true;
log(`UI显示模式已更新为: ${displayType === 0 ? "识别立即显示" : "手动显示"}`);
}
}
// 获取服务器当前的显示模式
function getServerDisplayType() {
if (!connection || connection.state !== signalR.HubConnectionState.Connected) {
log("无法获取显示模式:未连接");
return;
}
log("获取服务器当前显示模式...");
connection.invoke("GetDisplayType")
.then(displayType => {
log(`获取到服务器当前显示模式: ${displayType}`);
updateDisplayModeUI(displayType);
})
.catch(err => {
log("获取显示模式失败: " + err);
});
}
// 在中间区域显示文本(用于左侧列表,包含编辑功能)
function displayTextInCenter(text) {
// 显示文本区域
const textDisplayArea = document.getElementById("text-display-area");
textDisplayArea.style.display = "block";
// 设置文本内容
const displayedText = document.getElementById("displayed-text");
displayedText.textContent = text;
// 同时将文本填入输入框
document.getElementById("text-input").value = text;
// 显示文本编辑区域和按钮区域
document.getElementById("input-text-area").style.display = "block";
document.getElementById("action-buttons-area").style.display = "block";
log("在中间区域显示文本(带编辑功能)");
}
// 在中间区域只显示文本(用于右侧列表,不包含编辑功能)
function onlyDisplayTextInCenter(text) {
// 显示文本区域
const textDisplayArea = document.getElementById("text-display-area");
textDisplayArea.style.display = "block";
// 设置文本内容
const displayedText = document.getElementById("displayed-text");
displayedText.textContent = text;
// 隐藏文本编辑区域和按钮区域
document.getElementById("input-text-area").style.display = "none";
document.getElementById("action-buttons-area").style.display = "none";
log("在中间区域只显示文本(无编辑功能)");
}
// 关闭文本显示
function closeTextDisplay() {
const textDisplayArea = document.getElementById("text-display-area");
textDisplayArea.style.display = "none";
// 重新显示编辑区域和按钮区域
document.getElementById("input-text-area").style.display = "block";
document.getElementById("action-buttons-area").style.display = "block";
log("关闭文本显示");
}
// 复制显示的文本
function copyDisplayedText() {
const textArea = document.getElementById("displayed-text");
const text = textArea.textContent;
if (!text || text.trim() === "") {
showMessage("请先从左侧列表选择一条文本记录", "warning");
log("尝试复制文本失败:未选择文本");
return;
}
// 创建临时textarea元素用于复制
const tempTextArea = document.createElement("textarea");
tempTextArea.value = text;
document.body.appendChild(tempTextArea);
tempTextArea.select();
try {
// 尝试复制到剪贴板
const success = document.execCommand("copy");
if (success) {
showMessage("文本已复制到剪贴板", "success");
log("文本已复制到剪贴板");
} else {
showMessage("复制失败,请手动复制", "warning");
log("复制文本失败execCommand返回false");
}
} catch (err) {
showMessage("复制失败: " + err, "danger");
log("复制文本失败: " + err);
}
// 移除临时元素
document.body.removeChild(tempTextArea);
}
// 初始化工具提示
function initTooltips() {
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
}
// 添加显示文本
function addDisplayText() {
const textInput = document.getElementById("text-input");
const text = textInput.value.trim();
if (!text) {
showMessage("请输入要显示的文本", "warning");
log("添加显示文本失败:未输入文本");
return;
}
if (!connection || connection.state !== signalR.HubConnectionState.Connected) {
showMessage("无法添加显示文本:未连接到服务器", "danger");
log("添加显示文本失败:未连接到服务器");
return;
}
log(`正在添加显示文本: ${text.substring(0, 20)}${text.length > 20 ? '...' : ''}`);
connection.invoke("AddDisplayList", text)
.then(result => {
log("显示文本添加成功");
showMessage("文本已添加到显示队列", "success");
// 清空输入框
textInput.value = "";
// 刷新右侧显示文本列表
loadDisplayTextList();
})
.catch(err => {
log(`添加显示文本失败: ${err}`);
showMessage("添加显示文本失败", "danger");
});
}
// 添加显示文本并从监控列表中移除
function addDisplayTextAndRemoveMonitor() {
const textInput = document.getElementById("text-input");
const text = textInput.value.trim();
// 获取当前选中的监控文本项
const selectedMonitorItem = document.querySelector("#monitor-text-list .list-group-item.active");
if (!text) {
showMessage("请输入要显示的文本", "warning");
log("添加并移除操作失败:未输入文本");
return;
}
if (!selectedMonitorItem) {
showMessage("请先从左侧列表选择一条文本记录", "warning");
log("添加并移除操作失败:未选择监控文本");
return;
}
if (!connection || connection.state !== signalR.HubConnectionState.Connected) {
showMessage("无法添加显示文本:未连接到服务器", "danger");
log("添加显示文本失败:未连接到服务器");
return;
}
const monitorTextId = selectedMonitorItem.dataset.id;
log(`正在添加显示文本并移除监控文本: ${text.substring(0, 20)}${text.length > 20 ? '...' : ''}, ID: ${monitorTextId}`);
// 先添加显示文本
connection.invoke("AddDisplayList", text)
.then(result => {
log("显示文本添加成功,正在删除监控文本");
// 然后删除监控文本
connection.invoke("DelMonitorText", monitorTextId)
.then(delResult => {
if (delResult) {
log("监控文本删除成功");
// 从UI中移除该项
selectedMonitorItem.remove();
// 清空输入框
textInput.value = "";
// 关闭文本显示
closeTextDisplay();
// 刷新右侧显示文本列表
loadDisplayTextList();
showMessage("文本已添加到显示队列并从监控列表移除", "success");
} else {
log("监控文本删除失败");
showMessage("文本已添加到显示队列,但从监控列表移除失败", "warning");
// 刷新右侧显示文本列表
loadDisplayTextList();
}
})
.catch(err => {
log(`删除监控文本失败: ${err}`);
showMessage("文本已添加到显示队列,但从监控列表移除失败", "warning");
// 刷新右侧显示文本列表
loadDisplayTextList();
});
})
.catch(err => {
log(`添加显示文本失败: ${err}`);
showMessage("添加显示文本失败", "danger");
});
}
// 获取服务器当前的音频流设置
function getServerAudioStreamingSetting() {
if (!connection || connection.state !== signalR.HubConnectionState.Connected) {
log("无法获取音频流设置:未连接");
return;
}
log("获取服务器当前音频流设置...");
connection.invoke("GetAudioStreamingSetting")
.then(enabled => {
log(`获取到服务器当前音频流设置: ${enabled ? "开启" : "关闭"}`);
updateAudioStreamingUI(enabled);
isAudioStreamEnabled = enabled;
})
.catch(err => {
log("获取音频流设置失败: " + err);
});
}
// 更新音频流设置UI
function updateAudioStreamingUI(enabled) {
// 更新单选按钮状态
document.getElementById(`audioStreaming${enabled ? 1 : 0}`).checked = true;
// 更新本地状态
isAudioStreamEnabled = enabled;
log(`UI音频流设置已更新为: ${enabled ? "开启" : "关闭"}`);
}
// 设置音量控制监听器
function setupVolumeControl() {
const volumeControl = document.getElementById('volumeControl');
if (volumeControl) {
volumeControl.addEventListener('input', function(e) {
currentVolume = parseFloat(e.target.value);
log(`音量已调整为: ${currentVolume * 100}%`);
// 如果已经创建了增益节点,立即应用音量设置
if (audioGainNode) {
audioGainNode.gain.value = currentVolume;
}
// 同时调整已播放的录音音量
if (currentAudio) {
currentAudio.volume = currentVolume;
}
});
}
}
// 初始化音频上下文
function initAudioContext() {
if (audioContext) return; // 避免重复初始化
try {
// 创建音频上下文
const AudioContext = window.AudioContext || window.webkitAudioContext;
audioContext = new AudioContext();
// 创建增益节点用于控制音量
audioGainNode = audioContext.createGain();
audioGainNode.gain.value = currentVolume; // 设置初始音量
audioGainNode.connect(audioContext.destination);
log("音频上下文已初始化,创建了增益节点,初始音量: " + currentVolume);
// 如果音频上下文处于挂起状态,需要用户交互来激活
if (audioContext.state === 'suspended') {
const resumeAudio = function () {
audioContext.resume().then(() => {
log("用户交互已激活音频上下文");
document.removeEventListener('click', resumeAudio);
document.removeEventListener('touchstart', resumeAudio);
document.removeEventListener('touchend', resumeAudio);
});
};
document.addEventListener('click', resumeAudio);
document.addEventListener('touchstart', resumeAudio);
document.addEventListener('touchend', resumeAudio);
}
log("音频上下文已初始化,状态: " + audioContext.state + ", 采样率: " + audioContext.sampleRate);
} catch (e) {
log("无法创建音频上下文: " + e);
showMessage("无法初始化音频播放: " + e, "danger");
}
}
// 播放实时音频
function playRealTimeAudio(audioData) {
if (!audioContext || !isAudioStreamEnabled) return;
try {
log("接收到实时音频数据...");
// 检查音频数据类型
log(`音频数据类型: ${Object.prototype.toString.call(audioData)}, 长度: ${audioData ? (audioData.length || audioData.byteLength || 'unknown') : 'null'}`);
// 处理接收到的音频数据
let pcmData;
if (audioData instanceof Uint8Array) {
log("接收到 Uint8Array 类型数据");
pcmData = audioData;
} else if (audioData instanceof ArrayBuffer) {
log("接收到 ArrayBuffer 类型数据");
pcmData = new Uint8Array(audioData);
} else if (Array.isArray(audioData)) {
log("接收到数组类型数据,尝试转换");
// 尝试将数组转换为 Uint8Array
pcmData = new Uint8Array(audioData);
} else if (typeof audioData === 'object' && audioData !== null) {
log("接收到对象类型数据,尝试解析");
// 尝试解析对象
if (audioData.data && (audioData.data instanceof Uint8Array || audioData.data instanceof ArrayBuffer)) {
pcmData = audioData.data instanceof Uint8Array ? audioData.data : new Uint8Array(audioData.data);
} else if (audioData.buffer && audioData.buffer instanceof ArrayBuffer) {
pcmData = new Uint8Array(audioData.buffer);
} else {
// 尝试将对象转换为JSON并记录以便调试
try {
log("对象数据:" + JSON.stringify(audioData).substring(0, 100));
} catch (e) {
log("无法序列化对象: " + e);
}
log("无法识别的对象类型数据");
return;
}
} else {
log(`不支持的音频数据格式: ${typeof audioData}`);
// 尝试更详细地记录数据内容
if (typeof audioData === 'string') {
log("字符串数据前20字符: " + audioData.substring(0, 20));
// 尝试从Base64字符串解码
try {
const binary = atob(audioData);
pcmData = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
pcmData[i] = binary.charCodeAt(i);
}
log("已从Base64字符串转换为Uint8Array");
} catch (e) {
log("Base64转换失败: " + e);
return;
}
} else {
return;
}
}
// 检查数据是否为空或过小
if (!pcmData || pcmData.length < 2) {
log("音频数据无效或过小");
return;
}
log(`处理音频数据: 长度=${pcmData.length} 字节`);
// 确保数据长度是偶数16位样本需要2个字节
const validLength = Math.floor(pcmData.length / 2) * 2;
if (validLength < pcmData.length) {
log(`调整音频数据长度从 ${pcmData.length} 到 ${validLength} 字节`);
pcmData = pcmData.slice(0, validLength);
}
// 获取有效的DataView
let dataView;
try {
dataView = new DataView(pcmData.buffer, pcmData.byteOffset, pcmData.byteLength);
} catch (e) {
log("创建DataView失败: " + e);
// 尝试创建新的ArrayBuffer
try {
const newBuffer = new ArrayBuffer(pcmData.length);
const newBufferView = new Uint8Array(newBuffer);
newBufferView.set(pcmData);
dataView = new DataView(newBuffer);
log("通过创建新缓冲区成功获取DataView");
} catch (e2) {
log("创建替代DataView也失败: " + e2);
return;
}
}
// 将PCM数据16位整数转换为32位浮点数组
const floatData = new Float32Array(pcmData.length / 2);
// 转换16位PCM到32位浮点使用try-catch保护读取操作
try {
for (let i = 0; i < floatData.length; i++) {
// 确保我们不会读取超出范围的数据
if ((i * 2) + 1 < pcmData.length) {
// 读取16位整数小端序
const int16 = dataView.getInt16(i * 2, true);
// 转换为-1.0到1.0的浮点数
floatData[i] = int16 / 32768.0;
} else {
// 如果到达数据末尾使用0填充
floatData[i] = 0;
}
}
} catch (e) {
log("转换音频数据失败: " + e);
return;
}
// 创建一个包含音频数据的AudioBuffer
const sampleRate = 16000; // 采样率固定为16kHz
const buffer = audioContext.createBuffer(1, floatData.length, sampleRate);
// 将浮点数据复制到AudioBuffer的第一个通道
try {
const channel = buffer.getChannelData(0);
channel.set(floatData);
} catch (e) {
log("设置音频通道数据失败: " + e);
return;
}
// 创建音频源并连接到音频输出
const source = audioContext.createBufferSource();
source.buffer = buffer;
// 连接到增益节点而不是直接连接到输出
source.connect(audioGainNode);
// 确保音量设置被应用
if (audioGainNode) {
audioGainNode.gain.value = currentVolume;
}
// 播放
source.start(0);
log(`实时音频播放中...音量: ${currentVolume * 100}%`);
} catch (e) {
log("处理实时音频失败: " + e);
}
}
</script>
}