@@ -51,6 +51,31 @@ fn rand_jitter_ms(max_ms: u64) -> u64 {
5151 nanos % range
5252}
5353
54+ fn fallback_supported_formats ( ) -> Vec < AudioFormatSpec > {
55+ vec ! [
56+ AudioFormatSpec {
57+ codec: "pcm" . to_string( ) ,
58+ channels: 2 ,
59+ sample_rate: 48000 ,
60+ bit_depth: 16 ,
61+ } ,
62+ AudioFormatSpec {
63+ codec: "pcm" . to_string( ) ,
64+ channels: 2 ,
65+ sample_rate: 44100 ,
66+ bit_depth: 16 ,
67+ } ,
68+ ]
69+ }
70+
71+ fn format_specs_to_log_string ( formats : & [ AudioFormatSpec ] ) -> String {
72+ formats
73+ . iter ( )
74+ . map ( |f| format ! ( "{}ch/{}Hz/{}bit" , f. channels, f. sample_rate, f. bit_depth) )
75+ . collect :: < Vec < _ > > ( )
76+ . join ( ", " )
77+ }
78+
5479/// Commands sent to the playback thread
5580enum PlayerCommand {
5681 /// Create a new `SyncedPlayer` with the given format
@@ -386,6 +411,33 @@ async fn run_client(
386411 ResolvedVolumeMode :: None => vec ! [ ] ,
387412 } ;
388413
414+ // Resolve output device once per connection and derive supported formats for this device.
415+ // This avoids negotiating formats that the selected Windows output cannot open.
416+ let output_device = devices:: resolve_output_device ( config. audio_device_id . as_deref ( ) ) ;
417+ let mut supported_formats: Vec < AudioFormatSpec > =
418+ devices:: derive_supported_pcm_formats ( output_device. as_ref ( ) )
419+ . into_iter ( )
420+ . map ( |f| AudioFormatSpec {
421+ codec : "pcm" . to_string ( ) ,
422+ channels : f. channels as _ ,
423+ sample_rate : f. sample_rate ,
424+ bit_depth : f. bit_depth as _ ,
425+ } )
426+ . collect ( ) ;
427+
428+ if supported_formats. is_empty ( ) {
429+ supported_formats = fallback_supported_formats ( ) ;
430+ eprintln ! (
431+ "[Sendspin] No reliable device format capabilities found; using conservative fallback formats: {}" ,
432+ format_specs_to_log_string( & supported_formats)
433+ ) ;
434+ } else {
435+ eprintln ! (
436+ "[Sendspin] Advertising device-aware formats: {}" ,
437+ format_specs_to_log_string( & supported_formats)
438+ ) ;
439+ }
440+
389441 // Build ClientHello message
390442 // Request player, controller, and metadata roles for full functionality
391443 let hello = ClientHello {
@@ -403,26 +455,7 @@ async fn run_client(
403455 software_version : Some ( env ! ( "CARGO_PKG_VERSION" ) . to_string ( ) ) ,
404456 } ) ,
405457 player_v1_support : Some ( PlayerV1Support {
406- supported_formats : vec ! [
407- AudioFormatSpec {
408- codec: "pcm" . to_string( ) ,
409- channels: 2 ,
410- sample_rate: 44100 ,
411- bit_depth: 16 ,
412- } ,
413- AudioFormatSpec {
414- codec: "pcm" . to_string( ) ,
415- channels: 2 ,
416- sample_rate: 48000 ,
417- bit_depth: 24 ,
418- } ,
419- AudioFormatSpec {
420- codec: "pcm" . to_string( ) ,
421- channels: 2 ,
422- sample_rate: 96000 ,
423- bit_depth: 24 ,
424- } ,
425- ] ,
458+ supported_formats,
426459 // Buffer capacity in samples - larger buffer reduces server-side scheduling pressure
427460 // 480000 = 10 seconds of buffer at 48kHz
428461 buffer_capacity : 480000 ,
@@ -513,6 +546,7 @@ async fn run_client(
513546 command_rx,
514547 volume_change_rx,
515548 resolved_mode,
549+ output_device,
516550 )
517551 . await
518552}
@@ -573,6 +607,7 @@ async fn run_authenticated_client(
573607 mut command_rx : mpsc:: Receiver < String > ,
574608 mut volume_change_rx : mpsc:: Receiver < ( u8 , bool ) > ,
575609 resolved_mode : ResolvedVolumeMode ,
610+ output_device : Option < cpal:: Device > ,
576611) -> Result < ( ) , Box < dyn std:: error:: Error + Send + Sync > > {
577612 // Read initial volume/mute state once and reuse for both the
578613 // ClientState message and the local tracking variables.
@@ -637,22 +672,6 @@ async fn run_authenticated_client(
637672 // Create shared clock sync with Kalman filter-based drift correction
638673 let clock_sync = Arc :: new ( Mutex :: new ( ClockSync :: new ( Arc :: clone ( & clock) ) ) ) ;
639674
640- // Get audio device
641- let device = if let Some ( ref device_id) = config. audio_device_id {
642- match devices:: get_device_by_id ( device_id) {
643- Ok ( d) => Some ( d) ,
644- Err ( e) => {
645- eprintln ! (
646- "[Sendspin] Failed to get device {}: {}, using default" ,
647- device_id, e
648- ) ;
649- None
650- }
651- }
652- } else {
653- None
654- } ;
655-
656675 // Create channel for sending commands to the playback thread
657676 let ( player_tx, player_rx) = std_mpsc:: channel :: < PlayerCommand > ( ) ;
658677
@@ -663,7 +682,7 @@ async fn run_authenticated_client(
663682 run_playback_thread (
664683 player_rx,
665684 clock_sync_for_thread,
666- device ,
685+ output_device ,
667686 use_software_volume,
668687 ) ;
669688 } ) ;
@@ -740,6 +759,14 @@ async fn run_authenticated_client(
740759 continue ;
741760 } ;
742761
762+ eprintln!(
763+ "[Sendspin] StreamStart format from server: codec={}, channels={}, sample_rate={}, bit_depth={}" ,
764+ player_config. codec,
765+ player_config. channels,
766+ player_config. sample_rate,
767+ player_config. bit_depth
768+ ) ;
769+
743770 if player_config. codec != "pcm" {
744771 eprintln!( "[Sendspin] Unsupported codec: {}" , player_config. codec) ;
745772 continue ;
@@ -1014,10 +1041,22 @@ fn run_playback_thread(
10141041 mute,
10151042 ) {
10161043 Ok ( player) => {
1044+ eprintln ! (
1045+ "[Sendspin] SyncedPlayer created: channels={}, sample_rate={}, bit_depth={}" ,
1046+ format. channels,
1047+ format. sample_rate,
1048+ format. bit_depth
1049+ ) ;
10171050 synced_player = Some ( player) ;
10181051 }
10191052 Err ( e) => {
1020- eprintln ! ( "[Sendspin] Failed to create SyncedPlayer: {}" , e) ;
1053+ eprintln ! (
1054+ "[Sendspin] Failed to create SyncedPlayer for channels={}, sample_rate={}, bit_depth={}: {}" ,
1055+ format. channels,
1056+ format. sample_rate,
1057+ format. bit_depth,
1058+ e
1059+ ) ;
10211060 }
10221061 }
10231062 }
@@ -1117,7 +1156,16 @@ pub async fn stop() {
11171156pub async fn restart ( ) {
11181157 // Read lock is scoped to this block so it's released before start()
11191158 // calls stop(), which takes a write lock on SENDSPIN_CLIENT.
1120- let config = { SENDSPIN_CLIENT . read ( ) . as_ref ( ) . map ( |c| c. config . clone ( ) ) } ;
1159+ let config = {
1160+ SENDSPIN_CLIENT . read ( ) . as_ref ( ) . map ( |c| {
1161+ let mut config = c. config . clone ( ) ;
1162+ let settings = crate :: settings:: get_settings ( ) ;
1163+ config. audio_device_id = settings. audio_device_id ;
1164+ config. sync_delay_ms = settings. sync_delay_ms ;
1165+ config. player_name = settings. sendspin_player_name ;
1166+ config
1167+ } )
1168+ } ;
11211169 if let Some ( config) = config {
11221170 log:: info!( "Restarting Sendspin client to apply new settings" ) ;
11231171 let _ = start ( config) . await ;
0 commit comments