Listen to music together with friends using Cider. One person hosts a room, others join, and everyone's music stays in sync.
- P2P Sync - No server required, direct peer-to-peer connection via libp2p
- Cross-Platform - Native apps for macOS (SwiftUI) and Windows (WinUI 3)
- Real-time - Sub-second synchronization
Why no Linux? Each app is built with native UI frameworks (SwiftUI, WinUI 3), and I don't use Linux personally. PRs welcome!
- Everyone needs Cider running with an Apple Music subscription
- One person creates a room and shares the 8-character code
- Others join with the code
- The host controls playback, everyone stays in sync
- Rust (1.70+)
- Xcode 26+ (for macOS)
- Visual Studio 2026 with .NET 10 and Windows App SDK (for Windows)
# Build Rust library and generate Swift bindings
make macos
# Open in Xcode
make xcode# Build Rust library and generate C# bindings
make windows
# Open in Visual Studio
start apps/windows/CiderTogether/CiderTogether.slnAfter making changes to cider-core/:
# Rebuild and copy to both platforms
make allcider-listen-together/
├── cider-core/ # Rust core library
│ ├── src/
│ │ ├── cider/ # Cider API client
│ │ ├── network/ # libp2p P2P networking
│ │ ├── sync/ # Sync protocol
│ │ └── ffi/ # uniffi bindings
├── apps/
│ ├── macos/ # SwiftUI app
│ └── windows/ # WinUI 3 app
└── Makefile # Build commands
This app uses Cider's REST API on localhost:10767. Make sure to:
- Enable the API in Cider: Settings → Connectivity → Manage External Application Access
- Generate an API token (or disable authentication for local use)
The CiderBehaviour struct composes 6 libp2p protocols:
Architecture note: This is a WebRTC-style architecture using libp2p primitives. The signaling layer (ntfy.sh) exchanges relay addresses, the relay server enables NAT traversal, and DCUtR performs hole punching for direct connections.
All messages are JSON-serialized and sent via Gossipsub to topic cider-room-{code}:
pub enum SyncMessage {
// Room Management
RoomState { room_code, host_peer_id, participants, current_track, playback },
JoinRequest { display_name },
JoinResponse { accepted, room_code, reason },
ParticipantJoined(Participant),
ParticipantLeft { peer_id },
TransferHost { new_host_peer_id },
// Playback (host → listeners)
Play { track: TrackInfo, position_ms, timestamp_ms },
Pause { position_ms, timestamp_ms },
Seek { position_ms, timestamp_ms },
TrackChange { track: TrackInfo, position_ms, timestamp_ms },
// Clock Sync (RTT measurement)
Ping { sent_at_ms },
Pong { ping_sent_at_ms, received_at_ms },
// Periodic
Heartbeat { track_id, playback: PlaybackInfo },
}Listeners use an adaptive seek calibrator (EMA-based) that learns the optimal offset:
The calibrator starts at 500ms offset and converges to the actual Cider buffer latency (~700ms typical).
| Layer | File | What it does |
|---|---|---|
| FFI | ffi/session.rs |
Session object exported to Swift/C# via UniFFI |
| FFI | ffi/types.rs |
SessionCallback trait for Rust→Native async events |
| Network | network/behaviour.rs |
CiderBehaviour struct + 1000-line event loop |
| Network | network/signaling.rs |
ntfy.sh HTTP client for address exchange |
| Network | network/room_code.rs |
8-char room code generation (Base32 Crockford) |
| Sync | sync/protocol.rs |
SyncMessage enum definitions |
| Cider | cider/client.rs |
Cider REST API client (localhost:10767) |
| Relay | relay-server/src/network.rs |
Dedicated relay server implementation |
| macOS | AppState.swift |
@MainActor observable state machine |