1857 lines
85 KiB
Plaintext
1857 lines
85 KiB
Plaintext
@page
|
||
@model ShengShengBuXi.Pages.MonitorModel
|
||
@{
|
||
ViewData["Title"] = "清竹园-中控页面";
|
||
}
|
||
|
||
<!-- 添加Bootstrap Icons库的引用 -->
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
|
||
|
||
<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 class="btn-group ms-3" role="group">
|
||
控评开关:
|
||
<input type="radio" class="btn-check" name="screenControl" id="screenControlAuto" value="0"
|
||
checked>
|
||
<label class="btn btn-outline-primary" for="screenControlAuto">自动</label>
|
||
|
||
<input type="radio" class="btn-check" name="screenControl" id="screenControlManual"
|
||
value="1">
|
||
<label class="btn btn-outline-primary" for="screenControlManual">手动</label>
|
||
</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">
|
||
文本编辑 <span style="color:#6c757d;font-size:12px;">(最多输入100个文字)</span>
|
||
</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<textarea id="text-input" class="form-control h-100" placeholder="请输入要显示的文本..."
|
||
maxlength="100"></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>
|
||
|
||
<!-- 来电确认对话框 -->
|
||
<div class="modal fade" id="callConfirmDialog" tabindex="-1" aria-labelledby="callConfirmDialogLabel"
|
||
aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
||
<div class="modal-dialog modal-dialog-centered">
|
||
<div class="modal-content">
|
||
<div class="modal-header bg-primary text-white">
|
||
<h5 class="modal-title" id="callConfirmDialogLabel">
|
||
<i class="bi bi-telephone-inbound"></i> 来电提醒
|
||
</h5>
|
||
</div>
|
||
<div class="modal-body text-center py-4">
|
||
<div class="mb-3">
|
||
<i class="bi bi-telephone-fill text-primary" style="font-size: 3rem;"></i>
|
||
</div>
|
||
<h5>检测到新的通话</h5>
|
||
<p class="text-muted">请点击接听开始播放音频</p>
|
||
</div>
|
||
<div class="modal-footer justify-content-center">
|
||
<button type="button" class="btn btn-primary" onclick="confirmCall()">
|
||
<i class="bi bi-telephone"></i> 接听
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card mb-3">
|
||
<div class="card-header">
|
||
<h5>音频控制</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="form-check form-switch mb-3">
|
||
<input class="form-check-input" type="checkbox" id="audioStreamToggle">
|
||
<label class="form-check-label" for="audioStreamToggle">音频流接收</label>
|
||
</div>
|
||
|
||
<!-- 音量控制 -->
|
||
<div class="mb-3">
|
||
<label for="volumeSlider" class="form-label">音量控制 <span id="volumeDisplay">100%</span></label>
|
||
<div class="d-flex align-items-center">
|
||
<i class="bi bi-volume-down me-2"></i>
|
||
<input type="range" class="form-range flex-grow-1" id="volumeSlider" min="0" max="100" value="100">
|
||
<i class="bi bi-volume-up ms-2"></i>
|
||
</div>
|
||
<small class="text-muted">音量已增强3倍,可按需调整</small>
|
||
</div>
|
||
|
||
<div class="mt-3" id="callStatus">
|
||
<div class="alert alert-secondary">
|
||
未检测到通话
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
@section Scripts {
|
||
<script src="~/lib/microsoft-signalr/signalr.min.js"></script>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
|
||
<script src='https://web.sdk.qcloud.com/trtc/webrtc/v5/dist/trtc.js'></script>
|
||
<script src="~//js/lib-generate-test-usersig.min.js"></script>
|
||
<script src="~//js/generateTestUserSig.js"></script>
|
||
<script>
|
||
let connection = null;
|
||
let refreshDisplayInterval = null;
|
||
let callInProgress = false;
|
||
let isActiveShow = false;
|
||
// 当前播放的音频元素和按钮引用
|
||
let currentAudio = null;
|
||
let currentPlayButton = null;
|
||
|
||
// 控屏开关状态
|
||
let isManualScreenControl = false;
|
||
|
||
// 实时音频相关变量
|
||
let audioContext = null;
|
||
let audioStreamSource = null;
|
||
let isAudioStreamEnabled = false;
|
||
let audioGainNode = null;
|
||
let currentVolume = 1.0; // 默认音量
|
||
let volumeBoost = 3.0; // 音量增益倍数,提高接收到的音频音量
|
||
|
||
const sdkAppId = 1600079538;
|
||
const sdkSecretKey = "df2427757c0ec29ae8ca45611ddb70381144d55338e5ac73c2da27a9c32729f6";
|
||
let userId = "监听者:" + Math.random().toString(36).substring(2, 15);
|
||
let roomId = 8888;
|
||
|
||
let trtc = TRTC.create();
|
||
|
||
async function enterRoom() {
|
||
try {
|
||
trtc = TRTC.create();
|
||
// 生成用户签名
|
||
const { userSig } = genTestUserSig({ sdkAppId, userId, sdkSecretKey });
|
||
// 进入房间
|
||
await trtc.enterRoom({ sdkAppId, userId, userSig, roomId: roomId });
|
||
} catch (error) {
|
||
|
||
}
|
||
}
|
||
|
||
|
||
async function leaveRoom() {
|
||
try {
|
||
await trtc.exitRoom();
|
||
trtc.destroy();
|
||
// 更新状态指示器为红色(已断开)
|
||
} catch (error) {
|
||
|
||
}
|
||
}
|
||
|
||
// 调试日志
|
||
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();
|
||
|
||
// 获取当前控屏设置
|
||
getServerScreenControlSetting();
|
||
|
||
// 设置定时刷新显示列表
|
||
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("ReceiveSpeechToTextResult", (result) => {
|
||
log(`接收到实时语音识别结果: ${result.substring(0, 30)}${result.length > 30 ? '...' : ''}`);
|
||
|
||
// 使用防抖动技术显示实时识别结果,避免频繁更新导致页面卡顿
|
||
if (window.realtimeTextDebounceTimer) {
|
||
clearTimeout(window.realtimeTextDebounceTimer);
|
||
}
|
||
window.realtimeTextDebounceTimer = setTimeout(() => {
|
||
showRealtimeTextResult(result);
|
||
}, 300); // 300毫秒的防抖动延迟
|
||
});
|
||
|
||
// 接收最终语音识别结果
|
||
connection.on("ReceiveSpeechToEndTextResult", (result) => {
|
||
log(`接收到最终语音识别结果: ${result.substring(0, 30)}${result.length > 30 ? '...' : ''}`);
|
||
|
||
// 清除任何正在进行的实时文本更新定时器
|
||
if (window.realtimeTextDebounceTimer) {
|
||
clearTimeout(window.realtimeTextDebounceTimer);
|
||
}
|
||
|
||
// 显示最终识别结果
|
||
showFinalTextResult(result);
|
||
|
||
// 同时添加到监控列表的顶部(如果不存在于列表中)
|
||
addToMonitorList(result);
|
||
});
|
||
|
||
// 显示模式更新消息
|
||
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 (!isActiveShow) {
|
||
updateCallStatus(true);
|
||
return;
|
||
}
|
||
|
||
});
|
||
|
||
// 音频流设置更新消息
|
||
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");
|
||
}
|
||
});
|
||
|
||
// 控屏设置更新消息
|
||
connection.on("ScreenControlSettingChanged", (isManual) => {
|
||
log(`服务器控屏设置已更改为: ${isManual ? "手动" : "自动"}`);
|
||
updateScreenControlUI(isManual);
|
||
});
|
||
|
||
// 控屏设置更新结果
|
||
connection.on("ScreenControlSettingUpdated", (success, message) => {
|
||
if (success) {
|
||
log(`控屏设置更新成功: ${message}`);
|
||
showMessage(message, "success");
|
||
} else {
|
||
log(`控屏设置更新失败: ${message}`);
|
||
showMessage(message, "danger");
|
||
}
|
||
});
|
||
}
|
||
|
||
// 更新通话状态
|
||
function updateCallStatus(isActive) {
|
||
isActiveShow = isActive;
|
||
if (isActive) {
|
||
// 当检测到新通话时,先显示确认对话框
|
||
const confirmDialog = new bootstrap.Modal(document.getElementById('callConfirmDialog'));
|
||
confirmDialog.show();
|
||
enterRoom();
|
||
return; // 等待用户确认后再继续处理
|
||
} else {
|
||
const indicator = document.getElementById("status-indicator");
|
||
const statusText = document.getElementById("status-text");
|
||
indicator.style.backgroundColor = "red";
|
||
statusText.textContent = "未检测到通话";
|
||
|
||
// 如果确认对话框还在显示(用户未点击接听),则自动关闭
|
||
const confirmDialog = bootstrap.Modal.getInstance(document.getElementById('callConfirmDialog'));
|
||
if (confirmDialog) {
|
||
confirmDialog.hide();
|
||
exitRoom();
|
||
log("通话已结束,自动关闭确认对话框");
|
||
}
|
||
}
|
||
|
||
// 有新通话时刷新监控列表
|
||
loadMonitorTextList();
|
||
}
|
||
|
||
// 用户确认接听通话
|
||
function confirmCall() {
|
||
const indicator = document.getElementById("status-indicator");
|
||
const statusText = document.getElementById("status-text");
|
||
indicator.style.backgroundColor = "green";
|
||
statusText.textContent = "正在通话中";
|
||
callInProgress = true;
|
||
|
||
// 初始化音频上下文(如果需要且启用了音频流)
|
||
if (isAudioStreamEnabled) {
|
||
// 如果存在音频上下文,则先尝试关闭,然后重新创建
|
||
if (audioContext) {
|
||
log("重置音频上下文以确保新的通话正常播放");
|
||
try {
|
||
// 释放旧的增益节点
|
||
if (audioGainNode) {
|
||
audioGainNode.disconnect();
|
||
audioGainNode = null;
|
||
}
|
||
// 关闭旧的音频上下文
|
||
audioContext.close().catch(e => log("关闭音频上下文失败: " + e));
|
||
audioContext = null;
|
||
} catch (e) {
|
||
log("重置音频上下文失败: " + e);
|
||
}
|
||
}
|
||
|
||
// 创建新的音频上下文
|
||
initAudioContext();
|
||
}
|
||
|
||
// 关闭确认对话框
|
||
const confirmDialog = bootstrap.Modal.getInstance(document.getElementById('callConfirmDialog'));
|
||
if (confirmDialog) {
|
||
confirmDialog.hide();
|
||
}
|
||
|
||
// 刷新监控列表
|
||
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");
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// 设置音频流开关监听器
|
||
function setupAudioStreamingListeners() {
|
||
const audioStreamingButtons = document.querySelectorAll('input[name="audioStreaming"]');
|
||
audioStreamingButtons.forEach(radio => {
|
||
radio.addEventListener('change', function () {
|
||
const enabled = parseInt(this.value) === 1;
|
||
isAudioStreamEnabled = enabled;
|
||
log(`音频流已切换为: ${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");
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// 设置控屏开关监听器
|
||
function setupScreenControlListeners() {
|
||
console.log('aaaa')
|
||
const screenControlButtons = document.querySelectorAll('input[name="screenControl"]');
|
||
screenControlButtons.forEach(radio => {
|
||
radio.addEventListener('change', function () {
|
||
const isManual = parseInt(this.value) === 1;
|
||
isManualScreenControl = isManual;
|
||
log(`控屏模式已切换为: ${isManual ? "手动" : "自动"}`);
|
||
|
||
// 调用服务器端方法更新控屏设置
|
||
if (connection && connection.state === signalR.HubConnectionState.Connected) {
|
||
connection.invoke("UpdateScreenControlSetting", isManual)
|
||
.then(() => {
|
||
showMessage(`已切换为${isManual ? "手动" : "自动"}控屏模式`, "success");
|
||
})
|
||
.catch(err => {
|
||
log(`更新控屏设置失败: ${err}`);
|
||
showMessage("切换控屏模式失败", "danger");
|
||
|
||
// 恢复原来的选择
|
||
const currentValue = isManual ? 0 : 1;
|
||
document.getElementById(`screenControl${currentValue === 0 ? 'Auto' : 'Manual'}`).checked = true;
|
||
});
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// 设置音量控制监听器
|
||
function setupVolumeControlListener() {
|
||
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;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// 初始化页面
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
|
||
// 初始化音频上下文
|
||
initAudioContext();
|
||
|
||
// 初始化SignalR连接
|
||
initSignalR();
|
||
|
||
// 设置各种事件监听器
|
||
// setupEventListeners();
|
||
setupDisplayModeListeners();
|
||
setupAudioStreamingListeners();
|
||
setupScreenControlListeners();
|
||
|
||
// 初始化音量控制
|
||
setupVolumeControl();
|
||
|
||
// 注册为监控端
|
||
registerAsMonitor();
|
||
|
||
// 初始化工具提示
|
||
setTimeout(initTooltips, 1000);
|
||
|
||
// 默认开启音频传输
|
||
document.getElementById("audioStreaming1").checked = true;
|
||
isAudioStreamEnabled = true;
|
||
|
||
});
|
||
|
||
// 页面卸载前清理资源
|
||
window.addEventListener('beforeunload', function () {
|
||
if (refreshDisplayInterval) {
|
||
clearInterval(refreshDisplayInterval);
|
||
}
|
||
|
||
// 关闭音频上下文
|
||
if (audioContext) {
|
||
audioContext.close().catch(e => console.log("关闭音频上下文失败: " + e));
|
||
audioContext = null;
|
||
}
|
||
});
|
||
|
||
// 更新显示模式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 volumeSlider = document.getElementById("volumeSlider");
|
||
if (volumeSlider) {
|
||
// 初始化滑块值为当前音量 * 音量增益系数(转换为百分比)
|
||
volumeSlider.value = Math.floor(currentVolume * 100);
|
||
|
||
// 更新显示
|
||
const volumeDisplay = document.getElementById("volumeDisplay");
|
||
if (volumeDisplay) {
|
||
volumeDisplay.textContent = `${Math.floor(currentVolume * 100)}%`;
|
||
}
|
||
|
||
// 监听音量变化
|
||
volumeSlider.addEventListener("input", function () {
|
||
const newVolume = parseFloat(this.value) / 100;
|
||
currentVolume = newVolume;
|
||
|
||
// 应用音量设置(包含增益)
|
||
if (audioGainNode) {
|
||
audioGainNode.gain.value = newVolume * volumeBoost;
|
||
}
|
||
|
||
// 更新显示
|
||
if (volumeDisplay) {
|
||
volumeDisplay.textContent = `${Math.floor(newVolume * 100)}%`;
|
||
}
|
||
|
||
log(`音量已调整: ${Math.floor(newVolume * 100)}%, 实际增益: ${(newVolume * volumeBoost).toFixed(1)}`);
|
||
});
|
||
}
|
||
}
|
||
|
||
// 初始化音频上下文
|
||
function initAudioContext() {
|
||
try {
|
||
// 关闭现有的音频上下文
|
||
if (audioContext) {
|
||
try {
|
||
if (audioGainNode) {
|
||
audioGainNode.disconnect();
|
||
audioGainNode = null;
|
||
}
|
||
audioContext.close().catch(e => log("关闭音频上下文失败: " + e));
|
||
audioContext = null;
|
||
} catch (e) {
|
||
log("重置音频上下文失败: " + e);
|
||
}
|
||
}
|
||
|
||
// 创建新的音频上下文
|
||
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||
audioContext = new AudioContext({
|
||
latencyHint: 'interactive' // 低延迟设置
|
||
});
|
||
|
||
// 创建增益节点并设置更高的音量
|
||
audioGainNode = audioContext.createGain();
|
||
audioGainNode.gain.value = currentVolume * volumeBoost; // 应用音量增益
|
||
audioGainNode.connect(audioContext.destination);
|
||
|
||
log(`音频上下文已初始化: 采样率=${audioContext.sampleRate}Hz, 状态=${audioContext.state}, 音量增益=${volumeBoost}倍`);
|
||
|
||
// 恢复音频上下文
|
||
if (audioContext.state === 'suspended') {
|
||
const resumeAudio = function () {
|
||
if (audioContext && audioContext.state === 'suspended') {
|
||
audioContext.resume().then(() => {
|
||
log("音频上下文已激活");
|
||
}).catch(err => {
|
||
log("激活音频上下文失败: " + err);
|
||
});
|
||
}
|
||
};
|
||
|
||
// 设置单次事件监听
|
||
document.addEventListener('click', resumeAudio, { once: true });
|
||
document.addEventListener('touchstart', resumeAudio, { once: true });
|
||
document.addEventListener('keydown', resumeAudio, { once: true });
|
||
|
||
// 尝试立即恢复
|
||
resumeAudio();
|
||
}
|
||
} catch (e) {
|
||
log("初始化音频上下文失败: " + e);
|
||
showMessage("无法初始化音频播放: " + e, "danger");
|
||
}
|
||
}
|
||
|
||
// 播放实时音频 - 适应新格式
|
||
function playRealTimeAudio(audioPacket) {
|
||
if (!audioContext || !isAudioStreamEnabled || !callInProgress) return;
|
||
|
||
try {
|
||
// 解析音频元数据和数据
|
||
const { format, sampleRate, channels, data } = audioPacket;
|
||
|
||
// 确保格式正确
|
||
if (!format || !data) {
|
||
log("音频格式或数据无效");
|
||
return;
|
||
}
|
||
|
||
log(`接收到音频数据: 格式=${format}, 采样率=${sampleRate}, 声道=${channels}, 数据长度=${Array.isArray(data) ? data.length : '未知'}`);
|
||
|
||
// 根据不同的音频格式处理
|
||
if (format === "WAV") {
|
||
// 处理WAV格式数据 - 异步解码
|
||
processWavData(data, sampleRate, channels)
|
||
.then(audioBuffer => {
|
||
if (audioBuffer) {
|
||
playAudioBuffer(audioBuffer);
|
||
} else {
|
||
log("无法创建WAV音频缓冲区");
|
||
}
|
||
})
|
||
.catch(err => {
|
||
log("WAV处理错误: " + err);
|
||
});
|
||
} else if (format === "16bit_PCM") {
|
||
// 处理PCM格式数据 - 同步处理
|
||
const audioBuffer = processPcmData(data, sampleRate, channels);
|
||
if (audioBuffer) {
|
||
playAudioBuffer(audioBuffer);
|
||
} else {
|
||
log("无法创建PCM音频缓冲区");
|
||
}
|
||
} else {
|
||
log("不支持的音频格式: " + format);
|
||
}
|
||
} catch (e) {
|
||
log("处理实时音频失败: " + e);
|
||
}
|
||
}
|
||
|
||
// 播放音频缓冲区
|
||
function playAudioBuffer(audioBuffer) {
|
||
// 确保音频上下文活跃
|
||
if (audioContext.state === 'suspended') {
|
||
try {
|
||
audioContext.resume();
|
||
} catch (e) {
|
||
log("恢复音频上下文失败: " + e);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 创建音频源并连接
|
||
const source = audioContext.createBufferSource();
|
||
source.buffer = audioBuffer;
|
||
|
||
// 应用音量控制
|
||
source.connect(audioGainNode);
|
||
if (audioGainNode) {
|
||
audioGainNode.gain.value = currentVolume * volumeBoost;
|
||
}
|
||
|
||
// 立即播放
|
||
source.start(0);
|
||
log("开始播放音频");
|
||
}
|
||
|
||
// 处理WAV格式的数据 - 返回Promise
|
||
function processWavData(data, sampleRate, channels) {
|
||
return new Promise((resolve, reject) => {
|
||
try {
|
||
// 确保音频上下文存在
|
||
if (!audioContext || audioContext.state === 'closed') {
|
||
initAudioContext();
|
||
if (!audioContext) {
|
||
reject(new Error("无法初始化音频上下文"));
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 将数据转换为ArrayBuffer
|
||
let arrayBuffer;
|
||
if (data instanceof Uint8Array) {
|
||
arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
||
} else if (Array.isArray(data)) {
|
||
// 转换数组为Uint8Array
|
||
const uint8Array = new Uint8Array(data);
|
||
arrayBuffer = uint8Array.buffer;
|
||
} else if (typeof data === 'string') {
|
||
// 处理Base64编码
|
||
try {
|
||
const base64Str = data.trim().replace(/^data:[^;]+;base64,/, '');
|
||
const binary = atob(base64Str);
|
||
const uint8Array = new Uint8Array(binary.length);
|
||
for (let i = 0; i < binary.length; i++) {
|
||
uint8Array[i] = binary.charCodeAt(i);
|
||
}
|
||
arrayBuffer = uint8Array.buffer;
|
||
} catch (e) {
|
||
log("WAV数据Base64解码失败: " + e);
|
||
reject(e);
|
||
return;
|
||
}
|
||
} else if (data.buffer) {
|
||
arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
||
} else {
|
||
const error = new Error("无法处理的WAV数据类型");
|
||
log(error.message);
|
||
reject(error);
|
||
return;
|
||
}
|
||
|
||
// 使用Web Audio API解码音频
|
||
audioContext.decodeAudioData(
|
||
arrayBuffer,
|
||
(buffer) => {
|
||
log("WAV数据解码成功, 时长: " + buffer.duration.toFixed(2) + "秒");
|
||
resolve(buffer);
|
||
},
|
||
(err) => {
|
||
log("解码WAV数据失败: " + err);
|
||
reject(err);
|
||
}
|
||
);
|
||
} catch (e) {
|
||
log("处理WAV数据失败: " + e);
|
||
reject(e);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 处理PCM格式的数据
|
||
function processPcmData(data, sampleRate, channels) {
|
||
try {
|
||
// 确保音频上下文存在
|
||
if (!audioContext || audioContext.state === 'closed') {
|
||
initAudioContext();
|
||
if (!audioContext) return null;
|
||
}
|
||
|
||
// 转换数据为适合的格式
|
||
let pcmData;
|
||
if (data instanceof Uint8Array) {
|
||
pcmData = data;
|
||
} else if (Array.isArray(data)) {
|
||
pcmData = new Uint8Array(data);
|
||
} else if (typeof data === 'object' && data.buffer) {
|
||
pcmData = new Uint8Array(data.buffer);
|
||
} else if (typeof data === 'string') {
|
||
try {
|
||
// 处理Base64编码
|
||
const base64Str = data.trim().replace(/^data:[^;]+;base64,/, '');
|
||
const binary = atob(base64Str);
|
||
pcmData = new Uint8Array(binary.length);
|
||
for (let i = 0; i < binary.length; i++) {
|
||
pcmData[i] = binary.charCodeAt(i);
|
||
}
|
||
} catch (e) {
|
||
log("PCM数据Base64解码失败: " + e);
|
||
return null;
|
||
}
|
||
} else {
|
||
log("不支持的PCM数据类型");
|
||
return null;
|
||
}
|
||
|
||
// 确保有效的数据
|
||
if (!pcmData || pcmData.length < 2) {
|
||
log("PCM数据无效或太短");
|
||
return null;
|
||
}
|
||
|
||
// 确保数据长度是偶数(16位PCM)
|
||
const validLength = Math.floor(pcmData.length / 2) * 2;
|
||
if (validLength < pcmData.length) {
|
||
pcmData = pcmData.slice(0, validLength);
|
||
}
|
||
|
||
try {
|
||
// 从Uint8Array创建Int16Array视图
|
||
let int16Data;
|
||
|
||
try {
|
||
// 创建DataView以便正确解析16位整数
|
||
const dataView = new DataView(pcmData.buffer, pcmData.byteOffset, pcmData.byteLength);
|
||
int16Data = new Int16Array(pcmData.length / 2);
|
||
|
||
// 从小端字节序读取16位整数
|
||
for (let i = 0; i < pcmData.length; i += 2) {
|
||
if (i + 1 < pcmData.length) {
|
||
int16Data[i / 2] = dataView.getInt16(i, true); // true表示小端字节序
|
||
}
|
||
}
|
||
} catch (e) {
|
||
log("创建Int16Array失败: " + e);
|
||
|
||
// 备用方法
|
||
const newBuffer = new ArrayBuffer(pcmData.length);
|
||
const newView = new Uint8Array(newBuffer);
|
||
newView.set(pcmData);
|
||
const dataView = new DataView(newBuffer);
|
||
|
||
int16Data = new Int16Array(pcmData.length / 2);
|
||
for (let i = 0; i < pcmData.length; i += 2) {
|
||
if (i + 1 < pcmData.length) {
|
||
int16Data[i / 2] = dataView.getInt16(i, true);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 创建音频缓冲区,使用实际采样率
|
||
const audioSampleRate = sampleRate || audioContext.sampleRate;
|
||
const buffer = audioContext.createBuffer(channels || 1, int16Data.length, audioSampleRate);
|
||
|
||
// 将Int16数据转换为Float32数据并存入缓冲区
|
||
const channelData = buffer.getChannelData(0);
|
||
for (let i = 0; i < int16Data.length; i++) {
|
||
// 将Int16转换为-1.0到1.0的Float32
|
||
channelData[i] = Math.max(-1, Math.min(1, int16Data[i] / 32768.0));
|
||
}
|
||
|
||
return buffer;
|
||
} catch (e) {
|
||
log("PCM数据处理失败: " + e);
|
||
return null;
|
||
}
|
||
} catch (e) {
|
||
log("处理PCM数据失败: " + e);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// 显示实时语音识别结果
|
||
function showRealtimeTextResult(text) {
|
||
// 显示文本区域
|
||
const textDisplayArea = document.getElementById("text-display-area");
|
||
textDisplayArea.style.display = "block";
|
||
|
||
// 设置文本内容,添加一个标记表明这是实时结果
|
||
const displayedText = document.getElementById("displayed-text");
|
||
displayedText.innerHTML = `<span class="realtime-text" style="color: #0d6efd;">${text}</span> <small class="text-muted">(实时识别中...)</small>`;
|
||
|
||
log("显示实时语音识别结果");
|
||
}
|
||
|
||
// 显示最终语音识别结果
|
||
function showFinalTextResult(text) {
|
||
// 显示文本区域
|
||
const textDisplayArea = document.getElementById("text-display-area");
|
||
textDisplayArea.style.display = "block";
|
||
|
||
// 设置文本内容,并标记为最终结果
|
||
const displayedText = document.getElementById("displayed-text");
|
||
displayedText.innerHTML = `<span class="final-text">${text}</span>`;
|
||
|
||
// 同时将文本填入输入框,便于编辑
|
||
// document.getElementById("text-input").value = text;
|
||
|
||
log("显示最终语音识别结果");
|
||
}
|
||
|
||
// 添加文本到监控列表
|
||
function addToMonitorList(text) {
|
||
if (!text || text.trim() === "") return;
|
||
|
||
// 检查是否已经存在相同内容
|
||
const existingItems = document.querySelectorAll("#monitor-text-list .list-group-item");
|
||
for (let i = 0; i < existingItems.length; i++) {
|
||
if (existingItems[i].dataset.fullText === text) {
|
||
// 已存在相同内容,不需要添加
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 创建新的列表项
|
||
const container = document.getElementById("monitor-text-list");
|
||
const listItem = document.createElement("div");
|
||
listItem.className = "list-group-item";
|
||
// 使用临时ID,服务器端会分配真正的ID
|
||
listItem.dataset.id = "temp-" + Date.now();
|
||
listItem.dataset.fullText = text;
|
||
|
||
// 添加点击事件
|
||
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();
|
||
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')}`;
|
||
|
||
// 截取前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-danger btn-sm" onclick="deleteMonitorText('${listItem.dataset.id}')">
|
||
<i class="bi bi-trash"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div>${formattedDate} (本地)</div>
|
||
`;
|
||
|
||
// 添加到列表顶部
|
||
if (container.firstChild) {
|
||
container.insertBefore(listItem, container.firstChild);
|
||
} else {
|
||
container.appendChild(listItem);
|
||
}
|
||
|
||
log(`已添加文本到监控列表: ${shortText}...`);
|
||
}
|
||
|
||
// 更新控屏UI
|
||
function updateScreenControlUI(isManual) {
|
||
// 更新单选按钮状态
|
||
document.getElementById(isManual ? 'screenControlManual' : 'screenControlAuto').checked = true;
|
||
isManualScreenControl = isManual;
|
||
|
||
log(`UI控屏模式已更新为: ${isManual ? "手动" : "自动"}`);
|
||
}
|
||
|
||
// 获取服务器当前的控屏设置
|
||
function getServerScreenControlSetting() {
|
||
if (!connection || connection.state !== signalR.HubConnectionState.Connected) {
|
||
log("无法获取控屏设置:未连接");
|
||
return;
|
||
}
|
||
|
||
log("获取服务器当前控屏设置...");
|
||
|
||
connection.invoke("GetScreenControlSetting")
|
||
.then(isManual => {
|
||
log(`获取到服务器控屏设置: ${isManual ? "手动" : "自动"}`);
|
||
updateScreenControlUI(isManual);
|
||
})
|
||
.catch(err => {
|
||
log(`获取控屏设置失败: ${err}`);
|
||
// 默认设置为自动
|
||
updateScreenControlUI(false);
|
||
});
|
||
}
|
||
</script>
|
||
}
|