feat: UAC USB microphone passthrough

- Add UAC1 gadget function (ConfigFS) with optimized endpoint config
- Browser mic capture with Opus encoding via WebCodecs AudioEncoder
- WebSocket audio transport (Opus 64kbps, 100x bandwidth reduction vs raw PCM)
- aplay subprocess for reliable PCM playback to USB gadget
- Settings toggle + ActionBar mic button (hidden when UAC disabled)
- Dynamic PCM device resolution (/proc/asound)
- c_chmask=0 + req_number=4 fixes DWC3 composite isochronous endpoint issue
This commit is contained in:
arounyf
2026-07-23 06:56:53 +00:00
parent 4e32b05124
commit e9bed3688f
27 changed files with 977 additions and 16 deletions

View File

@@ -14,7 +14,7 @@ import { useComputerUseSocket, type ComputerUseServerMessage } from '@/composabl
import { useFeatureVisibility } from '@/composables/useFeatureVisibility'
import { useTheme } from '@/composables/useTheme'
import { getUnifiedAudio } from '@/composables/useUnifiedAudio'
import { streamApi, hidApi, atxApi, atxConfigApi, authApi, computerUseApi } from '@/api'
import { streamApi, hidApi, atxApi, atxConfigApi, authApi, computerUseApi, uacApi } from '@/api'
import type { ComputerUseScreenshot, ComputerUseSession } from '@/api'
import { CanonicalKey, HidBackend } from '@/types/generated'
import type { HidKeyboardEvent, HidMouseEvent } from '@/types/hid'
@@ -2961,7 +2961,15 @@ function handleToggleMouseMode() {
}
}
const uacEnabled = ref(false)
onMounted(async () => {
// Check if UAC is enabled (show mic button only if USB mic is available)
try {
const uacCfg = await uacApi.get()
uacEnabled.value = uacCfg.enabled
} catch { /* ignore */ }
consoleEvents.subscribe()
watch([wsConnected, wsNetworkError], ([connected, netError], [_prevConnected, prevNetError]) => {
@@ -3166,6 +3174,7 @@ onUnmounted(() => {
:show-terminal="showTerminal"
:show-computer-use="showComputerUse"
:show-paste-text="showPasteText"
:show-mic="uacEnabled"
@toggle-fullscreen="toggleFullscreen"
@toggle-stats="openStatsSheet"
@toggle-virtual-keyboard="handleToggleVirtualKeyboard"

View File

@@ -11,6 +11,7 @@ import {
authApi,
configApi,
otgNetworkApi,
uacApi,
hidApi,
streamApi,
atxConfigApi,
@@ -685,6 +686,7 @@ const config = ref({
turn_server: '',
turn_username: '',
turn_password: '',
uac_enabled: false,
})
const otgNetworkInterfaces = ref<NetworkInterfaceInfo[]>([])
@@ -1479,6 +1481,12 @@ async function saveConfig() {
},
})
otgNetworkStatus.value = response.status
await uacApi.update({
enabled: otgEnabled && config.value.uac_enabled,
sample_rate: 48000,
channels: 2,
})
}
if (activeSection.value !== 'hid') {
@@ -1499,7 +1507,7 @@ async function saveConfig() {
async function loadConfig() {
try {
const [video, stream, hid, msd, otgNetwork] = await Promise.all([
const [video, stream, hid, msd, otgNetwork, uac] = await Promise.all([
configStore.refreshVideo(),
configStore.refreshStream(),
configStore.refreshHid(),
@@ -1511,6 +1519,7 @@ async function loadConfig() {
host_mac: '',
device_mac: '',
})),
uacApi.get().catch(() => ({ enabled: false, sample_rate: 48000, channels: 2 })),
])
config.value = {
@@ -1536,6 +1545,7 @@ async function loadConfig() {
msd_dir: msd.msd_dir || '',
otg_network_enabled: otgNetwork.enabled,
otg_network_driver: otgNetwork.driver_mode,
uac_enabled: uac.enabled,
otg_network_interface: otgNetwork.bridge_interface,
encoder_backend: stream.encoder || 'auto',
stun_server: stream.stun_server || '',
@@ -3342,6 +3352,15 @@ watch(isWindows, () => {
{{ t('settings.otgRuntimeDegraded') }}: {{ otgNetworkStatus.error || t('common.error') }}
</p>
</div>
<div class="space-y-3 rounded-md border border-border/60 p-3">
<div class="flex items-center justify-between gap-4">
<div>
<Label>{{ t('settings.uacMic') }}</Label>
<p class="text-xs text-muted-foreground">{{ t('settings.uacMicDesc') }}</p>
</div>
<Switch v-model="config.uac_enabled" />
</div>
</div>
</div>
<p class="text-xs text-warning">
{{ t('settings.otgProfileWarning') }}