Q4 - The Playback Engine (Keeping a Watch Party in Sync)
We believe a variation of this interview question has recently been reported by Netflix candidates. Picture a Watch Party: several people stream the same show together, and whenever someone joins late or reconnects, the app must jump them to the exact spot everyone else is at. The clean design does not track the position as it moves; it stores a single anchor and computes the rest, and that small idea is what the interviewer is looking for.
Scenario: You are building the engine behind a Watch Party, a shared session where everyone watches the same video in sync. The host can play, pause, and seek, and at any moment a client asks for the current playback position, for example when it joins or reconnects. Wall-clock time is passed in as now (a timestamp), and the video has a fixed duration.
Part 1. Implement PlaybackSession(duration) with play(now), pause(now), seek(now, position), and position(now). The session starts paused at position 0, and the position never drops below 0 or runs past duration.
Example 1 (Part 1: play, pause, seek)
PlaybackSession(100) (a 100-second video; time and position are in seconds)
play(0) start playing at wall-clock 0
position(5) -> 5 5 seconds of wall-clock have passed while playing
pause(10) freeze
position(20) -> 10 paused, so the position holds at 10
play(20) resume
position(25) -> 15 10 + 5 more seconds
seek(30, 90) jump to 90
position(45) -> 100 90 + 15 would be 105, clamped to the length, 100
💡 Store an anchor, not the moving position. A number that changes every second is impossible to keep correct without a timer. So keep two numbers instead: anchor_pos, the position at the last event, and anchor_time, the wall-clock time of that event, plus whether you are playing. The current position is then pure arithmetic: while playing, anchor_pos + (now - anchor_time); while paused, just anchor_pos. Every play, pause, or seek first freezes the current position into the anchor, then changes state.
Part 1: The Anchor Trick
Three moves: keep the anchor as the whole state, freeze it before every change, and derive the position on demand.
Step 1: The anchor is the whole state
Just three fields: anchor_pos (the position at the last event), anchor_time (when that event happened), and playing. That is enough to answer position(now) for any future now, with no background clock ticking anywhere.
after play(0): anchor_pos = 0, anchor_time = 0, playing = true
after pause(10): anchor_pos = 10, anchor_time = 10, playing = false
after seek(30,90): anchor_pos = 90, anchor_time = 30, playing = (unchanged)
Step 2: Freeze before every change
Before play, pause, or seek changes anything, roll the anchor forward to now: if you were playing, advance anchor_pos by the elapsed time (clamped to duration), then set anchor_time = now. Now the anchor holds the true current position, so flipping playing or overwriting anchor_pos is safe.
_advance(now):
if playing: anchor_pos = min(duration, anchor_pos + (now - anchor_time))
anchor_time = now
def _advance(self, now):
# freeze the true current position into the anchor as of `now`
if self.playing:
self.anchor_pos = min(self.duration, self.anchor_pos + (now - self.anchor_time))
self.anchor_time = now
Step 3: Derive the position on demand
position(now) re-applies the same elapsed-time math without changing anything: while playing, anchor_pos + (now - anchor_time), clamped to duration; while paused, just anchor_pos. Because it is read-only, clients can ask as often as they like.
paused at anchor_pos = 10: position(anything) = 10
playing, anchor (10 @ t=20): position(25) = 10 + (25 - 20) = 15
Read the position off the timeline: it climbs while playing, holds flat while paused, jumps on a seek, and clamps at the video length:
# every method O(1) time | O(1) space
class PlaybackSession:
def __init__(self, duration):
self.duration = duration
# the anchor: playback position at the last event, and the wall-clock time of that event
self.anchor_pos = 0
self.anchor_time = 0
self.playing = False
def _advance(self, now):
# freeze the true current position into the anchor as of now
if self.playing:
self.anchor_pos = min(self.duration, self.anchor_pos + (now - self.anchor_time))
self.anchor_time = now
def play(self, now):
self._advance(now)
self.playing = True
def pause(self, now):
self._advance(now)
self.playing = False
def seek(self, now, position):
self._advance(now)
self.anchor_pos = max(0, min(self.duration, position))
def position(self, now):
if self.playing:
return min(self.duration, self.anchor_pos + (now - self.anchor_time))
return self.anchor_pos
Part 2: Variable Playback Speed
Add set_speed(now, speed), so the video can play at 1x, 2x, and so on. While playing at speed s, the position advances s seconds of video per second of wall-clock.
Example 2 (Part 2: playback speed)
PlaybackSession(100)
play(0) play at the default 1x
position(10) -> 10 10 seconds in
set_speed(10, 2) switch to 2x from here on
position(20) -> 30 10 + 2 * (20 - 10) = 30
position(60) -> 100 10 + 2 * 50 = 110, clamped to the length, 100
The anchor design absorbs this with almost no change. Add a speed field, and everywhere you advanced the position by the elapsed time, advance it by speed * elapsed instead. set_speed freezes the current position first (exactly like the other events), then updates the speed, so the new rate applies only from now on. (Speeds are whole multiples here; a fractional speed like 1.5x works the same way with floating-point.)
set_speed(now, s): _advance(now) first (freeze at the old speed), then speed = s
position(now): playing ? min(duration, anchor_pos + speed * (now - anchor_time)) : anchor_pos
def set_speed(self, now, speed):
# freeze at the old speed before switching
self._advance(now)
self.speed = speed
# every method O(1) time | O(1) space
class PlaybackSession:
def __init__(self, duration):
self.duration = duration
self.anchor_pos = 0
self.anchor_time = 0
self.playing = False
# playback speed (1x by default)
self.speed = 1
def _advance(self, now):
# advance by speed * elapsed, not just elapsed
if self.playing:
self.anchor_pos = min(self.duration, self.anchor_pos + self.speed * (now - self.anchor_time))
self.anchor_time = now
def play(self, now):
self._advance(now)
self.playing = True
def pause(self, now):
self._advance(now)
self.playing = False
def seek(self, now, position):
self._advance(now)
self.anchor_pos = max(0, min(self.duration, position))
def set_speed(self, now, speed):
# freeze at the old speed, then switch
self._advance(now)
self.speed = speed
def position(self, now):
if self.playing:
return min(self.duration, self.anchor_pos + self.speed * (now - self.anchor_time))
return self.anchor_pos
Complexity Analysis
- Every method is
O(1)time.play,pause,seek, andset_speedeach do one anchor update (a subtraction, a multiply, a clamp);positiondoes the same arithmetic and returns. There are no loops and nothing that grows with how long the session has run. - Space is
O(1). The whole session is a handful of numbers and a flag, no history and no buffer, no matter how many events or queries arrive. - Why this beats a ticking counter: a background timer that increments the position every second is easy to write and hard to keep correct: it drifts, it fights with pause and seek, and it does no useful work between queries. Deriving the position from an anchor is exact, needs no timer, and answers a query for any
now, past-relative or future, in constant time. - One monotonic clock. The design assumes
nownever moves backward between calls, which is exactly what a real session clock gives you. Naming that assumption is part of a clean answer.
Final Notes
🎬 We believe the "playback engine" problem, and stateful media or session-design questions like it, have appeared frequently in recent interviews for software engineers at Netflix. The signal is whether you reach for the anchor-plus-elapsed model instead of a ticking counter, and whether you keep every operation a clean constant-time update.
- Do not store the thing that moves. The position changes every instant; storing it forces a timer and invites drift. Store the anchor (position and time at the last event) and compute the position when asked. Saying that out loud is most of the design.
- Make one helper freeze the state. Routing
play,pause,seek, andset_speedthrough a singleadvance(now)that rolls the anchor forward means each public method is two lines, and the tricky elapsed-time math lives in exactly one place. - Clamp at both ends, on purpose. Position never goes below 0 (a seek before the start) or past
duration(playing off the end). Stating those bounds, and where you enforce them, is the kind of edge handling a senior answer includes without being asked. - Speed is a one-field change, so design for it. Because the elapsed-time math lives in one helper, adding variable speed is a field and a
* speed, and building Part 1 that way is why the follow-up is almost free. - Name the clock assumption. The math relies on
nowmoving forward. Call that out, and mention that a real deployment feeds it a monotonic clock.