diff --git a/client/src/pages/meeting-detail.html b/client/src/pages/meeting-detail.html
index 3253d2d..421ccd4 100644
--- a/client/src/pages/meeting-detail.html
+++ b/client/src/pages/meeting-detail.html
@@ -139,6 +139,172 @@
border-color: #2196F3;
box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2);
}
+ /* Audio Device Settings Panel */
+ .audio-device-panel {
+ background: #f8f9fa;
+ border: 1px solid #dee2e6;
+ border-radius: 8px;
+ margin-bottom: 15px;
+ overflow: hidden;
+ }
+ .audio-device-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 15px;
+ background: #e9ecef;
+ cursor: pointer;
+ user-select: none;
+ }
+ .audio-device-header:hover {
+ background: #dee2e6;
+ }
+ .audio-device-header h3 {
+ margin: 0;
+ font-size: 14px;
+ font-weight: 600;
+ color: #495057;
+ }
+ .audio-device-toggle {
+ font-size: 12px;
+ color: #6c757d;
+ transition: transform 0.2s;
+ }
+ .audio-device-panel.collapsed .audio-device-toggle {
+ transform: rotate(-90deg);
+ }
+ .audio-device-panel.collapsed .audio-device-body {
+ display: none;
+ }
+ .audio-device-body {
+ padding: 15px;
+ }
+ .audio-device-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 12px;
+ }
+ .audio-device-row:last-child {
+ margin-bottom: 0;
+ }
+ .audio-device-label {
+ font-size: 13px;
+ color: #495057;
+ min-width: 70px;
+ }
+ .audio-device-select {
+ flex: 1;
+ padding: 8px 12px;
+ border: 1px solid #ced4da;
+ border-radius: 4px;
+ font-size: 13px;
+ background: white;
+ }
+ .audio-device-select:focus {
+ outline: none;
+ border-color: #2196F3;
+ box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2);
+ }
+ .audio-refresh-btn {
+ padding: 8px;
+ border: 1px solid #ced4da;
+ border-radius: 4px;
+ background: white;
+ cursor: pointer;
+ font-size: 14px;
+ }
+ .audio-refresh-btn:hover {
+ background: #e9ecef;
+ }
+ /* Volume Meter */
+ .volume-meter-container {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ }
+ .volume-meter {
+ flex: 1;
+ height: 20px;
+ background: #e9ecef;
+ border-radius: 10px;
+ overflow: hidden;
+ position: relative;
+ }
+ .volume-meter-fill {
+ height: 100%;
+ width: 0%;
+ background: linear-gradient(to right, #28a745, #ffc107, #dc3545);
+ transition: width 0.05s ease-out;
+ border-radius: 10px;
+ }
+ .volume-meter-text {
+ font-size: 12px;
+ color: #6c757d;
+ min-width: 40px;
+ text-align: right;
+ }
+ /* Test Controls */
+ .audio-test-controls {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ }
+ .audio-test-btn {
+ padding: 8px 16px;
+ border: none;
+ border-radius: 4px;
+ font-size: 13px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ transition: all 0.2s;
+ }
+ .audio-test-btn.record {
+ background: #dc3545;
+ color: white;
+ }
+ .audio-test-btn.record:hover:not(:disabled) {
+ background: #c82333;
+ }
+ .audio-test-btn.record.recording {
+ background: #6c757d;
+ }
+ .audio-test-btn.play {
+ background: #28a745;
+ color: white;
+ }
+ .audio-test-btn.play:hover:not(:disabled) {
+ background: #218838;
+ }
+ .audio-test-btn.play.playing {
+ background: #6c757d;
+ }
+ .audio-test-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+ .audio-status {
+ font-size: 12px;
+ color: #6c757d;
+ margin-left: auto;
+ }
+ .audio-status.success {
+ color: #28a745;
+ }
+ .audio-status.error {
+ color: #dc3545;
+ }
+ .audio-status.recording {
+ color: #dc3545;
+ }
+ .no-input-hint {
+ font-size: 11px;
+ color: #dc3545;
+ margin-top: 4px;
+ }
@@ -164,6 +330,47 @@
+
+
+
+
+
+
+ 麥克風:
+
+
+
+
+
+
+
+
收音測試:
+
+
+
+ 準備就緒
+
+
+
+
+
@@ -284,6 +491,478 @@
const uploadProgressFill = document.getElementById('upload-progress-fill');
const whisperStatusEl = document.getElementById('whisper-status');
+ // Audio Device Settings Elements
+ const audioDevicePanel = document.getElementById('audio-device-panel');
+ const audioDeviceHeader = document.getElementById('audio-device-header');
+ const audioDeviceSelect = document.getElementById('audio-device-select');
+ const audioRefreshBtn = document.getElementById('audio-refresh-btn');
+ const volumeMeterFill = document.getElementById('volume-meter-fill');
+ const volumeMeterText = document.getElementById('volume-meter-text');
+ const testRecordBtn = document.getElementById('test-record-btn');
+ const testPlayBtn = document.getElementById('test-play-btn');
+ const audioStatusEl = document.getElementById('audio-status');
+
+ // Audio Device State
+ const audioDeviceState = {
+ availableDevices: [],
+ selectedDeviceId: null,
+ isMonitoring: false,
+ monitoringStream: null,
+ monitoringContext: null,
+ monitoringAnalyser: null,
+ animationFrameId: null,
+ testRecordingBlob: null,
+ testState: 'idle', // 'idle' | 'recording' | 'playing'
+ testMediaRecorder: null,
+ testAudioElement: null,
+ testCountdown: 0
+ };
+
+ // ========================================
+ // Audio Device Management Functions
+ // ========================================
+
+ // Check if deviceId is an alias (not a real device ID)
+ function isAliasDeviceId(id) {
+ return id === 'default' || id === 'communications' || !id;
+ }
+
+ // Enumerate audio devices and populate dropdown
+ async function enumerateAudioDevices() {
+ try {
+ const devices = await navigator.mediaDevices.enumerateDevices();
+ const audioInputs = devices.filter(d => d.kind === 'audioinput');
+
+ // Filter out virtual devices like Stereo Mix
+ const realDevices = audioInputs.filter(d =>
+ !d.label.includes('立體聲混音') &&
+ !d.label.toLowerCase().includes('stereo mix')
+ );
+
+ audioDeviceState.availableDevices = realDevices;
+
+ // Populate dropdown
+ audioDeviceSelect.innerHTML = '';
+
+ if (realDevices.length === 0) {
+ audioDeviceSelect.innerHTML = '';
+ setAudioStatus('未偵測到麥克風', 'error');
+ return;
+ }
+
+ realDevices.forEach((device, index) => {
+ const option = document.createElement('option');
+ option.value = device.deviceId;
+
+ // Create friendly label
+ let label = device.label || `麥克風 ${index + 1}`;
+ if (device.deviceId === 'default') {
+ label = `🔹 ${label} (系統預設)`;
+ } else if (device.deviceId === 'communications') {
+ label = `📞 ${label} (通訊裝置)`;
+ }
+ option.textContent = label;
+ audioDeviceSelect.appendChild(option);
+ });
+
+ // Try to restore saved preference
+ const savedDeviceId = localStorage.getItem('audioDevice.selectedId');
+ const savedDevice = realDevices.find(d => d.deviceId === savedDeviceId);
+
+ if (savedDevice) {
+ audioDeviceSelect.value = savedDeviceId;
+ audioDeviceState.selectedDeviceId = savedDeviceId;
+ } else {
+ // Prefer non-alias device
+ const preferredDevice = realDevices.find(d => !isAliasDeviceId(d.deviceId)) || realDevices[0];
+ if (preferredDevice) {
+ audioDeviceSelect.value = preferredDevice.deviceId;
+ audioDeviceState.selectedDeviceId = preferredDevice.deviceId;
+ }
+ }
+
+ console.log('Audio devices enumerated:', realDevices.length, realDevices);
+ setAudioStatus('準備就緒', 'success');
+
+ // Start volume monitoring with selected device
+ await startVolumeMonitoring();
+
+ } catch (error) {
+ console.error('Failed to enumerate audio devices:', error);
+ audioDeviceSelect.innerHTML = '';
+ setAudioStatus('無法存取麥克風: ' + error.message, 'error');
+ }
+ }
+
+ // Select audio device
+ async function selectAudioDevice(deviceId) {
+ audioDeviceState.selectedDeviceId = deviceId;
+
+ // Save preference
+ if (deviceId) {
+ localStorage.setItem('audioDevice.selectedId', deviceId);
+ const device = audioDeviceState.availableDevices.find(d => d.deviceId === deviceId);
+ if (device) {
+ localStorage.setItem('audioDevice.lastUsedLabel', device.label);
+ }
+ }
+
+ // Restart volume monitoring with new device
+ await startVolumeMonitoring();
+
+ console.log('Selected audio device:', deviceId);
+ }
+
+ // Start volume monitoring
+ async function startVolumeMonitoring() {
+ // Stop existing monitoring
+ stopVolumeMonitoring();
+
+ const deviceId = audioDeviceState.selectedDeviceId;
+ if (!deviceId && audioDeviceState.availableDevices.length === 0) {
+ return;
+ }
+
+ try {
+ // Get audio stream
+ let constraints;
+ if (isAliasDeviceId(deviceId)) {
+ constraints = { audio: true };
+ } else {
+ constraints = { audio: { deviceId: { exact: deviceId } } };
+ }
+
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
+ audioDeviceState.monitoringStream = stream;
+
+ // Create audio context and analyser
+ const audioContext = new (window.AudioContext || window.webkitAudioContext)();
+ const analyser = audioContext.createAnalyser();
+ analyser.fftSize = 256;
+ analyser.smoothingTimeConstant = 0.3;
+
+ const source = audioContext.createMediaStreamSource(stream);
+ source.connect(analyser);
+
+ audioDeviceState.monitoringContext = audioContext;
+ audioDeviceState.monitoringAnalyser = analyser;
+ audioDeviceState.isMonitoring = true;
+
+ // Start animation loop for volume meter
+ updateVolumeMeter();
+
+ setAudioStatus('正在監聽...', 'success');
+
+ } catch (error) {
+ console.error('Failed to start volume monitoring:', error);
+
+ if (error.name === 'NotAllowedError') {
+ setAudioStatus('麥克風權限被拒絕', 'error');
+ } else if (error.name === 'NotReadableError') {
+ setAudioStatus('麥克風被其他應用程式佔用', 'error');
+ } else {
+ setAudioStatus('無法存取麥克風', 'error');
+ }
+ }
+ }
+
+ // Stop volume monitoring
+ function stopVolumeMonitoring() {
+ if (audioDeviceState.animationFrameId) {
+ cancelAnimationFrame(audioDeviceState.animationFrameId);
+ audioDeviceState.animationFrameId = null;
+ }
+
+ if (audioDeviceState.monitoringStream) {
+ audioDeviceState.monitoringStream.getTracks().forEach(track => track.stop());
+ audioDeviceState.monitoringStream = null;
+ }
+
+ if (audioDeviceState.monitoringContext) {
+ audioDeviceState.monitoringContext.close();
+ audioDeviceState.monitoringContext = null;
+ }
+
+ audioDeviceState.monitoringAnalyser = null;
+ audioDeviceState.isMonitoring = false;
+
+ volumeMeterFill.style.width = '0%';
+ volumeMeterText.textContent = '0%';
+ }
+
+ // Update volume meter (animation loop)
+ function updateVolumeMeter() {
+ if (!audioDeviceState.isMonitoring || !audioDeviceState.monitoringAnalyser) {
+ return;
+ }
+
+ const analyser = audioDeviceState.monitoringAnalyser;
+ const dataArray = new Uint8Array(analyser.frequencyBinCount);
+ analyser.getByteFrequencyData(dataArray);
+
+ // Calculate RMS (root mean square) for more accurate volume
+ let sum = 0;
+ for (let i = 0; i < dataArray.length; i++) {
+ sum += dataArray[i] * dataArray[i];
+ }
+ const rms = Math.sqrt(sum / dataArray.length);
+
+ // Normalize to 0-100 range
+ const level = Math.min(100, Math.round((rms / 128) * 100));
+
+ volumeMeterFill.style.width = level + '%';
+ volumeMeterText.textContent = level + '%';
+
+ // Continue animation loop
+ audioDeviceState.animationFrameId = requestAnimationFrame(updateVolumeMeter);
+ }
+
+ // Set audio status message
+ function setAudioStatus(message, type = '') {
+ audioStatusEl.textContent = message;
+ audioStatusEl.className = 'audio-status';
+ if (type) {
+ audioStatusEl.classList.add(type);
+ }
+ }
+
+ // Start test recording (5 seconds)
+ async function startTestRecording() {
+ if (audioDeviceState.testState !== 'idle') return;
+
+ const deviceId = audioDeviceState.selectedDeviceId;
+
+ try {
+ // Get audio stream
+ let constraints;
+ if (isAliasDeviceId(deviceId)) {
+ constraints = { audio: true };
+ } else {
+ constraints = { audio: { deviceId: { exact: deviceId } } };
+ }
+
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
+
+ // Create MediaRecorder
+ const mediaRecorder = new MediaRecorder(stream, {
+ mimeType: 'audio/webm;codecs=opus'
+ });
+
+ const chunks = [];
+ mediaRecorder.ondataavailable = (e) => {
+ if (e.data.size > 0) {
+ chunks.push(e.data);
+ }
+ };
+
+ mediaRecorder.onstop = () => {
+ // Create blob from chunks
+ audioDeviceState.testRecordingBlob = new Blob(chunks, { type: 'audio/webm' });
+ stream.getTracks().forEach(track => track.stop());
+
+ // Update UI
+ audioDeviceState.testState = 'idle';
+ testRecordBtn.textContent = '🎤 測試錄音';
+ testRecordBtn.classList.remove('recording');
+ testRecordBtn.disabled = false;
+ testPlayBtn.disabled = false;
+ setAudioStatus('錄音完成,可播放測試', 'success');
+
+ // Restart volume monitoring
+ startVolumeMonitoring();
+ };
+
+ // Stop volume monitoring during test recording
+ stopVolumeMonitoring();
+
+ // Start recording
+ audioDeviceState.testMediaRecorder = mediaRecorder;
+ audioDeviceState.testState = 'recording';
+ audioDeviceState.testCountdown = 5;
+ mediaRecorder.start(100);
+
+ // Update UI
+ testRecordBtn.classList.add('recording');
+ testPlayBtn.disabled = true;
+ updateTestRecordingCountdown();
+
+ // Auto-stop after 5 seconds
+ setTimeout(() => {
+ if (audioDeviceState.testState === 'recording') {
+ stopTestRecording();
+ }
+ }, 5000);
+
+ } catch (error) {
+ console.error('Failed to start test recording:', error);
+
+ if (error.name === 'NotAllowedError') {
+ setAudioStatus('麥克風權限被拒絕', 'error');
+ } else if (error.name === 'NotReadableError') {
+ setAudioStatus('麥克風被其他應用程式佔用', 'error');
+ } else {
+ setAudioStatus('無法開始錄音: ' + error.message, 'error');
+ }
+
+ audioDeviceState.testState = 'idle';
+ }
+ }
+
+ // Update countdown during test recording
+ function updateTestRecordingCountdown() {
+ if (audioDeviceState.testState !== 'recording') return;
+
+ testRecordBtn.textContent = `⏹️ 錄音中... ${audioDeviceState.testCountdown}s`;
+ setAudioStatus(`錄音中... ${audioDeviceState.testCountdown} 秒`, 'recording');
+
+ if (audioDeviceState.testCountdown > 0) {
+ audioDeviceState.testCountdown--;
+ setTimeout(updateTestRecordingCountdown, 1000);
+ }
+ }
+
+ // Stop test recording
+ function stopTestRecording() {
+ if (audioDeviceState.testState !== 'recording') return;
+
+ if (audioDeviceState.testMediaRecorder && audioDeviceState.testMediaRecorder.state !== 'inactive') {
+ audioDeviceState.testMediaRecorder.stop();
+ }
+ }
+
+ // Play test recording
+ function playTestRecording() {
+ if (!audioDeviceState.testRecordingBlob || audioDeviceState.testState !== 'idle') return;
+
+ // Create audio element
+ const blobUrl = URL.createObjectURL(audioDeviceState.testRecordingBlob);
+ const audio = new Audio(blobUrl);
+ audioDeviceState.testAudioElement = audio;
+
+ audio.onplay = () => {
+ audioDeviceState.testState = 'playing';
+ testPlayBtn.textContent = '⏹️ 停止播放';
+ testPlayBtn.classList.add('playing');
+ testRecordBtn.disabled = true;
+ setAudioStatus('播放中...', 'success');
+ };
+
+ audio.onended = () => {
+ audioDeviceState.testState = 'idle';
+ testPlayBtn.textContent = '▶️ 播放測試';
+ testPlayBtn.classList.remove('playing');
+ testRecordBtn.disabled = false;
+ setAudioStatus('播放完成', 'success');
+ URL.revokeObjectURL(blobUrl);
+ audioDeviceState.testAudioElement = null;
+ };
+
+ audio.onerror = () => {
+ audioDeviceState.testState = 'idle';
+ testPlayBtn.textContent = '▶️ 播放測試';
+ testPlayBtn.classList.remove('playing');
+ testRecordBtn.disabled = false;
+ setAudioStatus('播放失敗', 'error');
+ URL.revokeObjectURL(blobUrl);
+ audioDeviceState.testAudioElement = null;
+ };
+
+ audio.play();
+ }
+
+ // Stop test playback
+ function stopTestPlayback() {
+ if (audioDeviceState.testAudioElement) {
+ audioDeviceState.testAudioElement.pause();
+ audioDeviceState.testAudioElement.currentTime = 0;
+
+ // Trigger onended manually
+ audioDeviceState.testState = 'idle';
+ testPlayBtn.textContent = '▶️ 播放測試';
+ testPlayBtn.classList.remove('playing');
+ testRecordBtn.disabled = false;
+ setAudioStatus('準備就緒', 'success');
+
+ if (audioDeviceState.testAudioElement.src) {
+ URL.revokeObjectURL(audioDeviceState.testAudioElement.src);
+ }
+ audioDeviceState.testAudioElement = null;
+ }
+ }
+
+ // Toggle panel collapse
+ function toggleAudioDevicePanel() {
+ audioDevicePanel.classList.toggle('collapsed');
+ const isCollapsed = audioDevicePanel.classList.contains('collapsed');
+ localStorage.setItem('audioDevice.panelCollapsed', isCollapsed);
+
+ if (isCollapsed) {
+ stopVolumeMonitoring();
+ } else {
+ startVolumeMonitoring();
+ }
+ }
+
+ // Initialize audio device panel
+ async function initAudioDevicePanel() {
+ // Restore panel collapse state
+ const isCollapsed = localStorage.getItem('audioDevice.panelCollapsed') === 'true';
+ if (isCollapsed) {
+ audioDevicePanel.classList.add('collapsed');
+ }
+
+ // Event listeners
+ audioDeviceHeader.addEventListener('click', toggleAudioDevicePanel);
+
+ audioDeviceSelect.addEventListener('change', (e) => {
+ selectAudioDevice(e.target.value);
+ });
+
+ audioRefreshBtn.addEventListener('click', async () => {
+ setAudioStatus('重新整理中...', '');
+ await enumerateAudioDevices();
+ });
+
+ testRecordBtn.addEventListener('click', () => {
+ if (audioDeviceState.testState === 'idle') {
+ startTestRecording();
+ } else if (audioDeviceState.testState === 'recording') {
+ stopTestRecording();
+ }
+ });
+
+ testPlayBtn.addEventListener('click', () => {
+ if (audioDeviceState.testState === 'idle') {
+ playTestRecording();
+ } else if (audioDeviceState.testState === 'playing') {
+ stopTestPlayback();
+ }
+ });
+
+ // Listen for device changes (hot-plug)
+ navigator.mediaDevices.addEventListener('devicechange', () => {
+ console.log('Audio devices changed');
+ enumerateAudioDevices();
+ });
+
+ // Initial enumeration (only if panel is not collapsed)
+ if (!isCollapsed) {
+ await enumerateAudioDevices();
+ }
+ }
+
+ // Get selected device for main recording
+ function getSelectedAudioDevice() {
+ return audioDeviceState.selectedDeviceId;
+ }
+
+ // Initialize audio device panel on page load
+ initAudioDevicePanel();
+
+ // ========================================
+ // End Audio Device Management
+ // ========================================
+
// Update Whisper status display
async function updateWhisperStatus() {
try {
@@ -421,68 +1100,45 @@
return;
}
- // Debug: Check if we're in a secure context
- console.log('isSecureContext:', window.isSecureContext);
+ // Stop volume monitoring during main recording
+ stopVolumeMonitoring();
- // Check for available audio devices first
- const devices = await navigator.mediaDevices.enumerateDevices();
- const audioInputs = devices.filter(d => d.kind === 'audioinput');
- console.log('Available audio inputs:', audioInputs.length, audioInputs);
+ // Get selected device from audio device panel
+ const selectedDeviceId = getSelectedAudioDevice();
+ console.log('Using selected audio device:', selectedDeviceId);
- if (audioInputs.length === 0) {
- alert('No microphone found. Please connect a microphone and try again.');
- return;
- }
-
- // Helper to identify alias deviceIds (not real device IDs)
- const isAlias = (id) => id === 'default' || id === 'communications';
-
- // Filter out Stereo Mix (立體聲混音) - it's not a real microphone
- const realMicrophones = audioInputs.filter(d =>
- !d.label.includes('立體聲混音') &&
- !d.label.toLowerCase().includes('stereo mix')
- );
-
- // Prefer actual device IDs (long strings), not aliases like 'default' or 'communications'
- // First try to find a real microphone with actual deviceId
- let selectedMic = realMicrophones.find(d =>
- !isAlias(d.deviceId) && (
- d.label.includes('麥克風') ||
- d.label.toLowerCase().includes('microphone')
- )
- ) || realMicrophones.find(d => !isAlias(d.deviceId)) || realMicrophones[0];
-
- console.log('Real microphones found:', realMicrophones.length);
- console.log('Selected microphone:', selectedMic);
- console.log('Selected deviceId is alias:', selectedMic ? isAlias(selectedMic.deviceId) : 'N/A');
-
- if (!selectedMic) {
- alert('No real microphone found. Only Stereo Mix detected. Please connect a microphone.');
- return;
- }
-
- // Get microphone stream with proper constraints handling
+ // Get microphone stream with user-selected device
try {
- if (isAlias(selectedMic.deviceId)) {
+ let constraints;
+ if (isAliasDeviceId(selectedDeviceId)) {
// For alias deviceIds (default/communications), let the system choose
console.log('Using system default (alias detected)');
- mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
- } else {
+ constraints = { audio: true };
+ } else if (selectedDeviceId) {
// For real deviceIds, try exact first, then ideal as fallback
- try {
- mediaStream = await navigator.mediaDevices.getUserMedia({
- audio: { deviceId: { exact: selectedMic.deviceId } }
- });
- } catch (exactErr) {
+ constraints = { audio: { deviceId: { exact: selectedDeviceId } } };
+ } else {
+ // No device selected, use default
+ constraints = { audio: true };
+ }
+
+ try {
+ mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
+ } catch (exactErr) {
+ if (selectedDeviceId && !isAliasDeviceId(selectedDeviceId)) {
console.warn('Exact device ID failed, trying ideal:', exactErr);
mediaStream = await navigator.mediaDevices.getUserMedia({
- audio: { deviceId: { ideal: selectedMic.deviceId } }
+ audio: { deviceId: { ideal: selectedDeviceId } }
});
+ } else {
+ throw exactErr;
}
}
- console.log('Successfully connected to:', selectedMic.label);
+ console.log('Successfully connected to microphone');
} catch (err) {
console.error('getUserMedia failed:', err.name, err.message);
+ // Restart volume monitoring on error
+ startVolumeMonitoring();
throw err; // Let outer catch handle the error message
}
@@ -499,13 +1155,13 @@
} catch (error) {
console.error('Start recording error:', error);
- let errorMsg = 'Error starting recording: ' + error.message;
+ let errorMsg = '無法開始錄音: ' + error.message;
if (error.name === 'NotAllowedError') {
- errorMsg = 'Microphone access denied. Please grant permission and try again.';
+ errorMsg = '麥克風權限被拒絕,請在系統設定中允許存取麥克風。';
} else if (error.name === 'NotFoundError') {
- errorMsg = 'No microphone found. Please connect a microphone and try again.';
+ errorMsg = '未偵測到麥克風,請連接麥克風後重試。';
} else if (error.name === 'NotReadableError') {
- errorMsg = 'Microphone is in use by another application. Please close other apps using the microphone.';
+ errorMsg = '麥克風正被其他應用程式使用,請關閉其他使用麥克風的程式後重試。';
}
alert(errorMsg);
await cleanupRecording();
@@ -640,6 +1296,11 @@
recordBtn.classList.add('btn-danger');
streamingStatusEl.classList.add('hidden');
processingIndicatorEl.classList.add('hidden');
+
+ // Restart volume monitoring after recording ends
+ if (!audioDevicePanel.classList.contains('collapsed')) {
+ startVolumeMonitoring();
+ }
}
// === Audio File Upload ===