@lox-audioserver/node-librespot 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lox-audioserver/node-librespot",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Node.js bindings for librespot (Spotify Connect) via N-API with prebuild support.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/lib.rs CHANGED
@@ -1865,12 +1865,21 @@ fn start_connect_device_inner(
1865
1865
  let player = Player::new(player_config, session.clone(), volume_getter, sink_builder);
1866
1866
  // Clone log_tsfn before it is moved into the player-event spawn below.
1867
1867
  let log_tsfn_for_disc = log_tsfn.clone();
1868
+ // [issue #252] Bumped by the player-event observer on signals that
1869
+ // prove audio is flowing (`playing` / `position_correction`). Read by
1870
+ // the log observer further down to suppress transient `audio_key_error`
1871
+ // events that fire while spirc is already recovering from an
1872
+ // account-side dealer-disconnect burst. A permanent audio-key fault
1873
+ // never produces a healthy event, so the error still surfaces after
1874
+ // the grace window.
1875
+ let healthy_seq = Arc::new(AtomicU64::new(0));
1868
1876
  // Forward player events to JS if requested.
1869
1877
  if let Some(tsfn_ev) = event_tsfn.clone() {
1870
1878
  let mut ev_rx = player.get_player_event_channel();
1871
1879
  let session_for_events = session.clone();
1872
1880
  let device_id_for_events = device_id.clone();
1873
1881
  let session_id_for_events = session_id.clone();
1882
+ let healthy_seq_for_events = healthy_seq.clone();
1874
1883
  let last_pcm_for_health = last_pcm_at.clone();
1875
1884
  let stop_flag_for_health = stop_flag_for_block.clone();
1876
1885
  let device_id_for_health = device_id.clone();
@@ -2250,6 +2259,13 @@ fn start_connect_device_inner(
2250
2259
  _ => {}
2251
2260
  }
2252
2261
 
2262
+ // [issue #252] Signal "audio is actually flowing" to the
2263
+ // log observer. Excludes `paused` because a paused stream
2264
+ // can't prove the dealer/session is healthy.
2265
+ if matches!(payload.r#type.as_str(), "playing" | "position_correction") {
2266
+ healthy_seq_for_events.fetch_add(1, Ordering::AcqRel);
2267
+ }
2268
+
2253
2269
  let _ = tsfn_ev.call(payload, ThreadsafeFunctionCallMode::NonBlocking);
2254
2270
  }
2255
2271
  });
@@ -2265,6 +2281,16 @@ fn start_connect_device_inner(
2265
2281
  let stop_flag_for_logs = stop_flag_for_block.clone();
2266
2282
  let device_id_for_logs = device_id.clone();
2267
2283
  let session_id_for_logs = session_id.clone();
2284
+ let healthy_seq_for_logs = healthy_seq.clone();
2285
+ // [issue #252] Grace window before surfacing audio_key_error to JS.
2286
+ // Account-side Spotify dealer-disconnect bursts cause every
2287
+ // offload=false zone to emit a single audio-key-timeout log line,
2288
+ // while spirc reconnects internally within seconds. Without this
2289
+ // delay every burst trips the per-zone cooldown in
2290
+ // SpotifyConnectInstance and produces ~5 minutes of unplayable
2291
+ // state per zone. 5s is well above typical spirc recovery time
2292
+ // and well below the cooldown window.
2293
+ const AUDIO_KEY_GRACE_MS: u64 = 5000;
2268
2294
  runtime().spawn(async move {
2269
2295
  while let Some(event) = log_rx.recv().await {
2270
2296
  if stop_flag_for_logs.load(Ordering::Acquire) {
@@ -2322,7 +2348,28 @@ fn start_connect_device_inner(
2322
2348
  metric_message: None,
2323
2349
  credentials_json: None,
2324
2350
  };
2325
- let _ = tsfn_ev.call(payload, ThreadsafeFunctionCallMode::NonBlocking);
2351
+ // [issue #252] Defer the emit by AUDIO_KEY_GRACE_MS and
2352
+ // suppress it if a healthy event arrived in the meantime
2353
+ // (spirc finished its internal reconnect). Re-arm
2354
+ // `error_sent` on suppression so a *later* fault in the
2355
+ // same session can still surface.
2356
+ let baseline = healthy_seq_for_logs.load(Ordering::Acquire);
2357
+ let healthy_seq_for_check = healthy_seq_for_logs.clone();
2358
+ let stop_flag_for_grace = stop_flag_for_logs.clone();
2359
+ let error_sent_for_reset = error_sent.clone();
2360
+ let tsfn_ev_for_grace = tsfn_ev.clone();
2361
+ runtime().spawn(async move {
2362
+ tokio::time::sleep(Duration::from_millis(AUDIO_KEY_GRACE_MS)).await;
2363
+ if stop_flag_for_grace.load(Ordering::Acquire) {
2364
+ return;
2365
+ }
2366
+ if healthy_seq_for_check.load(Ordering::Acquire) != baseline {
2367
+ error_sent_for_reset.store(false, Ordering::Release);
2368
+ return;
2369
+ }
2370
+ let _ = tsfn_ev_for_grace
2371
+ .call(payload, ThreadsafeFunctionCallMode::NonBlocking);
2372
+ });
2326
2373
  }
2327
2374
  });
2328
2375
  }