feat(music): W22 shared listening + release 0.6.0

Personal playlist on a dedicated music player (browse/play/prev/next/
seek/volume/reorder/remove, .pls/.m3u import), plus per-person shared
listening: broadcast your track over presence, peers tune in and stream
it point-to-point over the files plane. Playback is timeline-synced
(play/pause/skip/seek mirror with no drift) with gapless prefetch of the
next track and independent per-source volume per listener.

In the 3-column layout the playlist gets its own card stacked under the
chat, with a resizable divider and its own scrollbar; other layouts keep
it in the Controls panel.

Breaking wire change: gossip protocol v5 (presence gains music fields),
so 0.6.0 peers cannot share a swarm with 0.5.x. Version bumped 0.5.1 ->
0.6.0; CHANGELOG updated.

Untrusted-input handling: broadcast track name sanitized and size
cap-checked at gossip ingest, fetched bytes confirmed audio before
decode, only the descriptor rides gossip (bytes go point-to-point, one
fetch in flight).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-28 06:04:31 -04:00
co-authored by Claude Opus 4.8
parent c2e27c3367
commit bca2ccd6a4
15 changed files with 1440 additions and 28 deletions
+24
View File
@@ -645,6 +645,29 @@ impl RoomState for IrohGossipState {
let cleaned = crate::sanitize::sanitize_game_label(&g);
(!cleaned.is_empty()).then_some(cleaned)
});
// Music presence is untrusted peer data:
// the track name is display text (sanitize
// + cap like the game label) and the size
// bounds a future fetch (reject anything
// outside the attachment cap).
state.music = state.music.and_then(|mut m| {
let name = crate::sanitize::sanitize_game_label(&m.name);
if name.is_empty() || !crate::files::size_within_cap(m.size) {
return None;
}
m.name = name;
if m.next_id.is_some() {
let ok = m
.next_size
.map(crate::files::size_within_cap)
.unwrap_or(false);
if !ok {
m.next_id = None;
m.next_size = None;
}
}
Some(m)
});
// Bound an insider's advertised address set
// before we retain it / hand it to the dialer
// (Tier C F-01).
@@ -959,6 +982,7 @@ mod tests {
sharing: None,
avatar: crate::avatar::Avatar::default(),
game: None,
music: None,
}
}