@lox-audioserver/node-librespot 0.4.0 → 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
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/lib.rs
CHANGED
|
@@ -513,6 +513,11 @@ impl Sink for ChannelSink {
|
|
|
513
513
|
}
|
|
514
514
|
|
|
515
515
|
fn stop(&mut self) -> SinkResult<()> {
|
|
516
|
+
// Reset pacing so the next start() (e.g. after Connect pause) is paced from now,
|
|
517
|
+
// not from the original stream_start. Without this the post-resume catch-up logic
|
|
518
|
+
// forwards minutes of decoded PCM at full speed.
|
|
519
|
+
self.start = None;
|
|
520
|
+
self.expected_elapsed = Duration::from_millis(0);
|
|
516
521
|
Ok(())
|
|
517
522
|
}
|
|
518
523
|
|
|
@@ -606,10 +611,20 @@ impl Sink for ChannelSink {
|
|
|
606
611
|
if self.tx.try_send(bytes).is_err() {
|
|
607
612
|
// Drop chunk if JS side is backpressured to avoid blocking the player thread.
|
|
608
613
|
}
|
|
609
|
-
let start = self.start.get_or_insert_with(Instant::now);
|
|
610
|
-
self.expected_elapsed += duration;
|
|
611
|
-
let target = *start + self.expected_elapsed;
|
|
612
614
|
let now = Instant::now();
|
|
615
|
+
// If wall-clock has drifted far ahead of our expected timeline (a pause that didn't
|
|
616
|
+
// route through stop()/start(), a decoder stall, etc.), rebase pacing onto `now`.
|
|
617
|
+
// Otherwise `sleep_dur` saturates to 0 for the duration of the gap and the sink
|
|
618
|
+
// dumps the catch-up backlog at full speed downstream.
|
|
619
|
+
if let Some(start) = self.start {
|
|
620
|
+
if now > start + self.expected_elapsed + Duration::from_millis(200) {
|
|
621
|
+
self.start = None;
|
|
622
|
+
self.expected_elapsed = Duration::from_millis(0);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
let start = *self.start.get_or_insert(now);
|
|
626
|
+
self.expected_elapsed += duration;
|
|
627
|
+
let target = start + self.expected_elapsed;
|
|
613
628
|
let sleep_dur = target.saturating_duration_since(now);
|
|
614
629
|
if !sleep_dur.is_zero() {
|
|
615
630
|
sleep(sleep_dur);
|
|
@@ -1850,12 +1865,21 @@ fn start_connect_device_inner(
|
|
|
1850
1865
|
let player = Player::new(player_config, session.clone(), volume_getter, sink_builder);
|
|
1851
1866
|
// Clone log_tsfn before it is moved into the player-event spawn below.
|
|
1852
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));
|
|
1853
1876
|
// Forward player events to JS if requested.
|
|
1854
1877
|
if let Some(tsfn_ev) = event_tsfn.clone() {
|
|
1855
1878
|
let mut ev_rx = player.get_player_event_channel();
|
|
1856
1879
|
let session_for_events = session.clone();
|
|
1857
1880
|
let device_id_for_events = device_id.clone();
|
|
1858
1881
|
let session_id_for_events = session_id.clone();
|
|
1882
|
+
let healthy_seq_for_events = healthy_seq.clone();
|
|
1859
1883
|
let last_pcm_for_health = last_pcm_at.clone();
|
|
1860
1884
|
let stop_flag_for_health = stop_flag_for_block.clone();
|
|
1861
1885
|
let device_id_for_health = device_id.clone();
|
|
@@ -2235,6 +2259,13 @@ fn start_connect_device_inner(
|
|
|
2235
2259
|
_ => {}
|
|
2236
2260
|
}
|
|
2237
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
|
+
|
|
2238
2269
|
let _ = tsfn_ev.call(payload, ThreadsafeFunctionCallMode::NonBlocking);
|
|
2239
2270
|
}
|
|
2240
2271
|
});
|
|
@@ -2250,6 +2281,16 @@ fn start_connect_device_inner(
|
|
|
2250
2281
|
let stop_flag_for_logs = stop_flag_for_block.clone();
|
|
2251
2282
|
let device_id_for_logs = device_id.clone();
|
|
2252
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;
|
|
2253
2294
|
runtime().spawn(async move {
|
|
2254
2295
|
while let Some(event) = log_rx.recv().await {
|
|
2255
2296
|
if stop_flag_for_logs.load(Ordering::Acquire) {
|
|
@@ -2307,7 +2348,28 @@ fn start_connect_device_inner(
|
|
|
2307
2348
|
metric_message: None,
|
|
2308
2349
|
credentials_json: None,
|
|
2309
2350
|
};
|
|
2310
|
-
|
|
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
|
+
});
|
|
2311
2373
|
}
|
|
2312
2374
|
});
|
|
2313
2375
|
}
|