Back to journal

Engineering · 9 min read

Why 'Just Play Together' Is Harder Than It Sounds

Someone always asks: 'can't you just play the same YouTube video on two phones at the same time?' Yes. And that's almost exactly like asking whether you can cook a meal by buying groceries.

The 'Just Play It' Question

Every time I describe what Twunein does to someone outside of tech, the same question comes up: can't you just both press play at the same time? And the honest answer is: yes, in theory, but the gap between "both pressed play at roughly the same time" and "both hearing the same millisecond of the same song in real time, continuously, including after pauses and seeks and network interruptions" is the gap that contains most of the engineering work.

The analogy I use: "you can cook a meal if you have groceries" is technically true and practically useless. Yes, you have the ingredients. The question is how you combine them, in what order, at what temperature, with what timing, and how you handle it when the stove runs hotter than expected or one ingredient takes longer than the recipe said. The gap between having ingredients and producing a meal is exactly where cooking lives.

Similarly, two people having the same audio file available on their devices is the starting condition. Keeping them in the same millisecond of that file, continuously, across variable network conditions, supporting pause and seek and mid-session join, while the network jitters and device clocks drift and one person's Bluetooth headset briefly disconnects — that's where the engineering lives. The fact that the starting condition seems simple makes the gap to the actual problem harder to appreciate.

The Network Jitter Problem

I wrote separately about jitter and latency in the sync engineering piece, but it's worth coming at it from a different angle here — the angle of "what does jitter actually do to the experience, specifically."

Imagine you're coordinating two people playing the same song. You've established a shared reference point: both devices agree that the song started at a specific server timestamp. Each device calculates its current position in the song by comparing the current server time to the start time. This works perfectly when both devices can read the server time with consistent, known latency.

Now introduce jitter. Device A reads the server time in 30ms. Device B reads the server time in 80ms. Device A's position calculation is based on a time that's 30ms old. Device B's is based on a time that's 80ms old. They're already 50ms apart. Now imagine this happens continuously — the latency varies from read to read, sometimes favouring one device, sometimes the other. The sync drifts. It doesn't drift steadily in one direction (which would be easy to correct for) — it oscillates, requiring constant adjustment.

The corrections themselves introduce their own artefacts. Jumping the playback position to correct drift, even by a small amount, can produce audible glitches. Adjusting playback rate to gradually close a gap is smoother but requires careful management of how fast you adjust without making the audio sound strange. There's no approach that's free of tradeoffs. The question is which tradeoffs produce the best perceptual result most of the time.

Server Time vs. Device Time

The device clock problem deserves its own note because it's more severe than most people expect. Android devices use NTP synchronisation to keep their clocks accurate, but NTP correction is not continuous — it happens at intervals. Between corrections, the clock drifts. The drift rate varies by device. Some devices drift more than others, and the drift can be in either direction.

For most apps, clock drift of a few hundred milliseconds over the course of a day is invisible. For a sync app, it's directly visible as a growing offset between the two clients' positions. If one device's clock runs 50ms fast over the course of an hour, and the sync is anchored to device clocks, the two clients drift 50ms apart over that hour. At 50ms, most users will feel something is slightly off, even if they can't identify what it is.

The solution is to use server time as the authoritative clock — a timestamp from a server whose time is well-maintained and consistently available to both clients. But server time has its own latency: the time it takes to read the server clock is itself variable and adds to the uncertainty. The sync mechanism has to account for this latency, estimate it, and correct for it continuously. This is the kind of problem that looks simple on a whiteboard and is genuinely tricky in a distributed system under real-world network conditions.

The Edge Cases That Hurt Most

Normal playback synchronisation is the easy case. The hard cases are the ones that occur when something changes in the middle of a session — a seek, a pause, a new user joining an in-progress session, a network interruption followed by reconnection.

Seeking is particularly interesting. When one user seeks to a different position in the song, both devices need to move to that position simultaneously. But the seek request goes through the network — one device tells the shared state "we're now at position X," the other device reads that state update and adjusts. The time between the first device's seek and the second device's adjustment is the time for a Firestore round-trip and the client-side drift correction that follows. On a good network, this is very fast. On a poor network, it introduces a visible transition period where the two devices are at different positions. How you handle the UI during this transition — what you show the user, how you communicate that sync is being re-established — is as important as how you handle the sync itself.

Joining a session mid-stream is the case that requires the most care. If Person A has been listening for three minutes and Person B joins, Person B needs to jump to the correct position immediately — not start from the beginning, not start a few seconds behind. That requires knowing exactly where Person A is in the song at the moment of join, fetching that information with minimal latency, buffering enough of the audio to start playing from that position without a delay, and then maintaining sync from that point forward. Each of those steps has failure modes that need handling gracefully.

Buffering vs. Streaming Tradeoffs

Audio content can be delivered to a device in two broad ways: buffered (download ahead, then play from local buffer) or streamed (receive and play continuously). Both approaches create different sync challenges.

Buffering ahead reduces the risk of playback interruption when the network quality temporarily degrades — you have a reserve of audio that can play through a brief connection drop. But buffering ahead means the audio delivery and the playback position are decoupled: the audio that plays at minute 3 was delivered to the device at minute 1, and any sync mechanism built on delivery timing is useless. You have to sync based on playback position, not delivery time.

Streaming is more sensitive to network quality — a dropout in the stream produces an audible glitch or a pause — but the tight coupling between delivery and playback makes certain sync approaches more tractable. The sync mechanism can respond to delivery events rather than needing to maintain a separate position estimate.

In practice, most music players use adaptive buffering — buffering enough ahead to handle normal network variation, with the buffer size adjusting based on observed network quality. For sync purposes, this means the sync mechanism needs to work with buffered content, using playback position as the reference rather than delivery time.

Why Approximate Isn't Enough for Music

The case for precision sync in music specifically — not just "approximately together" — comes down to what the shared listening experience is trying to produce. If two people are in the same song at the same second, there's a real, documentable neurological phenomenon happening: neural entrainment to a shared rhythm. The precision of the sync is what makes that phenomenon possible.

An approximation — two people who are "pretty much" in the same place in the song — doesn't produce the same effect. They're hearing similar things but not the same thing at the same time. The beat doesn't land at the same moment. The chorus doesn't arrive together. The shared moment that the product is trying to create doesn't exist at approximate sync — it only exists at precise sync.

This is why the engineering investment in precision sync is worth making even though approximate sync would be easier, more robust in poor network conditions, and harder for users to distinguish in casual testing. The thing we're trying to build has a specific prerequisite: precision. Everything else — the architecture choices, the edge case handling, the correction mechanisms — is in service of that prerequisite.

Listen together, wherever you are.

Twunein keeps both of you in the same second of the same song.

Our Network