Build a Thread-Safe Video Playback Loop at 25 FPS

Quick Overview

Design a thread-safe video playback loop that targets 25 FPS despite variable frame reads. Address bounded buffering, startup races, method-level locking, monotonic scheduling, underruns, end-of-stream handling, shutdown, and duplicate play clicks.

Build a Thread-Safe Video Playback Loop at 25 FPS

Company: Nuro

Role: Software Engineer

Category: Software Engineering Fundamentals

Difficulty: hard

Interview Round: Technical Screen

## Scenario A `VideoPlayer` exposes two existing operations: - `Read()` returns the next `Frame`, or an end-of-stream result when no frames remain. - `Render(frame)` displays one frame. Implement the behavior behind `on_play_click()` so that one video plays from beginning to end at 25 frames per second. Reading can be slower or more variable than rendering, so the design may use a background producer and a bounded frame queue. `Read()` and `Render()` are not thread-safe: no operation may be invoked concurrently with another invocation of the same operation, and the design must state whether the underlying player also requires one shared lock across both methods. Present clear pseudocode and explain the synchronization, timing, startup, end-of-stream, and shutdown behavior. A general-purpose operating system cannot guarantee hard real-time deadlines, so interpret 25 FPS as a target presentation schedule with one frame due every 40 milliseconds. Explain how your loop avoids accumulating timing drift and what it does when a frame is not ready by its deadline. ### Constraints & Assumptions - `on_play_click()` starts at most one playback session for the player. A second click while playback is active should be ignored or reported, not start a competing reader or renderer. - Startup is atomic from the caller's perspective: workers must not begin playback until the session is fully published as `PLAYING`. If either worker cannot be started, cancel and join any worker that did start, then restore `IDLE`. - Exactly one producer owns calls to `Read()` and exactly one playback thread owns calls to `Render()`. - The queue is bounded so a long video cannot consume unbounded memory. - Use a monotonic clock for deadlines; wall-clock adjustments must not change playback pacing. - The first render deadline is established after startup buffering, not before the first frame exists. - End-of-stream must be distinguishable from a temporary empty queue. - `EOF`, `FAILED`, and `STOPPED` are terminal producer states. Once the queue is empty, the consumer must finish rather than repeatedly waking on an already-terminal state. - Only the currently active session may return the player to `IDLE`; a finishing or failed older session must never overwrite the state of another session. - If the player documentation says `Read()` and `Render()` cannot overlap each other, protect both calls with one player-operation mutex. If it only says each method is individually non-thread-safe, single ownership already satisfies that requirement. ### Clarifying Questions to Ask - Does 25 FPS mean evenly spaced presentation deadlines, or merely an average throughput of 25 frames per second? - When reading falls behind, should playback wait and preserve every frame, or drop late frames to preserve media time? - Is a small startup buffer allowed, and how many frames may the bounded queue hold? - Can the user stop or restart playback, and must `on_play_click()` return immediately to a UI event loop? - Are `Read()` and `Render()` unsafe only against concurrent calls to themselves, or unsafe against each other because they share player state? - How are read errors and end-of-stream represented? - What should the caller observe if one worker thread starts but creation of the other worker fails? ```hint Schedule against an absolute origin After the first frame is ready, compute deadline `start + frame_index * 40 ms`. Sleeping for 40 ms after each render adds the render and wake-up delay to every later interval. ``` ```hint Give an empty queue two meanings The consumer needs both the queue state and a producer state such as `PENDING`, `RUNNING`, `EOF`, `FAILED`, or `STOPPED`. An empty queue is temporary only while production can still continue. ``` ```hint Keep workers behind a start gate Reserve the session first, create both workers while they wait, publish `PLAYING`, and only then release them. This prevents a short or failed playback from setting `IDLE` before startup later writes `PLAYING`. ``` ### What a Strong Answer Covers - A bounded producer-consumer queue guarded by one mutex and coordinated with a condition variable, including backpressure when the queue is full. - Single ownership of `Read()` and `Render()`, plus a precise lock policy for the stronger case where the two methods cannot overlap each other. - Absolute monotonic deadlines spaced by 40 milliseconds, with sleep occurring before the corresponding render rather than an unconditional sleep after it. - An explicit underrun policy. Waiting preserves all frames but pauses the schedule; dropping catches up to media time but sacrifices frames. Either is acceptable when justified against the clarified requirement. - Separate handling for temporary queue emptiness, end-of-stream, read failure, `STOPPED`, stop requests, and duplicate play clicks, with no terminal-state spin. - Race-safe startup and cleanup: workers wait behind a start gate, partial thread-start failure cancels and joins what started, and only the matching active session resets the player to `IDLE`. ### Follow-up Questions 1. How would you add pause and resume without rendering a burst of overdue frames after resume? 2. If `Render()` occasionally takes longer than 40 milliseconds, how would the two underrun policies behave differently? 3. Where would audio synchronization fit if audio, rather than the first video frame, became the playback clock? 4. How would you test pacing without making the test suite wait for an entire real-time video?

