Integrate shared playback coordination and release 0.3.1

This commit is contained in:
ab
2026-09-10 17:07:41 +03:00
parent 5a193d839f
commit 532d6ee1e9
11 changed files with 473 additions and 204 deletions
+135 -6
View File
@@ -1,5 +1,111 @@
use super::*;
fn empty_playback_state() -> PlaybackStateWire {
PlaybackStateWire {
queue: vec![],
queue_pos: 0,
playing: false,
paused: false,
idle_since_ms: None,
position_secs: 0.0,
volume: 80,
shuffle: false,
repeat: PlaybackRepeat::Off,
}
}
#[test]
fn coordination_ignores_legacy_commands_and_wrong_snapshot_sender() {
let sync = test_sync();
let identity = sync.ensure_identity().unwrap();
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
sync.set_event_tx(tx);
let command = PlaybackCommand::SetState {
state: empty_playback_state(),
seek: false,
};
sync.apply_playback_command(&identity.device_id, &command, None, "remote", "legacy")
.unwrap();
assert!(rx.try_recv().is_err());
let mut remote = Engine::new(
"remote".into(),
PlaybackConfig::default(),
Default::default(),
0,
);
remote.transfer("remote", 0);
remote.set_output(true, true);
remote.heartbeat(1);
let snapshot = PlaybackSnapshot {
device_id: "remote".into(),
device_name: "Remote".into(),
active: true,
updated_at_ms: now_ms(),
state: empty_playback_state(),
coordination: Some(remote.announcement()),
};
sync.apply_playback_snapshot("different-sender", snapshot.clone())
.unwrap();
assert!(
sync.with_playback_engine(|engine| engine.owner().is_none())
.unwrap()
);
sync.apply_playback_snapshot("remote", snapshot).unwrap();
assert_eq!(
sync.playback_tick(true, false).unwrap().as_deref(),
Some("remote")
);
sync.publish_playback(PlaybackSnapshot {
device_id: identity.device_id,
device_name: identity.name,
active: false,
updated_at_ms: now_ms(),
state: empty_playback_state(),
coordination: None,
});
let gossip = sync
.local_playback_snapshot()
.unwrap()
.coordination
.unwrap();
assert_eq!(gossip.claim.unwrap().owner, "remote");
}
#[test]
fn checkpoint_is_not_reused_after_changing_trusted_group() {
let sync = test_sync();
sync.claim_playback("old-group-owner").unwrap();
sync.set_group_id("new-test-group").unwrap();
assert!(
sync.with_playback_engine(|engine| engine.owner().is_none())
.unwrap()
);
}
#[test]
fn queued_command_loses_its_fence_when_another_owner_wins() {
let sync = test_sync();
let identity = sync.ensure_identity().unwrap();
sync.claim_playback(&identity.device_id).unwrap();
let stamp = sync
.with_playback_engine(|engine| engine.stamp().unwrap())
.unwrap();
sync.apply_playback_command(
&identity.device_id,
&PlaybackCommand::SetState {
state: empty_playback_state(),
seek: false,
},
Some(&stamp),
&identity.device_id,
"queued",
)
.unwrap();
assert!(sync.playback_command_is_current(&identity.device_id, &stamp));
sync.claim_playback("new-owner").unwrap();
assert!(!sync.playback_command_is_current(&identity.device_id, &stamp));
}
static NEXT_TEST_DB: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
fn test_sync() -> DeviceSync {
@@ -212,19 +318,41 @@ fn playback_command_is_targeted_and_deduplicated() {
seek: false,
};
sync.apply_playback_command("dev_other", &command, "op_other")
sync.claim_playback(&identity.device_id).unwrap();
let stamp = sync
.with_playback_engine(|engine| engine.stamp().unwrap())
.unwrap();
sync.apply_playback_command(
"dev_other",
&command,
Some(&stamp),
&identity.device_id,
"op_other",
)
.unwrap();
assert!(rx.try_recv().is_err());
sync.apply_playback_command(&identity.device_id, &command, "op_1")
.unwrap();
sync.apply_playback_command(
&identity.device_id,
&command,
Some(&stamp),
&identity.device_id,
"op_1",
)
.unwrap();
assert!(matches!(
rx.try_recv().unwrap(),
crate::app::event::AppEvent::PlaybackCommand(_)
crate::app::event::AppEvent::PlaybackCommand { .. }
));
sync.apply_playback_command(&identity.device_id, &command, "op_1")
.unwrap();
sync.apply_playback_command(
&identity.device_id,
&command,
Some(&stamp),
&identity.device_id,
"op_1",
)
.unwrap();
assert!(rx.try_recv().is_err());
}
@@ -245,6 +373,7 @@ fn playback_commands_are_caught_up_only_by_their_target_while_fresh() {
},
seek: false,
};
sync.claim_playback("dev_target").unwrap();
sync.record_playback_command("dev_target", command.clone())
.unwrap();
sync.record_playback_command("dev_other", command).unwrap();