Katkat

Build a Thread-Safe Video Playback Loop at 25 FPS

NuroSoftware Engineer · Technical Screen

Last updated

Design and implement a thread-safe video playback engine that decodes and renders frames at a steady 25 frames per second using bounded buffering and precise monotonic scheduling.

Problem Statement

In autonomous vehicle testing and simulation playback, visual logs must be rendered at an exact real-time cadence (e.g., 25 frames per second) even though frame decoding times fluctuate significantly due to I/O latency or compression artifacts.

Implement a thread-safe VideoPlayer class that orchestrates an asynchronous decoder producer and a scheduled render consumer. Your implementation must satisfy the following requirements:

  1. Cadence & Timing: Deliver frames to the display sink at 25 FPS (40 ms per frame) using a monotonic clock. Compensation must prevent cumulative clock drift when individual frames take variable time to render.
  2. Bounded Buffering: Decoded frames must be buffered in a thread-safe queue with a configurable capacity max_buffer_size. The decoder thread must block if the buffer is full.
  3. State Management: Support play(), pause(), and stop(). Successive calls to play() while already playing must be no-ops (idempotent). Transitions between states must be free of race conditions.
  4. Buffer Underruns: If the render loop needs a frame but the buffer is empty, handle the underrun gracefully (e.g., wait without spinning 100% CPU, and log/count the dropped frame or underrun event) before resuming on the next target deadline.
  5. End-of-Stream (EOS) & Teardown: Upon reaching the end of the video stream or when stop() is called, all background threads must terminate cleanly without deadlock.

Class Interface

from typing import Optional

class VideoFrame:
    pts_ms: int  # Presentation timestamp in milliseconds
    data: bytes

class FrameDecoder:
    def read_frame(self) -> Optional[VideoFrame]:
        """Reads the next frame. Returns None on End-of-Stream (EOS)."""
        pass

class DisplaySink:
    def render(self, frame: VideoFrame) -> None:
        """Renders a frame to the screen."""
        pass

class VideoPlayer:
    def __init__(self, decoder: FrameDecoder, sink: DisplaySink, target_fps: int = 25, max_buffer_size: int = 10):
        pass

    def play(self) -> None:
        """Starts or resumes playback."""
        pass

    def pause(self) -> None:
        """Pauses playback without destroying thread state or dropping buffer."""
        pass

    def stop(self) -> None:
        """Stops playback, flushes buffers, and terminates worker threads."""
        pass

Constraints & Expectations

  • Decoding time per frame varies from 5 ms to 80 ms.
  • Do not use busy-waiting (while True: pass) anywhere.
  • Frame jitter should remain within +/- 2 ms of target intervals under normal buffer conditions.
  • Clean shutdown must complete within 200 ms of stop() being called.

Hints

Hint 1

Use a dedicated producer thread for decoding into a bounded thread-safe queue (`threading.Condition` or `queue.Queue`) and a separate consumer thread for rendering.

Hint 2

To eliminate timing drift in the consumer, calculate each frame's target deadline by adding increments of `1.0 / target_fps` to a base monotonic reference time rather than using relative sleep durations.

Hint 3

Guard state transitions with a mutex, and use a sentinel value (like `None` or a custom EOS token) in the queue alongside an explicit stop event to unblock waiting threads immediately upon shutdown.

Follow-up questions

  • How would you handle audio-video synchronization (A/V sync) if an audio sink drives the master clock?
  • If the decoder falls persistently behind real-time (sustained underrun), what frame-dropping or catch-up strategies would you implement?
  • How would you design the loop if users need reverse playback or scrub-seeking at arbitrary timestamps?