Quick Answer: Design a thread-safe video playback loop that targets 25 FPS despite variable frame reads. Address bounded buffering, startup races, method-level locking, monotonic scheduling, underruns, end-of-stream handling, shutdown, and duplicate play clicks.

|Home/Software Engineering Fundamentals/Nuro
Nuro logo
Nuro
Aug 13, 2026
hardSoftware EngineerTechnical ScreenSoftware Engineering Fundamentals
2
0

Scenario

A VideoPlayer exposes two existing operations:

  • Read() returns the next Frame , or an end-of-stream result when no frames remain.
  • Render(frame) displays one frame.

Implement the behavior behind on_play_click() so that one video plays from beginning to end at 25 frames per second. Reading can be slower or more variable than rendering, so the design may use a background producer and a bounded frame queue. Read() and Render() are not thread-safe: no operation may be invoked concurrently with another invocation of the same operation, and the design must state whether the underlying player also requires one shared lock across both methods.

Present clear pseudocode and explain the synchronization, timing, startup, end-of-stream, and shutdown behavior. A general-purpose operating system cannot guarantee hard real-time deadlines, so interpret 25 FPS as a target presentation schedule with one frame due every 40 milliseconds. Explain how your loop avoids accumulating timing drift and what it does when a frame is not ready by its deadline.

Constraints & Assumptions

  • on_play_click() starts at most one playback session for the player. A second click while playback is active should be ignored or reported, not start a competing reader or renderer.
  • Startup is atomic from the caller's perspective: workers must not begin playback until the session is fully published as PLAYING . If either worker cannot be started, cancel and join any worker that did start, then restore IDLE .
  • Exactly one producer owns calls to Read() and exactly one playback thread owns calls to Render() .
  • The queue is bounded so a long video cannot consume unbounded memory.
  • Use a monotonic clock for deadlines; wall-clock adjustments must not change playback pacing.
  • The first render deadline is established after startup buffering, not before the first frame exists.
  • End-of-stream must be distinguishable from a temporary empty queue.
  • EOF , FAILED , and STOPPED are terminal producer states. Once the queue is empty, the consumer must finish rather than repeatedly waking on an already-terminal state.
  • Only the currently active session may return the player to IDLE ; a finishing or failed older session must never overwrite the state of another session.
  • If the player documentation says Read() and Render() cannot overlap each other, protect both calls with one player-operation mutex. If it only says each method is individually non-thread-safe, single ownership already satisfies that requirement.

Clarifying Questions to Ask Guidance

  • Does 25 FPS mean evenly spaced presentation deadlines, or merely an average throughput of 25 frames per second?
  • When reading falls behind, should playback wait and preserve every frame, or drop late frames to preserve media time?
  • Is a small startup buffer allowed, and how many frames may the bounded queue hold?
  • Can the user stop or restart playback, and must on_play_click() return immediately to a UI event loop?
  • Are Read() and Render() unsafe only against concurrent calls to themselves, or unsafe against each other because they share player state?
  • How are read errors and end-of-stream represented?
  • What should the caller observe if one worker thread starts but creation of the other worker fails?

What a Strong Answer Covers Guidance

  • A bounded producer-consumer queue guarded by one mutex and coordinated with a condition variable, including backpressure when the queue is full.
  • Single ownership of Read() and Render() , plus a precise lock policy for the stronger case where the two methods cannot overlap each other.
  • Absolute monotonic deadlines spaced by 40 milliseconds, with sleep occurring before the corresponding render rather than an unconditional sleep after it.
  • An explicit underrun policy. Waiting preserves all frames but pauses the schedule; dropping catches up to media time but sacrifices frames. Either is acceptable when justified against the clarified requirement.
  • Separate handling for temporary queue emptiness, end-of-stream, read failure, STOPPED , stop requests, and duplicate play clicks, with no terminal-state spin.
  • Race-safe startup and cleanup: workers wait behind a start gate, partial thread-start failure cancels and joins what started, and only the matching active session resets the player to IDLE .

Follow-up Questions Guidance

  1. How would you add pause and resume without rendering a burst of overdue frames after resume?
  2. If Render() occasionally takes longer than 40 milliseconds, how would the two underrun policies behave differently?
  3. Where would audio synchronization fit if audio, rather than the first video frame, became the playback clock?
  4. How would you test pacing without making the test suite wait for an entire real-time video?
Loading comments...