ShengShengBuXi/ShengShengBuXi/Pages/Monitor.cshtml
2025-03-29 18:10:29 +08:00

1857 lines
85 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"] = "清竹园-中控页面";
}
<!-- 添加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>
}