Duet
An app for moving with a stranger. You open it, start swaying with your phone in hand, and moments later a second figure appears on screen mirroring your motion — another person, somewhere else, paired with you automatically. No lobby, no invite, no names. Just two people improvising together in real time.
The piece leans on three Starfish features:
- Pools to pair two people without a shared room name.
- WebRTC for a direct, low-latency link between them.
- Unreliable delivery so motion stays fluid instead of stuttering.
Pools — automatic pairing. A pool is a waiting area the server matches for you. Enter looking for a partner, and Starfish hands you both a private session the moment someone else is waiting too. The default auto mode pairs entrants by group size; create: true lets whoever arrives first open the pool.
await client.join("duet-lobby");
client.pool.matched$.subscribe(async ({ session, peers }) => {
await client.leave(); // leave the lobby
await client.join(session); // enter the private duet session
beginDuet(peers[0].id);
});
await client.pool.enter("duets", { groupSize: 2, create: true });WebRTC — a direct link. Once paired, the two phones connect peer-to-peer, so motion travels between them without a round trip through the server.
function beginDuet(partnerId) {
client.connectRTC(partnerId);
client.rtcPeers$?.subscribe((peers) => {
const link = peers.find((p) => p.peerId === partnerId);
if (link?.state === "connected") setStatus("dancing");
});
}Unreliable delivery — keep it fluid. Each frame of motion matters only for an instant, so it's sent best-effort: a dropped frame is invisible, but a late one would break the feeling of moving together.
onDeviceMotion((tilt) => {
client.send(partnerId, { tilt }, {
delivery: { reliability: "unreliable", preferTransport: "rtc", fallback: true },
});
});
client.messagesFrom$(partnerId).subscribe((frame) => renderPartner(frame.payload.tilt));Two strangers, matched by the room and connected directly, moving as a pair.