Design a Watch Party system where a host can create a virtual room for a movie, and other users can join the room using a unique Room ID. All users in the room must watch the movie in a synchronized manner.
The system must support real-time playback controls such as play, pause, seek, and change playback speed. When the host performs a valid playback action, all participants should observe the same updated playback state.
The media file itself is not part of this problem. Assume every client can stream the same movie independently. Your task is to design room management and synchronized playback state management.
Method Signatures
String createRoom(String roomId, String hostUserId, String movieId, long movieDurationMs, long serverTimeMs)
- Creates a new watch party room with the given
roomId, host, movie, movie duration, and server time.
- The given
roomId is used as the unique Room ID for the room.
- The host is automatically added as a participant of the room.
- The initial playback state is
PAUSED at position 0 with playback speed 1.0.
- The initial
anchorPositionMs is 0.
- The initial
anchorServerTimeMs is the given serverTimeMs.
- The room's
currentVersion starts from 1.
- If this is the first room created for the given
movieId, store the given movieDurationMs as the duration for that movie.
- If the same
movieId is used again in another room, the given movieDurationMs must be exactly the same as the previously stored duration for that movieId.
- Returns the given Room ID after successfully creating the room.
- If a room with the same
roomId already exists, returns "ERROR:ROOM_ALREADY_EXISTS".
- If the same
movieId was already used with a different movie duration, returns "ERROR:MOVIE_DURATION_MISMATCH".
String joinRoom(String roomId, String userId, long serverTimeMs)
- Adds the user to the given room.
- Returns the current playback state after the user joins.
- If a user joins late, return the computed movie position at
serverTimeMs.
- If the user has already joined, do not add the user again and return the current playback state.
- If the user has already joined, return the current playback state even if the room is already full.
- If the room does not exist, returns
"ERROR:ROOM_NOT_FOUND".
- If the room already has the maximum allowed number of participants, returns
"ERROR:ROOM_FULL".
String play(String roomId, String userId, int expectedVersion, long serverTimeMs)
- Starts or resumes playback in the room.
expectedVersion is the room version known to the caller when making this request.
- The operation succeeds only when
expectedVersion is equal to the room's current currentVersion.
- Calling
play when the room is already PLAYING is still treated as a successful host playback action if all validations pass.
- Before updating the playback status, compute the current position at
serverTimeMs, discard any decimal part, clamp it to the movie duration, and store it as the new anchorPositionMs.
- Store
serverTimeMs as the new anchorServerTimeMs.
- After a successful operation, increase
currentVersion by 1.
- Returns the updated playback state after a successful operation.
- If the room does not exist, returns
"ERROR:ROOM_NOT_FOUND".
- If the user is not part of the room, returns
"ERROR:USER_NOT_IN_ROOM".
- If the user is not the host, returns
"ERROR:UNAUTHORIZED".
- If
expectedVersion is not equal to currentVersion, returns "ERROR:VERSION_MISMATCH".
String pause(String roomId, String userId, int expectedVersion, long serverTimeMs)
- Pauses playback in the room.
expectedVersion is the room version known to the caller when making this request.
- The operation succeeds only when
expectedVersion is equal to the room's current currentVersion.
- Calling
pause when the room is already PAUSED is still treated as a successful host playback action if all validations pass.
- Before updating the playback status, compute the current position at
serverTimeMs, discard any decimal part, clamp it to the movie duration, and store it as the new anchorPositionMs.
- Store
serverTimeMs as the new anchorServerTimeMs.
- After a successful operation, increase
currentVersion by 1.
- Returns the updated playback state after a successful operation.
- If the room does not exist, returns
"ERROR:ROOM_NOT_FOUND".
- If the user is not part of the room, returns
"ERROR:USER_NOT_IN_ROOM".
- If the user is not the host, returns
"ERROR:UNAUTHORIZED".
- If
expectedVersion is not equal to currentVersion, returns "ERROR:VERSION_MISMATCH".
String seek(String roomId, String userId, int expectedVersion, long positionMs, long serverTimeMs)
- Moves playback to the given movie position.
expectedVersion is the room version known to the caller when making this request.
- The operation succeeds only when
expectedVersion is equal to the room's current currentVersion.
- The given
positionMs is stored as the new anchorPositionMs.
- Store
serverTimeMs as the new anchorServerTimeMs.
- The playback status must remain unchanged after seeking.
- After a successful operation, increase
currentVersion by 1.
- Returns the updated playback state after a successful operation.
- If the room does not exist, returns
"ERROR:ROOM_NOT_FOUND".
- If the user is not part of the room, returns
"ERROR:USER_NOT_IN_ROOM".
- If the user is not the host, returns
"ERROR:UNAUTHORIZED".
- If
expectedVersion is not equal to currentVersion, returns "ERROR:VERSION_MISMATCH".
String changePlaybackSpeed(String roomId, String userId, int expectedVersion, double playbackSpeed, long serverTimeMs)
- Changes the playback speed for the room.
expectedVersion is the room version known to the caller when making this request.
- The operation succeeds only when
expectedVersion is equal to the room's current currentVersion.
- Compute the current position at
serverTimeMs using the old playback speed.
- Discard any decimal part, clamp the computed position to the movie duration, and store it as the new
anchorPositionMs.
- Store
serverTimeMs as the new anchorServerTimeMs.
- Then update the playback speed to the given
playbackSpeed.
- The playback status must remain unchanged after changing speed.
- Changing to the same playback speed is still treated as a successful host playback action if all validations pass.
- After a successful operation, increase
currentVersion by 1.
- Returns the updated playback state after a successful operation.
- If the room does not exist, returns
"ERROR:ROOM_NOT_FOUND".
- If the user is not part of the room, returns
"ERROR:USER_NOT_IN_ROOM".
- If the user is not the host, returns
"ERROR:UNAUTHORIZED".
- If
expectedVersion is not equal to currentVersion, returns "ERROR:VERSION_MISMATCH".
String getPlaybackState(String roomId, String userId, long serverTimeMs)
- Returns the current playback state visible to the given user at the provided server time.
- If the movie is playing, the current position must be computed using elapsed server time and playback speed.
- The computed position must never be greater than the movie duration.
- If the room does not exist, returns
"ERROR:ROOM_NOT_FOUND".
- If the user is not part of the room, returns
"ERROR:USER_NOT_IN_ROOM".
Return Values
Methods return either a successful value or a failure value as a String.
Successful Room ID Return
"ROOM-1"
Successful Playback State Return
"roomId=ROOM-1,movieId=MOVIE-1,movieDurationMs=120000,status=PLAYING,positionMs=5000,playbackSpeed=1.0,version=2"
Failure Returns
"ERROR:ROOM_ALREADY_EXISTS"
"ERROR:MOVIE_DURATION_MISMATCH"
"ERROR:ROOM_NOT_FOUND"
"ERROR:ROOM_FULL"
"ERROR:USER_NOT_IN_ROOM"
"ERROR:UNAUTHORIZED"
"ERROR:VERSION_MISMATCH"
Playback State Format
Every method that returns a playback state must return it in this exact format:
"roomId=ROOM-1,movieId=MOVIE-1,movieDurationMs=120000,status=PLAYING,positionMs=5000,playbackSpeed=1.0,version=2"
status must be either PLAYING or PAUSED.
movieDurationMs is the duration stored for the movie.
positionMs is the computed movie position at the given serverTimeMs.
positionMs must never be greater than movieDurationMs.
playbackSpeed must be printed with one decimal digit.
version is the room's current currentVersion.
version starts from 1 when the room is created.
- Every successful host playback action increases
version by 1.
- Failed operations must not update the room's stored playback anchor state, playback status, playback speed, or currentVersion.
- If the room is already
PLAYING, time may still affect the computed positionMs returned by a later successful state query.
Movie Duration Rule
Each movieId has exactly one movie duration across the whole system.
- When a
movieId is used for the first time in createRoom, store the given movieDurationMs for that movie.
- After that, every new room created with the same
movieId must use the same movieDurationMs.
- If a different
movieDurationMs is provided for an already known movieId, return "ERROR:MOVIE_DURATION_MISMATCH".
- Movie duration is not changed by playback actions.
- The current playback position must never exceed the movie duration.
- If a computed position is greater than
movieDurationMs, use movieDurationMs.
- Reaching
movieDurationMs does not automatically change status from PLAYING to PAUSED.
Room Version Rule
Each room stores a currentVersion. When a room is created, its currentVersion is 1.
createRoom initializes currentVersion to 1.
joinRoom does not change currentVersion.
getPlaybackState does not change currentVersion.
- Failed operations do not change
currentVersion.
- Every successful host playback action increases
currentVersion by 1.
Host playback actions are:
play
pause
seek
changePlaybackSpeed
Use of Version
The version field is used to prevent stale playback commands from changing the room state. A host control command must include the expectedVersion that the host currently knows.
If expectedVersion is different from the room's current currentVersion, the operation must fail with "ERROR:VERSION_MISMATCH". This keeps playback updates deterministic when multiple requests arrive late or out of order.
Rules
- The server is the source of truth for playback state.
- Only the host can control playback.
- The host is also a participant of the room.
- The maximum number of participants in a room is
50, including the host.
- Participants can join and read playback state.
- Across all rooms and all method calls,
serverTimeMs is globally non-decreasing.
- When playback is
PLAYING, current position is calculated as anchorPositionMs + (serverTimeMs - anchorServerTimeMs) * playbackSpeed.
- When playback is
PAUSED, current position does not increase with time.
- If the computed position has a decimal value, use only the integer part.
- If the computed position is greater than
movieDurationMs, use movieDurationMs.
- Whenever the current position is computed before a successful host playback action, first discard any decimal part, then clamp it to the movie duration, then store that integer value as the new
anchorPositionMs.
- For
play, pause, and changePlaybackSpeed, first compute the current position at serverTimeMs, then update the state.
- For
changePlaybackSpeed, compute the current position using the old playback speed before storing the new playback speed.
- For
seek, set the position to positionMs at serverTimeMs.
- For every successful host playback action, the returned version must be
expectedVersion + 1.
- Time passing does not update
currentVersion.
- If an operation fails, return only the error string.
Method-wise Error Priority
createRoom
If multiple outcomes are possible for createRoom, apply them in this order:
- If a room with the same
roomId already exists, return "ERROR:ROOM_ALREADY_EXISTS".
- If the same
movieId was already used with a different movieDurationMs, return "ERROR:MOVIE_DURATION_MISMATCH".
- Otherwise, create the room and return the given
roomId.
joinRoom
If multiple outcomes are possible for joinRoom, apply them in this order:
- If the room does not exist, return
"ERROR:ROOM_NOT_FOUND".
- If the user is already part of the room, return the current playback state.
- If the room already has
50 participants, including the host, return "ERROR:ROOM_FULL".
- Otherwise, add the user to the room and return the current playback state.
play, pause, seek, and changePlaybackSpeed
If multiple errors are possible for a host playback action, return the first matching error in this order:
"ERROR:ROOM_NOT_FOUND"
"ERROR:USER_NOT_IN_ROOM"
"ERROR:UNAUTHORIZED"
"ERROR:VERSION_MISMATCH"
getPlaybackState
If multiple errors are possible for getPlaybackState, return the first matching error in this order:
"ERROR:ROOM_NOT_FOUND"
"ERROR:USER_NOT_IN_ROOM"
Constraints
1 ≤ hostUserId.length ≤ 100
1 ≤ userId.length ≤ 100
1 ≤ movieId.length ≤ 100
1 ≤ roomId.length ≤ 100
1 ≤ number of rooms ≤ 100,000
1 ≤ total number of method calls ≤ 100,000
1 ≤ participants per room ≤ 50
- The participant count includes the host.
1 ≤ expectedVersion ≤ 1,000,000,000
1 ≤ version ≤ 1,000,000,000
1 ≤ movieDurationMs ≤ 1,000,000,000
0 ≤ positionMs ≤ movieDurationMs for that room's movie.
0 ≤ serverTimeMs ≤ 1,000,000,000
- Across all rooms and all method calls,
serverTimeMs is globally non-decreasing.
playbackSpeed is one of 0.5, 1.0, 1.5, or 2.0.
- All inputs are valid strings or numbers according to their types.
- The final computed
positionMs will fit in a 32-bit signed integer.
Examples
The examples below are cumulative and are executed on the same system instance.
Example 1
createRoom(roomId="ROOM-1", hostUserId="hostA", movieId="MOVIE-7", movieDurationMs=120000, serverTimeMs=0)
Output: "ROOM-1"
joinRoom(roomId="ROOM-1", userId="userB", serverTimeMs=500)
Output: "roomId=ROOM-1,movieId=MOVIE-7,movieDurationMs=120000,status=PAUSED,positionMs=0,playbackSpeed=1.0,version=1"
play(roomId="ROOM-1", userId="hostA", expectedVersion=1, serverTimeMs=1000)
Output: "roomId=ROOM-1,movieId=MOVIE-7,movieDurationMs=120000,status=PLAYING,positionMs=0,playbackSpeed=1.0,version=2"
getPlaybackState(roomId="ROOM-1", userId="userB", serverTimeMs=4000)
Output: "roomId=ROOM-1,movieId=MOVIE-7,movieDurationMs=120000,status=PLAYING,positionMs=3000,playbackSpeed=1.0,version=2"
Example 2
createRoom(roomId="ROOM-2", hostUserId="hostX", movieId="FILM-9", movieDurationMs=200000, serverTimeMs=4500)
Output: "ROOM-2"
play(roomId="ROOM-2", userId="hostX", expectedVersion=1, serverTimeMs=5000)
Output: "roomId=ROOM-2,movieId=FILM-9,movieDurationMs=200000,status=PLAYING,positionMs=0,playbackSpeed=1.0,version=2"
changePlaybackSpeed(roomId="ROOM-2", userId="hostX", expectedVersion=2, playbackSpeed=2.0, serverTimeMs=7000)
Output: "roomId=ROOM-2,movieId=FILM-9,movieDurationMs=200000,status=PLAYING,positionMs=2000,playbackSpeed=2.0,version=3"
getPlaybackState(roomId="ROOM-2", userId="hostX", serverTimeMs=9000)
Output: "roomId=ROOM-2,movieId=FILM-9,movieDurationMs=200000,status=PLAYING,positionMs=6000,playbackSpeed=2.0,version=3"
Example 3
createRoom(roomId="ROOM-3", hostUserId="hostM", movieId="MOVIE-3", movieDurationMs=180000, serverTimeMs=9100)
Output: "ROOM-3"
joinRoom(roomId="ROOM-3", userId="guest1", serverTimeMs=9200)
Output: "roomId=ROOM-3,movieId=MOVIE-3,movieDurationMs=180000,status=PAUSED,positionMs=0,playbackSpeed=1.0,version=1"
seek(roomId="ROOM-3", userId="guest1", expectedVersion=1, positionMs=60000, serverTimeMs=9300)
Output: "ERROR:UNAUTHORIZED"
seek(roomId="ROOM-3", userId="hostM", expectedVersion=1, positionMs=60000, serverTimeMs=9400)
Output: "roomId=ROOM-3,movieId=MOVIE-3,movieDurationMs=180000,status=PAUSED,positionMs=60000,playbackSpeed=1.0,version=2"
Example 4
createRoom(roomId="ROOM-4", hostUserId="hostZ", movieId="MOVIE-8", movieDurationMs=90000, serverTimeMs=9500)
Output: "ROOM-4"
play(roomId="ROOM-4", userId="hostZ", expectedVersion=1, serverTimeMs=10000)
Output: "roomId=ROOM-4,movieId=MOVIE-8,movieDurationMs=90000,status=PLAYING,positionMs=0,playbackSpeed=1.0,version=2"
pause(roomId="ROOM-4", userId="hostZ", expectedVersion=1, serverTimeMs=12000)
Output: "ERROR:VERSION_MISMATCH"
pause(roomId="ROOM-4", userId="hostZ", expectedVersion=2, serverTimeMs=13000)
Output: "roomId=ROOM-4,movieId=MOVIE-8,movieDurationMs=90000,status=PAUSED,positionMs=3000,playbackSpeed=1.0,version=3"
Example 5
createRoom(roomId="ROOM-5", hostUserId="hostQ", movieId="MOVIE-10", movieDurationMs=60000, serverTimeMs=13500)
Output: "ROOM-5"
play(roomId="ROOM-5", userId="hostQ", expectedVersion=1, serverTimeMs=14000)
Output: "roomId=ROOM-5,movieId=MOVIE-10,movieDurationMs=60000,status=PLAYING,positionMs=0,playbackSpeed=1.0,version=2"
joinRoom(roomId="ROOM-5", userId="lateUser", serverTimeMs=19000)
Output: "roomId=ROOM-5,movieId=MOVIE-10,movieDurationMs=60000,status=PLAYING,positionMs=5000,playbackSpeed=1.0,version=2"
Example 6
createRoom(roomId="ROOM-5", hostUserId="anotherHost", movieId="MOVIE-99", movieDurationMs=70000, serverTimeMs=19500)
Output: "ERROR:ROOM_ALREADY_EXISTS"
Example 7
createRoom(roomId="ROOM-6", hostUserId="hostR", movieId="MOVIE-10", movieDurationMs=60000, serverTimeMs=20000)
Output: "ROOM-6"
createRoom(roomId="ROOM-7", hostUserId="hostS", movieId="MOVIE-10", movieDurationMs=70000, serverTimeMs=21000)
Output: "ERROR:MOVIE_DURATION_MISMATCH"
Example 8
createRoom(roomId="ROOM-8", hostUserId="hostEnd", movieId="SHORT-MOVIE", movieDurationMs=3000, serverTimeMs=22000)
Output: "ROOM-8"
play(roomId="ROOM-8", userId="hostEnd", expectedVersion=1, serverTimeMs=23000)
Output: "roomId=ROOM-8,movieId=SHORT-MOVIE,movieDurationMs=3000,status=PLAYING,positionMs=0,playbackSpeed=1.0,version=2"
getPlaybackState(roomId="ROOM-8", userId="hostEnd", serverTimeMs=28000)
Output: "roomId=ROOM-8,movieId=SHORT-MOVIE,movieDurationMs=3000,status=PLAYING,positionMs=3000,playbackSpeed=1.0,version=2"