Skip to content

Commit 9979406

Browse files
committed
Refine microphone troubleshooting UI
1 parent 45432ae commit 9979406

5 files changed

Lines changed: 207 additions & 267 deletions

File tree

src_assets/common/assets/web/configs/tabs/AudioVideo.vue

Lines changed: 1 addition & 254 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup>
2-
import {ref, computed, inject, onMounted, onBeforeUnmount} from 'vue'
2+
import {ref, computed, inject} from 'vue'
33
import {$tp} from '../../platform-i18n'
44
import PlatformLayout from '../../PlatformLayout.vue'
55
import AdapterNameSelector from './audiovideo/AdapterNameSelector.vue'
@@ -28,129 +28,6 @@ const sudovdaStatus = {
2828
const currentDriverStatus = computed(() => sudovdaStatus[props.vdisplay])
2929
3030
const config = ref(props.config)
31-
const micDebug = ref(null)
32-
let micDebugPollHandle = null
33-
34-
const micMeterWidth = computed(() => `${Math.round(((micDebug.value?.lastInputLevel ?? 0) * 100))}%`)
35-
36-
const isFreshAge = (ageMs) => ageMs >= 0 && ageMs < 3000
37-
38-
const stageColorClass = (state) => ({
39-
success: 'bg-success-subtle border-success-subtle text-success-emphasis',
40-
warning: 'bg-warning-subtle border-warning-subtle text-warning-emphasis',
41-
danger: 'bg-danger-subtle border-danger-subtle text-danger-emphasis',
42-
idle: 'bg-secondary-subtle border-secondary-subtle text-secondary-emphasis',
43-
}[state] || 'bg-secondary-subtle border-secondary-subtle text-secondary-emphasis')
44-
45-
const stageBadgeClass = (state) => ({
46-
success: 'text-bg-success',
47-
warning: 'text-bg-warning',
48-
danger: 'text-bg-danger',
49-
idle: 'text-bg-secondary',
50-
}[state] || 'text-bg-secondary')
51-
52-
const micStages = computed(() => {
53-
const debug = micDebug.value
54-
if (!debug) {
55-
return []
56-
}
57-
58-
const captureState = !debug.sessionActive ? 'idle' : (debug.firstPacketReceived ? 'success' : 'warning')
59-
const captureDetail = !debug.sessionActive
60-
? 'Start a remote session with microphone redirection enabled.'
61-
: (debug.firstPacketReceived
62-
? 'Moonlight is sending microphone audio to Apollo. Use the Moonlight preview bar to validate the local source.'
63-
: 'Apollo has negotiated microphone redirection, but Moonlight has not sent microphone audio yet.')
64-
65-
let packetState = 'idle'
66-
let packetDetail = 'No active microphone session.'
67-
if (debug.sessionActive) {
68-
if (!debug.firstPacketReceived) {
69-
packetState = 'warning'
70-
packetDetail = 'Waiting for the first microphone packet from Moonlight.'
71-
} else if (isFreshAge(debug.lastPacketAgeMs)) {
72-
packetState = 'success'
73-
packetDetail = `Packets are arriving from Moonlight (${debug.lastPacketAgeMs} ms ago).`
74-
} else {
75-
packetState = 'warning'
76-
packetDetail = 'Packets arrived previously, but Apollo has not seen a fresh microphone packet recently.'
77-
}
78-
}
79-
80-
let decodeState = 'idle'
81-
let decodeDetail = 'No decoded microphone audio on the host yet.'
82-
if (debug.sessionActive) {
83-
if (debug.decodeErrors > 0 && !debug.decodeActive) {
84-
decodeState = 'danger'
85-
decodeDetail = 'Apollo received microphone packets but could not decode them.'
86-
} else if (debug.decodeActive && isFreshAge(debug.lastDecodeAgeMs)) {
87-
decodeState = 'success'
88-
decodeDetail = `Apollo decoded microphone audio successfully (${debug.lastDecodeAgeMs} ms ago).`
89-
} else if (debug.packetsReceived > 0) {
90-
decodeState = 'warning'
91-
decodeDetail = 'Apollo is receiving packets, but decoded microphone audio has not been confirmed yet.'
92-
}
93-
}
94-
95-
let renderState = 'idle'
96-
let renderDetail = 'Steam Streaming Microphone rendering has not started.'
97-
if (debug.sessionActive) {
98-
if (debug.renderErrors > 0 && !debug.renderActive) {
99-
renderState = 'danger'
100-
renderDetail = 'Apollo decoded microphone audio, but writing it into Steam Streaming Microphone failed.'
101-
} else if (debug.renderActive && isFreshAge(debug.lastRenderAgeMs)) {
102-
renderState = 'success'
103-
renderDetail = `Apollo is rendering microphone audio into ${debug.targetDeviceName || 'Steam Streaming Microphone'} (${debug.lastRenderAgeMs} ms ago).`
104-
} else if (debug.decodeActive) {
105-
renderState = 'warning'
106-
renderDetail = 'Apollo decoded microphone audio, but the Steam Streaming Microphone render stage has not completed yet.'
107-
}
108-
}
109-
110-
let signalState = 'idle'
111-
let signalDetail = 'No decoded microphone signal is available yet.'
112-
if (debug.sessionActive) {
113-
if (debug.signalDetected) {
114-
signalState = 'success'
115-
signalDetail = 'Apollo is detecting non-silent microphone audio from Moonlight.'
116-
} else if (debug.decodeActive) {
117-
signalState = 'warning'
118-
signalDetail = 'Decoded microphone audio is currently silent or below the signal threshold.'
119-
} else if (debug.firstPacketReceived) {
120-
signalState = 'warning'
121-
signalDetail = 'Packets are arriving, but Apollo has not decoded usable microphone audio yet.'
122-
}
123-
}
124-
125-
return [
126-
{ key: 'capture', label: 'Moonlight capture/send', state: captureState, detail: captureDetail },
127-
{ key: 'packets', label: 'Packets arriving', state: packetState, detail: packetDetail },
128-
{ key: 'decode', label: 'Decoded on host', state: decodeState, detail: decodeDetail },
129-
{ key: 'render', label: 'Rendered into Steam Streaming Microphone', state: renderState, detail: renderDetail },
130-
{ key: 'signal', label: 'Live signal detected', state: signalState, detail: signalDetail },
131-
]
132-
})
133-
134-
const loadMicDebug = async () => {
135-
if (props.platform !== 'windows') {
136-
return
137-
}
138-
139-
try {
140-
const response = await fetch('./api/audio-debug', {
141-
credentials: 'include',
142-
})
143-
144-
if (!response.ok) {
145-
return
146-
}
147-
148-
micDebug.value = await response.json()
149-
} catch (e) {
150-
console.error('Failed to load microphone debug status', e)
151-
}
152-
}
153-
15431
const validateFallbackMode = (event) => {
15532
const value = event.target.value;
15633
if (!value.match(/^\d+x\d+x\d+(\.\d+)?$/)) {
@@ -161,21 +38,6 @@ const validateFallbackMode = (event) => {
16138
16239
event.target.reportValidity();
16340
}
164-
165-
onMounted(() => {
166-
loadMicDebug()
167-
168-
if (props.platform === 'windows') {
169-
micDebugPollHandle = window.setInterval(loadMicDebug, 1000)
170-
}
171-
})
172-
173-
onBeforeUnmount(() => {
174-
if (micDebugPollHandle !== null) {
175-
window.clearInterval(micDebugPollHandle)
176-
micDebugPollHandle = null
177-
}
178-
})
17941
</script>
18042

18143
<template>
@@ -354,121 +216,6 @@ onBeforeUnmount(() => {
354216
</div>
355217
<div class="form-text" v-if="platform === 'windows' && vdisplay">Please ensure SudoVDA driver is installed to the latest version and enabled properly.</div>
356218

357-
<details class="mt-4" v-if="platform === 'windows' && micDebug">
358-
<summary class="fw-semibold">Remote Microphone Debug</summary>
359-
360-
<div class="mt-3">
361-
<div class="alert" :class="micDebug.renderActive ? 'alert-success' : (micDebug.sessionActive ? 'alert-warning' : 'alert-secondary')">
362-
{{ micDebug.state || 'No active remote microphone session' }}
363-
</div>
364-
365-
<div class="small text-muted mb-2">
366-
Use the Moonlight client preview to validate local microphone capture. The Apollo ladder below shows where the host-side path is succeeding or failing.
367-
</div>
368-
369-
<div class="list-group mb-3">
370-
<div
371-
class="list-group-item border"
372-
:class="stageColorClass(stage.state)"
373-
v-for="stage in micStages"
374-
:key="stage.key"
375-
>
376-
<div class="d-flex justify-content-between align-items-center gap-3">
377-
<div class="fw-semibold">{{ stage.label }}</div>
378-
<span class="badge" :class="stageBadgeClass(stage.state)">
379-
{{ stage.state === 'success' ? 'OK' : (stage.state === 'warning' ? 'Waiting' : (stage.state === 'danger' ? 'Error' : 'Idle')) }}
380-
</span>
381-
</div>
382-
<div class="small mt-1">{{ stage.detail }}</div>
383-
</div>
384-
</div>
385-
386-
<div class="row g-3">
387-
<div class="col-md-6">
388-
<div class="small text-muted mb-1">Client</div>
389-
<div>{{ micDebug.clientName || 'No active client' }}</div>
390-
391-
<div class="small text-muted mt-3 mb-1">Backend</div>
392-
<div>{{ micDebug.backendName || 'Not initialized' }}</div>
393-
394-
<div class="small text-muted mt-3 mb-1">Target device</div>
395-
<div>{{ micDebug.targetDeviceName || 'Unavailable' }}</div>
396-
397-
<div class="small text-muted mt-3 mb-1">Endpoint mix format</div>
398-
<div>{{ micDebug.endpointMixFormat || 'Unavailable' }}</div>
399-
400-
<div class="small text-muted mt-3 mb-1">Render device format</div>
401-
<div>{{ micDebug.renderDeviceFormat || 'Unavailable' }}</div>
402-
403-
<div class="small text-muted mt-3 mb-1">Initialized render format</div>
404-
<div>{{ micDebug.renderFormat || 'Unavailable' }}</div>
405-
406-
<div class="small text-muted mt-3 mb-1">Capture device</div>
407-
<div>{{ micDebug.captureDeviceName || 'Unavailable' }}</div>
408-
409-
<div class="small text-muted mt-3 mb-1">Capture endpoint mix format</div>
410-
<div>{{ micDebug.captureEndpointMixFormat || 'Unavailable' }}</div>
411-
412-
<div class="small text-muted mt-3 mb-1">Capture device format</div>
413-
<div>{{ micDebug.captureDeviceFormat || 'Unavailable' }}</div>
414-
415-
<div class="small text-muted mt-3 mb-1">Channel mapping</div>
416-
<div>{{ micDebug.channelMapping || 'Unavailable' }}</div>
417-
418-
<div class="small text-muted mt-3 mb-1">Packet flow</div>
419-
<div>Received: {{ micDebug.packetsReceived }}</div>
420-
<div>Decoded: {{ micDebug.packetsDecoded }}</div>
421-
<div>Rendered: {{ micDebug.packetsRendered }}</div>
422-
<div>Dropped: {{ micDebug.packetsDropped }}</div>
423-
<div>Decrypt errors: {{ micDebug.decryptErrors }}</div>
424-
<div>Decode errors: {{ micDebug.decodeErrors }}</div>
425-
<div>Render errors: {{ micDebug.renderErrors }}</div>
426-
<div>Silent packets: {{ micDebug.silentPackets }}</div>
427-
</div>
428-
429-
<div class="col-md-6">
430-
<div class="small text-muted mb-1">Decoded microphone level</div>
431-
<div class="progress mb-2" role="progressbar" aria-label="Decoded microphone level">
432-
<div class="progress-bar" :class="micDebug.signalDetected ? 'bg-success' : 'bg-secondary'" :style="{ width: micMeterWidth }">
433-
{{ Math.round((micDebug.lastInputLevel || 0) * 100) }}%
434-
</div>
435-
</div>
436-
<div class="small text-muted">
437-
{{ micDebug.signalDetected ? 'Host is detecting non-silent microphone input from Moonlight.' : 'No non-silent microphone input detected yet.' }}
438-
</div>
439-
440-
<div class="small text-muted mt-3 mb-1">Session state</div>
441-
<div>Mic requested: {{ micDebug.micRequested ? 'Yes' : 'No' }}</div>
442-
<div>Encryption: {{ micDebug.encryptionEnabled ? 'Enabled' : 'Disabled' }}</div>
443-
<div>First packet received: {{ micDebug.firstPacketReceived ? 'Yes' : 'No' }}</div>
444-
<div>Decode active: {{ micDebug.decodeActive ? 'Yes' : 'No' }}</div>
445-
<div>Resampling active: {{ micDebug.resamplingActive ? 'Yes' : 'No' }}</div>
446-
<div>Recommended format active: {{ micDebug.recommendedFormatActive ? 'Yes' : 'No' }}</div>
447-
<div>Recommended format enforced: {{ micDebug.recommendedFormatEnforced ? 'Yes' : 'No' }}</div>
448-
<div>Last packet age: {{ micDebug.lastPacketAgeMs >= 0 ? `${micDebug.lastPacketAgeMs} ms` : 'N/A' }}</div>
449-
<div>Last decode age: {{ micDebug.lastDecodeAgeMs >= 0 ? `${micDebug.lastDecodeAgeMs} ms` : 'N/A' }}</div>
450-
<div>Last render age: {{ micDebug.lastRenderAgeMs >= 0 ? `${micDebug.lastRenderAgeMs} ms` : 'N/A' }}</div>
451-
<div>Last payload size: {{ micDebug.lastPayloadSize || 0 }} bytes</div>
452-
<div>Last sequence number: {{ micDebug.lastSequenceNumber || 0 }}</div>
453-
454-
<div class="alert alert-danger mt-3 py-2" v-if="micDebug.lastError">
455-
{{ micDebug.lastError }}
456-
</div>
457-
</div>
458-
</div>
459-
460-
<div class="mt-3">
461-
<div class="small text-muted mb-1">Recent microphone events</div>
462-
<ul class="list-group">
463-
<li class="list-group-item small" v-for="event in micDebug.recentEvents" :key="event">{{ event }}</li>
464-
<li class="list-group-item small text-muted" v-if="!micDebug.recentEvents || micDebug.recentEvents.length === 0">
465-
No microphone debug events yet.
466-
</li>
467-
</ul>
468-
</div>
469-
</div>
470-
</details>
471-
472219
</div>
473220
</template>
474221

src_assets/common/assets/web/public/assets/locale/en.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@
187187
"mic_backend_auto": "Steam Streaming Microphone",
188188
"mic_backend_apollo_virtual_mic": "Steam Streaming Microphone",
189189
"mic_backend_legacy_device": "Steam Streaming Microphone",
190-
"mic_backend_desc_windows": "Apollo Mic on Windows uses Steam Streaming Microphone as the only supported host microphone backend. This does not replace normal streamed audio playback. Apollo still uses Steam Streaming Speakers for game audio, while redirected client microphone audio is written into \"Speakers (Steam Streaming Microphone)\". Apollo normalizes only the Steam microphone pair to 2 channels, 32-bit, 48000 Hz when microphone streaming starts, then renders into the Steam microphone endpoint with a float32 shared-mode stream. Host applications should use \"Microphone (Steam Streaming Microphone)\" as the microphone device. Redirected microphone transport is always required to use encrypted microphone packets; if the client negotiates plaintext mic transport, Apollo disables mic passthrough for that session.",
190+
"mic_backend_desc_windows": "Windows microphone passthrough uses Steam Streaming Microphone. Apollo writes to \"Speakers (Steam Streaming Microphone)\", and host apps should use \"Microphone (Steam Streaming Microphone)\".",
191191
"mic_device_desc_linux": "The virtual sink that should receive redirected client microphone audio. Apollo writes decoded mic audio into this sink; applications should then capture the sink's monitor source. Leave blank to disable microphone redirection on Linux.",
192192
"mic_device_desc_macos": "The virtual microphone or loopback device that should receive redirected client microphone audio. Use a BlackHole or Loopback-style device name. Leave blank to disable microphone redirection on macOS.",
193193
"mic_device_desc_windows": "Unused on Windows. Apollo Mic auto-detects the Steam Streaming Microphone render endpoint instead of using a manually selected playback device.",
@@ -434,7 +434,7 @@
434434
"stream_audio": "Stream Audio",
435435
"stream_audio_desc": "Whether to stream audio or not. Disabling this can be useful for streaming headless displays as second monitors.",
436436
"stream_mic": "Enable Microphone Passthrough",
437-
"stream_mic_desc": "Whether Apollo should accept redirected client microphone audio and inject it into the host microphone device. On Windows, Apollo writes the redirected microphone into \"Speakers (Steam Streaming Microphone)\" so host applications can read from \"Microphone (Steam Streaming Microphone)\". Apollo automatically normalizes only the Steam microphone endpoints to 2 channels, 32-bit, 48000 Hz when microphone streaming starts, then renders into the Steam microphone endpoint with a float32 shared-mode stream. Redirected microphone transport is always required to use encrypted microphone packets; if the client negotiates plaintext mic transport, Apollo disables mic passthrough for that session. Apollo can install the local Steam audio drivers when Steam is present.",
437+
"stream_mic_desc": "Allow Apollo to receive client microphone audio and send it to the host microphone backend.",
438438
"sunshine_name": "Server Name",
439439
"sunshine_name_desc": "The name displayed by Moonlight. If not specified, the PC's hostname is used",
440440
"sw_preset": "SW Presets",
@@ -588,7 +588,7 @@
588588
"force_close_error": "Error while closing Application",
589589
"force_close_success": "Application Closed Successfully!",
590590
"logs": "Logs",
591-
"logs_desc": "See the logs uploaded by Apollo",
591+
"logs_desc": "See Apollo logs and recent microphone events",
592592
"logs_find": "Find...",
593593
"restart_apollo": "Restart Apollo",
594594
"restart_apollo_desc": "If Apollo isn't working properly, you can try restarting it. This will terminate any running sessions.",

src_assets/common/assets/web/public/assets/locale/en_GB.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@
135135
"mic_backend_auto": "Steam Streaming Microphone",
136136
"mic_backend_apollo_virtual_mic": "Steam Streaming Microphone",
137137
"mic_backend_legacy_device": "Steam Streaming Microphone",
138-
"mic_backend_desc_windows": "Apollo exposes redirected client microphone audio on Windows through Steam Streaming Microphone only. Apollo writes into \"Speakers (Steam Streaming Microphone)\", normalizes only the Steam microphone pair to 2 channels, 32-bit, 48000 Hz when microphone streaming starts, then renders into the endpoint with a float32 shared-mode stream. Host applications should use \"Microphone (Steam Streaming Microphone)\" as the paired input device. Redirected microphone transport always requires encrypted microphone packets; if the client negotiates plaintext mic transport, Apollo disables mic passthrough for that session.",
138+
"mic_backend_desc_windows": "Windows microphone passthrough uses Steam Streaming Microphone. Apollo writes to \"Speakers (Steam Streaming Microphone)\", and host apps should use \"Microphone (Steam Streaming Microphone)\".",
139139
"mic_device_desc_linux": "The virtual sink that should receive redirected client microphone audio. Apollo writes decoded mic audio into this sink; applications should then capture the sink's monitor source. Leave blank to disable microphone redirection on Linux.",
140140
"mic_device_desc_macos": "The virtual microphone or loopback device that should receive redirected client microphone audio. Use a BlackHole or Loopback-style device name. Leave blank to disable microphone redirection on macOS.",
141141
"mic_device_desc_windows": "Unused on Windows. Apollo auto-detects the Steam Streaming Microphone render endpoint.",
@@ -342,7 +342,7 @@
342342
"resolutions": "Advertised Resolutions",
343343
"restart_note": "Apollo is restarting to apply changes.",
344344
"stream_mic": "Enable Microphone Passthrough",
345-
"stream_mic_desc": "Whether Apollo should accept redirected client microphone audio and inject it into a host virtual audio device. On Windows, Apollo writes into Steam Streaming Microphone, automatically normalizes only the Steam microphone endpoints to 2 channels, 32-bit, 48000 Hz when microphone streaming starts, renders into the endpoint with a float32 shared-mode stream, and requires encrypted microphone packets for redirected microphone transport. If the client negotiates plaintext mic transport, Apollo disables mic passthrough for that session.",
345+
"stream_mic_desc": "Allow Apollo to receive client microphone audio and send it to the host microphone backend.",
346346
"sunshine_name": "Apollo Name",
347347
"sunshine_name_desc": "The name displayed by Moonlight. If not specified, the PC's hostname is used",
348348
"sw_preset": "SW Presets",
@@ -443,7 +443,7 @@
443443
"force_close_error": "Error while closing Application",
444444
"force_close_success": "Application Closed Successfully!",
445445
"logs": "Logs",
446-
"logs_desc": "See the logs uploaded by Apollo",
446+
"logs_desc": "See Apollo logs and recent microphone events",
447447
"logs_find": "Find...",
448448
"restart_apollo": "Restart Apollo",
449449
"restart_apollo_desc": "If Apollo isn't working properly, you can try restarting it. This will terminate any running sessions.",

0 commit comments

Comments
 (0)