Rockbox open source high quality audio player as a Music Player Daemon
mpris rockbox mpd libadwaita audio rust zig deno
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

UPnP: Notify renderer only on initial play

Let the monitor perform SetAVTransportURI+Play for subsequent track
changes and resumes. Only send the initial Play from pcm_upnp_start to
avoid an audible blip when re-sending Play for the first detection.
Reduce monitor poll interval from 2s to 500ms, rename variables for
clarity, simplify the AVTransport info log, and shorten the ICY tag
text in pcm_server.

+85 -57
+84 -54
crates/upnp/src/lib.rs
··· 290 290 } 291 291 } 292 292 293 - // --- Notify the renderer with current track metadata --- 293 + // --- Notify the renderer on initial play only --- 294 294 let (port, sample_rate, renderer_url) = { 295 295 let cfg = CONFIG.lock().unwrap(); 296 296 (cfg.pcm_port, cfg.sample_rate, cfg.renderer_url.clone()) ··· 302 302 *rp = true; 303 303 !was 304 304 }; 305 - let local_ip = get_local_ip(); 306 - let track = rockbox_sys::playback::current_track(); 307 - let rt = get_runtime(); 308 305 309 - // Start (or replace) the track-change monitor. 310 - ensure_track_monitor(url.clone(), port); 311 - 312 - rt.spawn(async move { 306 + if need_play { 307 + ensure_track_monitor(url.clone(), port); 308 + let local_ip = get_local_ip(); 313 309 let stream_url = format!("http://{}:{}/stream.wav", local_ip, port); 314 - let album_art_url = if let Some(ref t) = track { 315 - get_album_art_url(&t.path, local_ip).await 316 - } else { 317 - None 318 - }; 319 - if let Err(e) = avtransport_play( 320 - &url, 321 - &stream_url, 322 - track.as_ref(), 323 - sample_rate, 324 - need_play, 325 - album_art_url.as_deref(), 326 - ) 327 - .await 328 - { 329 - tracing::warn!("UPnP AVTransport play failed: {}", e); 330 - } 331 - }); 310 + let rt = get_runtime(); 311 + rt.spawn(async move { 312 + let track = rockbox_sys::playback::current_track(); 313 + let album_art_url = if let Some(ref t) = track { 314 + get_album_art_url(&t.path, local_ip).await 315 + } else { 316 + None 317 + }; 318 + if let Err(e) = avtransport_play( 319 + &url, 320 + &stream_url, 321 + track.as_ref(), 322 + sample_rate, 323 + true, 324 + album_art_url.as_deref(), 325 + ) 326 + .await 327 + { 328 + tracing::warn!("UPnP AVTransport play failed: {}", e); 329 + } 330 + }); 331 + } 332 + // On subsequent sink_dma_start calls (track change or resume after pause), 333 + // the monitor handles everything: it detects the new current_track() path 334 + // and sends SetAVTransportURI+Play with fresh metadata. No action needed here. 332 335 } 333 336 0 334 337 } ··· 381 384 // --------------------------------------------------------------------------- 382 385 383 386 fn ensure_track_monitor(renderer_url: String, port: u16) { 384 - let gen = MONITOR_GEN.fetch_add(1, Ordering::SeqCst) + 1; 387 + let monitor_gen = MONITOR_GEN.fetch_add(1, Ordering::SeqCst) + 1; 385 388 let rt = get_runtime(); 386 389 rt.spawn(async move { 387 - let mut last_path = String::new(); 390 + let mut last_current_path = String::new(); 391 + 388 392 loop { 389 - tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; 393 + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; 390 394 391 - // Exit if a newer monitor was started (e.g. renderer changed). 392 - if MONITOR_GEN.load(Ordering::SeqCst) != gen { 395 + if MONITOR_GEN.load(Ordering::SeqCst) != monitor_gen { 393 396 return; 394 397 } 395 - 396 398 if !*RENDERER_PLAYING.lock().unwrap() { 397 399 return; 398 400 } 399 401 400 - let track = rockbox_sys::playback::current_track(); 401 - let current_path = track.as_ref().map(|t| t.path.clone()).unwrap_or_default(); 402 + let sample_rate = CONFIG.lock().unwrap().sample_rate; 403 + let local_ip = get_local_ip(); 402 404 403 - if current_path.is_empty() || current_path == last_path { 404 - continue; 405 - } 406 - last_path = current_path.clone(); 405 + let current_track = rockbox_sys::playback::current_track(); 406 + let current_path = current_track 407 + .as_ref() 408 + .map(|t| t.path.clone()) 409 + .unwrap_or_default(); 407 410 408 - let local_ip = get_local_ip(); 409 - let stream_url = format!("http://{}:{}/stream.wav", local_ip, port); 410 - let sample_rate = CONFIG.lock().unwrap().sample_rate; 411 - let album_art_url = get_album_art_url(&current_path, local_ip).await; 411 + if !current_path.is_empty() && current_path != last_current_path { 412 + let is_first = last_current_path.is_empty(); 413 + last_current_path = current_path.clone(); 414 + 415 + // Send SetAVTransportURI+Play on every track detection — including the 416 + // first one — so the renderer always has up-to-date metadata and the 417 + // stream URL is always correct. For the very first track this also 418 + // corrects any stale metadata from the initial async play call. 419 + if !is_first { 420 + tracing::info!( 421 + "UPnP monitor: track changed → «{}»", 422 + current_track 423 + .as_ref() 424 + .map(|t| t.title.as_str()) 425 + .unwrap_or("?") 426 + ); 427 + } else { 428 + tracing::info!( 429 + "UPnP monitor: first track «{}»", 430 + current_track 431 + .as_ref() 432 + .map(|t| t.title.as_str()) 433 + .unwrap_or("?") 434 + ); 435 + } 412 436 413 - if let Err(e) = avtransport_play( 414 - &renderer_url, 415 - &stream_url, 416 - track.as_ref(), 417 - sample_rate, 418 - false, 419 - album_art_url.as_deref(), 420 - ) 421 - .await 422 - { 423 - tracing::warn!("UPnP track monitor: failed to update metadata: {}", e); 437 + let stream_url = format!("http://{}:{}/stream.wav", local_ip, port); 438 + let art = get_album_art_url(&current_path, local_ip).await; 439 + if let Err(e) = avtransport_play( 440 + &renderer_url, 441 + &stream_url, 442 + current_track.as_ref(), 443 + sample_rate, 444 + // Only send Play on a real track change (not the first detection) 445 + // because the renderer is already playing from the initial start. 446 + // For the first track, re-sending Play causes an audible blip. 447 + !is_first, 448 + art.as_deref(), 449 + ) 450 + .await 451 + { 452 + tracing::warn!("UPnP: SetAVTransportURI failed: {e}"); 453 + } 424 454 } 425 455 } 426 456 }); ··· 448 478 449 479 let metadata = build_didl_metadata(track, stream_url, sample_rate, album_art_url); 450 480 let metadata_escaped = xml_escape(&metadata); 451 - tracing::info!("UPnP AVTransport: setting URI to {stream_url} with metadata:\n{metadata}"); 481 + tracing::info!("UPnP AVTransport: setting URI to {stream_url}"); 452 482 453 483 let set_uri_body = format!( 454 484 r#"<?xml version="1.0" encoding="utf-8"?>
+1 -3
crates/upnp/src/pcm_server.rs
··· 358 358 359 359 tracing::info!( 360 360 "upnp/pcm: streaming WAV{} to {peer}", 361 - if req.wants_icy { " (ICY metadata)" } else { "" } 361 + if req.wants_icy { " (ICY)" } else { "" } 362 362 ); 363 363 364 364 let mut rx = buf.subscribe(); 365 365 366 366 if req.wants_icy { 367 367 let art_url = art_base_url(local_ip, port); 368 - // The WAV header bytes already occupy the start of the body, so the first 369 - // ICY boundary arrives after (ICY_METAINT - 44) bytes of PCM data. 370 368 let mut bytes_since_meta: usize = wav_hdr.len(); 371 369 let mut last_track_key = String::new(); 372 370