@@ -49,6 +49,31 @@ fn rand_jitter_ms(max_ms: u64) -> u64 {
4949 nanos % range
5050}
5151
52+ fn fallback_supported_formats ( ) -> Vec < AudioFormatSpec > {
53+ vec ! [
54+ AudioFormatSpec {
55+ codec: "pcm" . to_string( ) ,
56+ channels: 2 ,
57+ sample_rate: 48000 ,
58+ bit_depth: 16 ,
59+ } ,
60+ AudioFormatSpec {
61+ codec: "pcm" . to_string( ) ,
62+ channels: 2 ,
63+ sample_rate: 44100 ,
64+ bit_depth: 16 ,
65+ } ,
66+ ]
67+ }
68+
69+ fn format_specs_to_log_string ( formats : & [ AudioFormatSpec ] ) -> String {
70+ formats
71+ . iter ( )
72+ . map ( |f| format ! ( "{}ch/{}Hz/{}bit" , f. channels, f. sample_rate, f. bit_depth) )
73+ . collect :: < Vec < _ > > ( )
74+ . join ( ", " )
75+ }
76+
5277/// Commands sent to the playback thread
5378enum PlayerCommand {
5479 /// Create a new `SyncedPlayer` with the given format
@@ -384,6 +409,33 @@ async fn run_client(
384409 ResolvedVolumeMode :: None => vec ! [ ] ,
385410 } ;
386411
412+ // Resolve output device once per connection and derive supported formats for this device.
413+ // This avoids negotiating formats that the selected Windows output cannot open.
414+ let output_device = devices:: resolve_output_device ( config. audio_device_id . as_deref ( ) ) ;
415+ let mut supported_formats: Vec < AudioFormatSpec > =
416+ devices:: derive_supported_pcm_formats ( output_device. as_ref ( ) )
417+ . into_iter ( )
418+ . map ( |f| AudioFormatSpec {
419+ codec : "pcm" . to_string ( ) ,
420+ channels : f. channels as _ ,
421+ sample_rate : f. sample_rate ,
422+ bit_depth : f. bit_depth as _ ,
423+ } )
424+ . collect ( ) ;
425+
426+ if supported_formats. is_empty ( ) {
427+ supported_formats = fallback_supported_formats ( ) ;
428+ eprintln ! (
429+ "[Sendspin] No reliable device format capabilities found; using conservative fallback formats: {}" ,
430+ format_specs_to_log_string( & supported_formats)
431+ ) ;
432+ } else {
433+ eprintln ! (
434+ "[Sendspin] Advertising device-aware formats: {}" ,
435+ format_specs_to_log_string( & supported_formats)
436+ ) ;
437+ }
438+
387439 // Build ClientHello message
388440 // Request player, controller, and metadata roles for full functionality
389441 let hello = ClientHello {
@@ -401,26 +453,7 @@ async fn run_client(
401453 software_version : Some ( env ! ( "CARGO_PKG_VERSION" ) . to_string ( ) ) ,
402454 } ) ,
403455 player_v1_support : Some ( PlayerV1Support {
404- supported_formats : vec ! [
405- AudioFormatSpec {
406- codec: "pcm" . to_string( ) ,
407- channels: 2 ,
408- sample_rate: 44100 ,
409- bit_depth: 16 ,
410- } ,
411- AudioFormatSpec {
412- codec: "pcm" . to_string( ) ,
413- channels: 2 ,
414- sample_rate: 48000 ,
415- bit_depth: 24 ,
416- } ,
417- AudioFormatSpec {
418- codec: "pcm" . to_string( ) ,
419- channels: 2 ,
420- sample_rate: 96000 ,
421- bit_depth: 24 ,
422- } ,
423- ] ,
456+ supported_formats,
424457 // Buffer capacity in samples - larger buffer reduces server-side scheduling pressure
425458 // 480000 = 10 seconds of buffer at 48kHz
426459 buffer_capacity : 480000 ,
@@ -511,6 +544,7 @@ async fn run_client(
511544 command_rx,
512545 volume_change_rx,
513546 resolved_mode,
547+ output_device,
514548 )
515549 . await
516550}
@@ -570,6 +604,7 @@ async fn run_authenticated_client(
570604 mut command_rx : mpsc:: Receiver < String > ,
571605 mut volume_change_rx : mpsc:: Receiver < ( u8 , bool ) > ,
572606 resolved_mode : ResolvedVolumeMode ,
607+ output_device : Option < cpal:: Device > ,
573608) -> Result < ( ) , Box < dyn std:: error:: Error + Send + Sync > > {
574609 // Read initial volume/mute state once and reuse for both the
575610 // ClientState message and the local tracking variables.
@@ -631,22 +666,6 @@ async fn run_authenticated_client(
631666 // Create shared clock sync with Kalman filter-based drift correction
632667 let clock_sync = Arc :: new ( Mutex :: new ( ClockSync :: new ( ) ) ) ;
633668
634- // Get audio device
635- let device = if let Some ( ref device_id) = config. audio_device_id {
636- match devices:: get_device_by_id ( device_id) {
637- Ok ( d) => Some ( d) ,
638- Err ( e) => {
639- eprintln ! (
640- "[Sendspin] Failed to get device {}: {}, using default" ,
641- device_id, e
642- ) ;
643- None
644- }
645- }
646- } else {
647- None
648- } ;
649-
650669 // Create channel for sending commands to the playback thread
651670 let ( player_tx, player_rx) = std_mpsc:: channel :: < PlayerCommand > ( ) ;
652671
@@ -657,7 +676,7 @@ async fn run_authenticated_client(
657676 run_playback_thread (
658677 player_rx,
659678 clock_sync_for_thread,
660- device ,
679+ output_device ,
661680 use_software_volume,
662681 ) ;
663682 } ) ;
@@ -726,6 +745,14 @@ async fn run_authenticated_client(
726745 continue ;
727746 } ;
728747
748+ eprintln!(
749+ "[Sendspin] StreamStart format from server: codec={}, channels={}, sample_rate={}, bit_depth={}" ,
750+ player_config. codec,
751+ player_config. channels,
752+ player_config. sample_rate,
753+ player_config. bit_depth
754+ ) ;
755+
729756 if player_config. codec != "pcm" {
730757 eprintln!( "[Sendspin] Unsupported codec: {}" , player_config. codec) ;
731758 continue ;
@@ -1008,10 +1035,22 @@ fn run_playback_thread(
10081035 mute,
10091036 ) {
10101037 Ok ( player) => {
1038+ eprintln ! (
1039+ "[Sendspin] SyncedPlayer created: channels={}, sample_rate={}, bit_depth={}" ,
1040+ format. channels,
1041+ format. sample_rate,
1042+ format. bit_depth
1043+ ) ;
10111044 synced_player = Some ( player) ;
10121045 }
10131046 Err ( e) => {
1014- eprintln ! ( "[Sendspin] Failed to create SyncedPlayer: {}" , e) ;
1047+ eprintln ! (
1048+ "[Sendspin] Failed to create SyncedPlayer for channels={}, sample_rate={}, bit_depth={}: {}" ,
1049+ format. channels,
1050+ format. sample_rate,
1051+ format. bit_depth,
1052+ e
1053+ ) ;
10151054 }
10161055 }
10171056 }
@@ -1111,7 +1150,16 @@ pub async fn stop() {
11111150pub async fn restart ( ) {
11121151 // Read lock is scoped to this block so it's released before start()
11131152 // calls stop(), which takes a write lock on SENDSPIN_CLIENT.
1114- let config = { SENDSPIN_CLIENT . read ( ) . as_ref ( ) . map ( |c| c. config . clone ( ) ) } ;
1153+ let config = {
1154+ SENDSPIN_CLIENT . read ( ) . as_ref ( ) . map ( |c| {
1155+ let mut config = c. config . clone ( ) ;
1156+ let settings = crate :: settings:: get_settings ( ) ;
1157+ config. audio_device_id = settings. audio_device_id ;
1158+ config. sync_delay_ms = settings. sync_delay_ms ;
1159+ config. player_name = settings. sendspin_player_name ;
1160+ config
1161+ } )
1162+ } ;
11151163 if let Some ( config) = config {
11161164 log:: info!( "Restarting Sendspin client to apply new settings" ) ;
11171165 let _ = start ( config) . await ;
0 commit comments