Build a Thread-Safe Video Playback Loop at 25 FPS
NuroSoftware Engineer · Technical ScreenLast 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:
- 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.
- 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. - State Management: Support
play(),pause(), andstop(). Successive calls toplay()while already playing must be no-ops (idempotent). Transitions between states must be free of race conditions. - 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.
- 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?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.