* fix(linux): avoid audio streams during permission checks (#968) * fix(ci): remove stale qwen3 submodule gitlink * fix(ci): restore qwen3 submodule mapping
This commit is contained in:
1 parent
42c1b31754
commit
1ee93576b0
3 files changed
+64
-46
No files matched your search
@@ -335,26 +335,40 @@ mod platform {
|
||||
|
||||
/// Windows 的麦克风权限走系统设置 → 隐私 → 麦克风;
|
||||
/// 这里用 cpal 建立一次短生命周期输入流,避免只查设备格式时误报已授权。
|
||||
/// Linux 没有对应的应用级权限状态,只检查设备是否存在,避免 PipeWire
|
||||
/// 因权限探测建立临时输入流而触发桌面音量 OSD(issue #968)。
|
||||
pub fn check_microphone() -> PermissionStatus {
|
||||
if windows_microphone_registry_denied() {
|
||||
log::warn!("[mic] Windows microphone privacy registry is denied");
|
||||
return PermissionStatus::Denied;
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
return if has_microphone_input_device() {
|
||||
PermissionStatus::Granted
|
||||
} else {
|
||||
PermissionStatus::NoDevice
|
||||
};
|
||||
}
|
||||
|
||||
let host = cpal::default_host();
|
||||
let Some(device) = host.default_input_device() else {
|
||||
log::warn!("[mic] no default input device");
|
||||
return PermissionStatus::NoDevice;
|
||||
};
|
||||
let supported = match device.default_input_config() {
|
||||
Ok(config) => config,
|
||||
Err(err) => return classify_audio_probe_error(err.to_string()),
|
||||
};
|
||||
let sample_format = supported.sample_format();
|
||||
let config: StreamConfig = supported.config();
|
||||
match probe_input_stream(&device, &config, sample_format) {
|
||||
Ok(()) => PermissionStatus::Granted,
|
||||
Err(message) => classify_audio_probe_error(message),
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
if windows_microphone_registry_denied() {
|
||||
log::warn!("[mic] Windows microphone privacy registry is denied");
|
||||
return PermissionStatus::Denied;
|
||||
}
|
||||
|
||||
let host = cpal::default_host();
|
||||
let Some(device) = host.default_input_device() else {
|
||||
log::warn!("[mic] no default input device");
|
||||
return PermissionStatus::NoDevice;
|
||||
};
|
||||
let supported = match device.default_input_config() {
|
||||
Ok(config) => config,
|
||||
Err(err) => return classify_audio_probe_error(err.to_string()),
|
||||
};
|
||||
let sample_format = supported.sample_format();
|
||||
let config: StreamConfig = supported.config();
|
||||
match probe_input_stream(&device, &config, sample_format) {
|
||||
Ok(()) => PermissionStatus::Granted,
|
||||
Err(message) => classify_audio_probe_error(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// 录音提示音:监听 capsule:state 事件,在"开始录音"边沿播放合成提示音。
|
||||
// 独立组件,不依赖胶囊窗口显示——Linux 上胶囊隐藏也能正常工作。
|
||||
// Android Web Audio 输出会触发部分设备的录音输入路由切换,移动端禁用。
|
||||
// 独立组件,不依赖胶囊窗口显示——胶囊隐藏时也能正常工作。
|
||||
// Linux 的 Web Audio 输出会被 PipeWire/KDE 当作 sink-input,启动/退出时触发音量 OSD;
|
||||
// Android 也可能因 Web Audio 输出切换录音输入路由,因此这两个平台禁用。
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { detectOS } from './WindowChrome';
|
||||
import { isAndroid, isTauri } from '../lib/ipc';
|
||||
import { playRecordStartCue, primeAudioCue, stopAudioCue } from '../lib/audioCue';
|
||||
import type { CapsuleState, UserPreferences } from '../lib/types';
|
||||
@@ -18,7 +20,7 @@ interface CapsulePayload {
|
||||
export function AudioCueListener() {
|
||||
const audioCueEnabledRef = useRef<boolean>(true);
|
||||
const prevStateRef = useRef<CapsuleState>('idle' as CapsuleState);
|
||||
const audioCueRuntimeEnabled = !isAndroid();
|
||||
const audioCueRuntimeEnabled = !isAndroid() && detectOS() !== 'linux';
|
||||
|
||||
// 读取设置(默认开启)
|
||||
useEffect(() => {
|
||||
|
||||
@@ -424,32 +424,34 @@ export function RecordingInputSection() {
|
||||
<SettingRow label={t('settings.recording.muteDuringRecordingLabel')} desc={t('settings.recording.muteDuringRecordingDesc')}>
|
||||
<Toggle on={prefs.muteDuringRecording} onToggle={onMuteDuringRecordingChange} />
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label={t('settings.recording.audioCueLabel')}
|
||||
desc={t('settings.recording.audioCueDesc')}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<Toggle on={prefs.audioCueOnRecord} onToggle={onAudioCueChange} />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => playRecordStartCue()}
|
||||
style={{
|
||||
padding: '5px 12px',
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
fontFamily: 'inherit',
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
borderRadius: 8,
|
||||
background: 'var(--ol-surface-2)',
|
||||
color: 'var(--ol-ink-2)',
|
||||
cursor: 'default',
|
||||
transition: 'background 0.16s var(--ol-motion-quick)',
|
||||
}}
|
||||
>
|
||||
{t('settings.recording.audioCuePreview')}
|
||||
</button>
|
||||
</div>
|
||||
</SettingRow>
|
||||
{os !== 'linux' && (
|
||||
<SettingRow
|
||||
label={t('settings.recording.audioCueLabel')}
|
||||
desc={t('settings.recording.audioCueDesc')}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<Toggle on={prefs.audioCueOnRecord} onToggle={onAudioCueChange} />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => playRecordStartCue()}
|
||||
style={{
|
||||
padding: '5px 12px',
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
fontFamily: 'inherit',
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
borderRadius: 8,
|
||||
background: 'var(--ol-surface-2)',
|
||||
color: 'var(--ol-ink-2)',
|
||||
cursor: 'default',
|
||||
transition: 'background 0.16s var(--ol-motion-quick)',
|
||||
}}
|
||||
>
|
||||
{t('settings.recording.audioCuePreview')}
|
||||
</button>
|
||||
</div>
|
||||
</SettingRow>
|
||||
)}
|
||||
{os === 'linux' && (
|
||||
<SettingRow label={t('settings.advanced.streamingInsertLabel')}>
|
||||
<Toggle
|
||||
|
||||
Reference in new issue
Block a user