Add audio device reinit for proper exclusive mode release
This commit is contained in:
1 parent
b6b019e659
commit
0eb0b2ec1f
4 files changed
+151
-77
No files matched your search
@@ -348,6 +348,17 @@ pub fn get_audio_output_status(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reinitialize audio device (releases and re-acquires)
|
||||||
|
/// Call this when changing audio settings like exclusive mode or output device
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn reinit_audio_device(
|
||||||
|
device: Option<String>,
|
||||||
|
state: tauri::State<'_, crate::AppState>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
log::info!("Command: reinit_audio_device {:?}", device);
|
||||||
|
state.player.reinit_device(device)
|
||||||
|
}
|
||||||
|
|
||||||
/// PipeWire/PulseAudio sink information
|
/// PipeWire/PulseAudio sink information
|
||||||
#[derive(serde::Serialize, Clone)]
|
#[derive(serde::Serialize, Clone)]
|
||||||
pub struct PipewireSink {
|
pub struct PipewireSink {
|
||||||
|
|||||||
@@ -236,6 +236,7 @@ pub fn run() {
|
|||||||
commands::get_audio_devices,
|
commands::get_audio_devices,
|
||||||
commands::get_audio_output_status,
|
commands::get_audio_output_status,
|
||||||
commands::get_pipewire_sinks,
|
commands::get_pipewire_sinks,
|
||||||
|
commands::reinit_audio_device,
|
||||||
// Queue commands
|
// Queue commands
|
||||||
commands::add_to_queue,
|
commands::add_to_queue,
|
||||||
commands::add_to_queue_next,
|
commands::add_to_queue_next,
|
||||||
|
|||||||
+119
-68
@@ -34,6 +34,8 @@ enum AudioCommand {
|
|||||||
SetVolume(f32),
|
SetVolume(f32),
|
||||||
/// Seek to position in seconds
|
/// Seek to position in seconds
|
||||||
Seek(u64),
|
Seek(u64),
|
||||||
|
/// Reinitialize audio device (releases and re-acquires)
|
||||||
|
ReinitDevice { device_name: Option<String> },
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Event payload for playback state updates
|
/// Event payload for playback state updates
|
||||||
@@ -197,85 +199,82 @@ impl Player {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Find the requested device or use default
|
// Helper to find and initialize audio device
|
||||||
let device = if let Some(ref name) = device_name {
|
let init_device = |name: &Option<String>, state: &SharedState| -> Option<(OutputStream, rodio::OutputStreamHandle)> {
|
||||||
log::info!("Looking for audio device: {}", name);
|
let device = if let Some(ref name) = name {
|
||||||
let found = host.output_devices()
|
log::info!("Looking for audio device: {}", name);
|
||||||
.ok()
|
let found = host.output_devices()
|
||||||
.and_then(|mut devices| {
|
.ok()
|
||||||
devices.find(|d| d.name().ok().as_ref() == Some(name))
|
.and_then(|mut devices| {
|
||||||
});
|
devices.find(|d| d.name().ok().as_ref() == Some(name))
|
||||||
|
});
|
||||||
|
|
||||||
// Validate the found device
|
match found {
|
||||||
match found {
|
Some(d) if is_device_valid(&d) => {
|
||||||
Some(d) if is_device_valid(&d) => {
|
log::info!("Found and validated device: {}", name);
|
||||||
log::info!("Found and validated device: {}", name);
|
Some(d)
|
||||||
Some(d)
|
}
|
||||||
|
Some(_) => {
|
||||||
|
log::warn!("Device '{}' found but has no valid output configs, using default", name);
|
||||||
|
host.default_output_device()
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
log::warn!("Device '{}' not found, using default", name);
|
||||||
|
host.default_output_device()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Some(_) => {
|
} else {
|
||||||
log::warn!("Device '{}' found but has no valid output configs, using default", name);
|
log::info!("Using default audio device");
|
||||||
host.default_output_device()
|
host.default_output_device()
|
||||||
|
};
|
||||||
|
|
||||||
|
let device = match device {
|
||||||
|
Some(d) => {
|
||||||
|
if let Ok(name) = d.name() {
|
||||||
|
log::info!("Using audio device: {}", name);
|
||||||
|
state.set_current_device(Some(name));
|
||||||
|
}
|
||||||
|
d
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
log::warn!("Device '{}' not found, using default", name);
|
log::error!("No audio output device available");
|
||||||
host.default_output_device()
|
state.set_current_device(None);
|
||||||
|
return None;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
} else {
|
|
||||||
log::info!("Using default audio device");
|
|
||||||
host.default_output_device()
|
|
||||||
};
|
|
||||||
|
|
||||||
let device = match device {
|
match OutputStream::try_from_device(&device) {
|
||||||
Some(d) => {
|
Ok(s) => {
|
||||||
if let Ok(name) = d.name() {
|
log::info!("Audio output initialized successfully");
|
||||||
log::info!("Using audio device: {}", name);
|
Some(s)
|
||||||
thread_state.set_current_device(Some(name));
|
|
||||||
}
|
}
|
||||||
d
|
Err(e) => {
|
||||||
}
|
log::error!("Failed to create audio output on device: {}. Trying default...", e);
|
||||||
None => {
|
match OutputStream::try_default() {
|
||||||
log::error!("No audio output device available");
|
Ok(s) => {
|
||||||
thread_state.set_current_device(None);
|
log::info!("Fallback to default audio output succeeded");
|
||||||
// Keep thread alive to receive commands
|
Some(s)
|
||||||
loop {
|
}
|
||||||
match rx.recv() {
|
Err(e2) => {
|
||||||
Ok(_) => log::warn!("Audio command received but no device available"),
|
log::error!("Failed to create default audio output: {}", e2);
|
||||||
Err(_) => break,
|
state.set_current_device(None);
|
||||||
}
|
None
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create output stream on the selected device
|
|
||||||
let (_stream, stream_handle) = match OutputStream::try_from_device(&device) {
|
|
||||||
Ok(s) => {
|
|
||||||
log::info!("Audio output initialized successfully");
|
|
||||||
s
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to create audio output on device: {}. Trying default...", e);
|
|
||||||
// Fallback to default
|
|
||||||
match OutputStream::try_default() {
|
|
||||||
Ok(s) => {
|
|
||||||
log::info!("Fallback to default audio output succeeded");
|
|
||||||
s
|
|
||||||
}
|
|
||||||
Err(e2) => {
|
|
||||||
log::error!("Failed to create default audio output: {}", e2);
|
|
||||||
loop {
|
|
||||||
match rx.recv() {
|
|
||||||
Ok(_) => log::warn!("Audio command received but audio output is unavailable"),
|
|
||||||
Err(_) => break,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Initialize audio device
|
||||||
|
let mut current_device_name = device_name.clone();
|
||||||
|
let mut stream_opt: Option<(OutputStream, rodio::OutputStreamHandle)> =
|
||||||
|
init_device(¤t_device_name, &thread_state);
|
||||||
|
|
||||||
|
if stream_opt.is_none() {
|
||||||
|
// Wait for ReinitDevice command
|
||||||
|
log::warn!("No audio device available, waiting for reinit command...");
|
||||||
|
}
|
||||||
|
|
||||||
let mut current_sink: Option<Sink> = None;
|
let mut current_sink: Option<Sink> = None;
|
||||||
// Store audio data for seeking (we need to re-decode from the beginning)
|
// Store audio data for seeking (we need to re-decode from the beginning)
|
||||||
let mut current_audio_data: Option<Vec<u8>> = None;
|
let mut current_audio_data: Option<Vec<u8>> = None;
|
||||||
@@ -291,6 +290,12 @@ impl Player {
|
|||||||
Ok(AudioCommand::Play { data, track_id, duration_secs }) => {
|
Ok(AudioCommand::Play { data, track_id, duration_secs }) => {
|
||||||
log::info!("Audio thread: playing track {}", track_id);
|
log::info!("Audio thread: playing track {}", track_id);
|
||||||
|
|
||||||
|
// Check if we have an audio stream
|
||||||
|
let Some(ref stream) = stream_opt else {
|
||||||
|
log::error!("Audio thread: no audio device available");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
// Stop existing playback
|
// Stop existing playback
|
||||||
if let Some(sink) = current_sink.take() {
|
if let Some(sink) = current_sink.take() {
|
||||||
sink.stop();
|
sink.stop();
|
||||||
@@ -300,7 +305,7 @@ impl Player {
|
|||||||
current_audio_data = Some(data.clone());
|
current_audio_data = Some(data.clone());
|
||||||
|
|
||||||
// Create new sink
|
// Create new sink
|
||||||
let sink = match Sink::try_new(&stream_handle) {
|
let sink = match Sink::try_new(&stream.1) {
|
||||||
Ok(s) => {
|
Ok(s) => {
|
||||||
consecutive_sink_failures = 0; // Reset on success
|
consecutive_sink_failures = 0; // Reset on success
|
||||||
s
|
s
|
||||||
@@ -391,6 +396,11 @@ impl Player {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let Some(ref stream) = stream_opt else {
|
||||||
|
log::error!("Audio thread: cannot seek - no audio device available");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
log::info!("Audio thread: seeking to {}s", position_secs);
|
log::info!("Audio thread: seeking to {}s", position_secs);
|
||||||
|
|
||||||
// Stop current sink
|
// Stop current sink
|
||||||
@@ -399,7 +409,7 @@ impl Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create new sink
|
// Create new sink
|
||||||
let sink = match Sink::try_new(&stream_handle) {
|
let sink = match Sink::try_new(&stream.1) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to create sink for seek: {}", e);
|
log::error!("Failed to create sink for seek: {}", e);
|
||||||
@@ -442,6 +452,39 @@ impl Player {
|
|||||||
current_sink = Some(sink);
|
current_sink = Some(sink);
|
||||||
log::info!("Audio thread: seeked to {}s (was_playing: {})", position_secs, was_playing);
|
log::info!("Audio thread: seeked to {}s (was_playing: {})", position_secs, was_playing);
|
||||||
}
|
}
|
||||||
|
Ok(AudioCommand::ReinitDevice { device_name: new_device }) => {
|
||||||
|
log::info!("Audio thread: reinitializing device (new: {:?})", new_device);
|
||||||
|
|
||||||
|
// Stop any current playback
|
||||||
|
if let Some(sink) = current_sink.take() {
|
||||||
|
sink.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop the current stream to release the device
|
||||||
|
// This is the key step - dropping releases exclusive access
|
||||||
|
drop(stream_opt.take());
|
||||||
|
log::info!("Audio thread: previous stream dropped, device released");
|
||||||
|
|
||||||
|
// Small delay to let the system release the device
|
||||||
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
|
||||||
|
// Reinitialize with new device
|
||||||
|
current_device_name = new_device;
|
||||||
|
stream_opt = init_device(¤t_device_name, &thread_state);
|
||||||
|
|
||||||
|
if stream_opt.is_some() {
|
||||||
|
log::info!("Audio thread: device reinitialized successfully");
|
||||||
|
consecutive_sink_failures = 0;
|
||||||
|
} else {
|
||||||
|
log::error!("Audio thread: failed to reinitialize device");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear playback state
|
||||||
|
thread_state.is_playing.store(false, Ordering::SeqCst);
|
||||||
|
thread_state.position.store(0, Ordering::SeqCst);
|
||||||
|
thread_state.playback_start_millis.store(0, Ordering::SeqCst);
|
||||||
|
current_audio_data = None;
|
||||||
|
}
|
||||||
Err(RecvTimeoutError::Timeout) => {
|
Err(RecvTimeoutError::Timeout) => {
|
||||||
// Check if sink is empty (playback finished)
|
// Check if sink is empty (playback finished)
|
||||||
if let Some(ref sink) = current_sink {
|
if let Some(ref sink) = current_sink {
|
||||||
@@ -598,6 +641,14 @@ impl Player {
|
|||||||
.map_err(|e| format!("Failed to send seek command: {}", e))
|
.map_err(|e| format!("Failed to send seek command: {}", e))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reinitialize audio device (releases and re-acquires the device)
|
||||||
|
/// Use this when changing audio settings like exclusive mode
|
||||||
|
pub fn reinit_device(&self, device_name: Option<String>) -> Result<(), String> {
|
||||||
|
self.tx
|
||||||
|
.send(AudioCommand::ReinitDevice { device_name })
|
||||||
|
.map_err(|e| format!("Failed to send reinit command: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
/// Get current playback state with real-time position
|
/// Get current playback state with real-time position
|
||||||
pub fn get_state(&self) -> Result<PlaybackState, String> {
|
pub fn get_state(&self) -> Result<PlaybackState, String> {
|
||||||
Ok(PlaybackState {
|
Ok(PlaybackState {
|
||||||
|
|||||||
@@ -361,17 +361,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get current raw device name for reinit
|
||||||
|
function getCurrentRawDevice(): string | null {
|
||||||
|
const rawName = deviceNameMap.get(outputDevice) ?? outputDevice;
|
||||||
|
return rawName === 'System Default' ? null : rawName;
|
||||||
|
}
|
||||||
|
|
||||||
async function handleOutputDeviceChange(prettyName: string) {
|
async function handleOutputDeviceChange(prettyName: string) {
|
||||||
outputDevice = prettyName;
|
outputDevice = prettyName;
|
||||||
// Convert pretty name back to raw name for saving
|
// Convert pretty name back to raw name for saving
|
||||||
const rawName = deviceNameMap.get(prettyName) ?? prettyName;
|
const rawName = deviceNameMap.get(prettyName) ?? prettyName;
|
||||||
|
const deviceToUse = rawName === 'System Default' ? null : rawName;
|
||||||
try {
|
try {
|
||||||
await invoke('set_audio_output_device', {
|
await invoke('set_audio_output_device', { device: deviceToUse });
|
||||||
device: rawName === 'System Default' ? null : rawName
|
// Reinitialize audio to apply the change immediately
|
||||||
});
|
await invoke('reinit_audio_device', { device: deviceToUse });
|
||||||
console.log('Audio output device saved:', rawName, '(displayed as:', prettyName, ')');
|
console.log('Audio output device changed and reinitialized:', rawName, '(displayed as:', prettyName, ')');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to save audio output device:', err);
|
console.error('Failed to change audio output device:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,9 +386,11 @@
|
|||||||
exclusiveMode = enabled;
|
exclusiveMode = enabled;
|
||||||
try {
|
try {
|
||||||
await invoke('set_audio_exclusive_mode', { enabled });
|
await invoke('set_audio_exclusive_mode', { enabled });
|
||||||
console.log('Exclusive mode saved:', enabled);
|
// Reinitialize audio to apply/release exclusive mode immediately
|
||||||
|
await invoke('reinit_audio_device', { device: getCurrentRawDevice() });
|
||||||
|
console.log('Exclusive mode changed and audio reinitialized:', enabled);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to save exclusive mode:', err);
|
console.error('Failed to change exclusive mode:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,9 +398,11 @@
|
|||||||
dacPassthrough = enabled;
|
dacPassthrough = enabled;
|
||||||
try {
|
try {
|
||||||
await invoke('set_audio_dac_passthrough', { enabled });
|
await invoke('set_audio_dac_passthrough', { enabled });
|
||||||
console.log('DAC passthrough saved:', enabled);
|
// DAC passthrough may also require reinit for proper effect
|
||||||
|
await invoke('reinit_audio_device', { device: getCurrentRawDevice() });
|
||||||
|
console.log('DAC passthrough changed and audio reinitialized:', enabled);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to save DAC passthrough:', err);
|
console.error('Failed to change DAC passthrough:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user