Pythonic video streaming made simple. Turn messy OpenCV video loops into clean Python generators.
Working with video in Python (OpenCV) is painful. You need 40+ lines of boilerplate just to handle threading, frame drops, and cleanup properly.
import cv2
import threading
from queue import Queue, Empty
# ========== 40+ LINES OF BOILERPLATE ==========
class ThreadedCamera:
def __init__(self, src=0):
self.cap = cv2.VideoCapture(src)
self.q = Queue(maxsize=128)
self.stopped = False
def start(self):
t = threading.Thread(target=self._reader)
t.daemon = True
t.start()
return self
def _reader(self):
while not self.stopped:
ret, frame = self.cap.read()
if not ret:
self.stopped = True
break
if self.q.full():
try:
self.q.get_nowait()
except Empty:
pass
self.q.put(frame)
def read(self):
return self.q.get(timeout=1.0)
def stop(self):
self.stopped = True
self.cap.release()
# ========== FINALLY, YOUR ACTUAL CODE ==========
cam = ThreadedCamera(0)
cam.start()
try:
while True:
try:
frame = cam.read()
except Empty:
break
# Your AI inference here
results = model.predict(frame)
cv2.imshow("Detection", frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
finally:
cam.stop()
cv2.destroyAllWindows()from vision_stream_toolkit import stream
import cv2
for frame in stream(source=0):
# Your AI inference here
results = model.predict(frame)
cv2.imshow("Detection", frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
breakThat's it. 6 lines instead of 50+.
| Problem | Traditional OpenCV | Vision Stream Toolkit |
|---|---|---|
| Boilerplate code | 40+ lines for threading | 1 line: stream(source=0) |
| Frame lag | Manual queue management | Automatic buffer with smart dropping |
| Resource cleanup | try/finally everywhere |
Automatic via context manager + atexit |
| Frame skipping | Implement yourself | Built-in skip_frames parameter |
| FPS control | Manual sleep calculations | Built-in fps_limit parameter |
| Transforms | Separate processing loop | Built-in transform parameter |
| Crash safety | Resources often leak | Auto-cleanup on exit/crash |
| Code style | while True loops |
Pythonic generators |
| Stream monitoring | Build your own metrics | Built-in stats with FPS, drops, latency |
| Connection drops | Manual reconnect logic | auto_reconnect=True handles it |
| Event handling | Polling for state changes | Callbacks: on_connect, on_disconnect |
pip install vision-stream-toolkitfrom vision_stream_toolkit import stream
for frame in stream(source=0):
cv2.imshow("Webcam", frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
breakfor frame in stream(source="video.mp4", drop_frames=False):
process_frame(frame)for frame in stream(source="rtsp://192.168.1.100:554/stream"):
process_frame(frame)The camera runs in a background thread, so your slow AI model never blocks frame acquisition.
Without threading:
[Capture] → [Process 200ms] → [Capture] → [Process 200ms]
↑ Camera blocked, frames lost!
With Vision Stream Toolkit:
[Capture Thread] → [Queue] → [Your Code]
↓ ↓
Continuous Process when ready
30+ FPS Always fresh frames
Control whether to drop old frames (real-time) or keep all frames (offline processing).
# Real-time mode (default) - always get fresh frames
for frame in stream(source=0, drop_frames=True):
slow_model.predict(frame) # Old frames dropped, always current
# Offline mode - process EVERY frame
for frame in stream(source="video.mp4", drop_frames=False):
slow_model.predict(frame) # No frames lost, takes longerVisual explanation:
Camera: 30 FPS → [1][2][3][4][5][6][7][8][9][10]...
Model: 5 FPS (200ms per inference)
drop_frames=True (Real-time):
Model sees: [1]...[4]...[7]...[10] → Always fresh, frames 2,3,5,6,8,9 dropped
drop_frames=False (Offline):
Model sees: [1][2][3][4][5][6][7][8][9][10] → All frames, 6x slower than real-time
Intentionally process every Nth frame to reduce compute load.
# Process every 3rd frame (10 FPS from 30 FPS source)
for frame in stream(source=0, skip_frames=3):
expensive_model.predict(frame)
# Process every 10th frame (for thumbnails, time-lapse)
for frame in stream(source="video.mp4", skip_frames=10):
save_thumbnail(frame)Visual explanation:
Source: [1][2][3][4][5][6][7][8][9][10][11][12]...
skip_frames=1: [1][2][3][4][5][6][7][8][9][10][11][12] (all frames)
skip_frames=3: [3] [6] [9] [12] (every 3rd)
skip_frames=5: [5] [10] (every 5th)
Cap the frame rate to reduce CPU/GPU usage.
# Limit to 15 FPS (saves CPU when you don't need 30+ FPS)
for frame in stream(source=0, fps_limit=15):
process(frame)Apply transformations before frames reach your code.
def preprocess(frame):
frame = cv2.resize(frame, (640, 480))
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
return frame
for frame in stream(source=0, transform=preprocess):
# Frames already resized and converted to RGB
model.predict(frame)Access timestamps and frame numbers for analysis.
from vision_stream_toolkit import VideoStream
with VideoStream(source=0) as vs:
for frame_data in vs.frames(include_metadata=True):
print(f"Frame #{frame_data.frame_number}")
print(f"Timestamp: {frame_data.timestamp:.2f}s")
process(frame_data.frame)Resources are automatically released on:
- Normal exit
- Exceptions/crashes
- Ctrl+C (KeyboardInterrupt)
- Context manager exit
# No try/finally needed!
for frame in stream(source=0):
if some_condition:
raise Exception("Oops!") # Camera still released properlyMonitor stream performance with detailed metrics.
from vision_stream_toolkit import VideoStream
with VideoStream(source=0) as vs:
for i, frame in enumerate(vs):
if i % 100 == 0: # Print stats every 100 frames
stats = vs.stats
print(f"Capture FPS: {stats.fps_capture:.1f}")
print(f"Process FPS: {stats.fps_processing:.1f}")
print(f"Dropped: {stats.frames_dropped}")
print(f"Latency: {stats.latency_ms:.1f}ms")
if cv2.waitKey(1) & 0xFF == ord('q'):
break
# Print final statistics
vs.print_stats()Available Statistics:
| Metric | Description |
|---|---|
fps_capture |
Actual frames per second being captured |
fps_processing |
Frames per second being consumed |
frames_captured |
Total frames captured from source |
frames_processed |
Total frames yielded to user |
frames_dropped |
Frames dropped (buffer full) |
frames_skipped |
Frames skipped (skip_frames setting) |
latency_ms |
Capture-to-process latency in milliseconds |
uptime_seconds |
Total seconds stream has been running |
reconnect_count |
Number of reconnection attempts |
is_connected |
Current connection status |
Automatically reconnect when RTSP streams or cameras disconnect.
# RTSP stream with auto-reconnect
for frame in stream(
source="rtsp://192.168.1.100:554/stream",
auto_reconnect=True,
reconnect_delay=2.0, # Wait 2s between attempts
max_reconnect_attempts=10, # Try 10 times (0 = infinite)
):
process(frame)With Callbacks:
def on_connect():
print("Stream connected!")
def on_disconnect():
print("Stream disconnected! Attempting reconnect...")
def on_reconnect(attempt):
print(f"Reconnected after {attempt} attempts!")
vs = VideoStream(
source="rtsp://camera.local/stream",
auto_reconnect=True,
max_reconnect_attempts=0, # Infinite retries
on_connect=on_connect,
on_disconnect=on_disconnect,
on_reconnect=on_reconnect,
)
with vs:
for frame in vs:
process(frame)from vision_stream_toolkit import stream
for frame in stream(
source=0, # Camera index or video path
queue_size=128, # Frame buffer size
fps_limit=None, # Optional FPS cap
transform=None, # Optional transform function
drop_frames=True, # Drop old frames when buffer full
skip_frames=1, # Process every Nth frame
auto_reconnect=False, # Auto-reconnect on disconnect
reconnect_delay=1.0, # Delay between reconnect attempts
max_reconnect_attempts=5, # Max reconnect attempts (0 = infinite)
):
process(frame)| Parameter | Type | Default | Description |
|---|---|---|---|
source |
int | str |
0 |
Camera index, video file path, or RTSP URL |
queue_size |
int |
128 |
Maximum frames to buffer |
fps_limit |
float | None |
None |
Cap frame rate (None = unlimited) |
transform |
Callable | None |
None |
Function to transform each frame |
drop_frames |
bool |
True |
Drop old frames to stay real-time |
skip_frames |
int |
1 |
Process every Nth frame (1 = all) |
auto_reconnect |
bool |
False |
Auto-reconnect on disconnect |
reconnect_delay |
float |
1.0 |
Seconds between reconnect attempts |
max_reconnect_attempts |
int |
5 |
Max attempts (0 = infinite) |
from vision_stream_toolkit import VideoStream
with VideoStream(source=0) as vs:
print(f"FPS: {vs.fps}")
print(f"Size: {vs.frame_size}")
print(f"Running: {vs.is_running}")
for frame in vs:
process(frame)Methods:
| Method | Description |
|---|---|
start() |
Start the capture thread |
stop() |
Stop and release resources |
read(timeout=1.0) |
Read a single frame |
frames(include_metadata=False) |
Generator for frames |
print_stats() |
Print current statistics to console |
Properties:
| Property | Type | Description |
|---|---|---|
fps |
float |
Source video FPS |
frame_size |
tuple |
Frame dimensions (width, height) |
is_running |
bool |
Whether stream is active |
is_connected |
bool |
Whether stream is connected to source |
stats |
StreamStats |
Current performance statistics |
@dataclass
class FrameData:
frame: np.ndarray # The video frame (BGR)
timestamp: float # Seconds since stream start
frame_number: int # Sequential frame count@dataclass
class StreamStats:
fps_capture: float # Capture FPS
fps_processing: float # Processing FPS
frames_captured: int # Total captured
frames_processed: int # Total processed
frames_dropped: int # Dropped frames
frames_skipped: int # Skipped frames
latency_ms: float # Current latency
uptime_seconds: float # Stream uptime
reconnect_count: int # Reconnection count
is_connected: bool # Connection statusSpecifies where to capture video from. Accepts camera index, file path, or RTSP URL.
# Webcam (default camera)
stream(source=0)
# Secondary camera
stream(source=1)
# Video file
stream(source="/path/to/video.mp4")
stream(source="recording.avi")
# RTSP stream (IP camera)
stream(source="rtsp://192.168.1.100:554/stream")
stream(source="rtsp://admin:password@camera.local/live")
# HTTP stream
stream(source="http://camera.local/video.mjpg")Controls how many frames are buffered between capture and processing. Larger buffers handle processing spikes but use more memory.
# Small buffer (low memory, may drop frames if processing is slow)
stream(source=0, queue_size=16)
# Default buffer (balanced)
stream(source=0, queue_size=128)
# Large buffer (high memory, handles processing spikes)
stream(source=0, queue_size=512)When to adjust:
| Scenario | Recommended queue_size |
|---|---|
| Low memory device (Raspberry Pi) | 16-32 |
| Normal desktop | 128 (default) |
| Bursty processing (batch inference) | 256-512 |
| Video file processing | 64-128 |
Limits the capture frame rate to reduce CPU/GPU usage. Set to None for maximum FPS.
# No limit (capture as fast as possible)
stream(source=0, fps_limit=None)
# Limit to 30 FPS
stream(source=0, fps_limit=30)
# Limit to 15 FPS (saves CPU for slow models)
stream(source=0, fps_limit=15)
# Limit to 1 FPS (time-lapse, periodic capture)
stream(source=0, fps_limit=1)Example: Reduce CPU for lightweight tasks
# Only need 10 FPS for motion detection
for frame in stream(source=0, fps_limit=10):
if detect_motion(frame):
alert()Apply a function to every frame before it reaches your code. Runs in the capture thread for efficiency.
import cv2
# Resize frames
def resize_frame(frame):
return cv2.resize(frame, (640, 480))
for frame in stream(source=0, transform=resize_frame):
process(frame) # Already 640x480# Convert to RGB (for ML models)
def to_rgb(frame):
return cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
for frame in stream(source=0, transform=to_rgb):
model.predict(frame) # RGB format# Chain multiple transforms
def preprocess(frame):
frame = cv2.resize(frame, (224, 224)) # Resize
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # BGR to RGB
frame = frame / 255.0 # Normalize
return frame
for frame in stream(source=0, transform=preprocess):
model.predict(frame)# Crop region of interest
def crop_center(frame):
h, w = frame.shape[:2]
return frame[h//4:3*h//4, w//4:3*w//4]
for frame in stream(source=0, transform=crop_center):
process(frame) # Center 50% of frameControls what happens when the frame buffer is full (processing is slower than capture).
# Real-time mode (default): Drop old frames, always get fresh ones
stream(source=0, drop_frames=True)
# Offline mode: Keep ALL frames, block capture if buffer full
stream(source="video.mp4", drop_frames=False)Visual comparison:
Camera: 30 FPS, Model: 5 FPS
drop_frames=True:
Buffer: [new][new][new] → Always fresh, older frames discarded
Result: Real-time, but only ~17% of frames processed
drop_frames=False:
Buffer: [1][2][3]...[waiting] → Keeps all, capture waits
Result: All frames processed, but 6x slower than real-time
When to use each:
| Use Case | drop_frames |
|---|---|
| Live webcam | True (default) |
| Security camera | True |
| Robotics | True |
| Video file analysis | False |
| Dataset creation | False |
| Frame extraction | False |
Process only every Nth frame to reduce compute load. Unlike drop_frames, this is intentional and predictable.
# Process all frames (default)
stream(source=0, skip_frames=1)
# Process every 2nd frame (15 FPS from 30 FPS source)
stream(source=0, skip_frames=2)
# Process every 5th frame (6 FPS from 30 FPS source)
stream(source=0, skip_frames=5)
# Process every 30th frame (1 FPS from 30 FPS source)
stream(source=0, skip_frames=30)Example: Reduce GPU load for heavy model
# YOLOv8 is slow, only run on every 3rd frame
for frame in stream(source=0, skip_frames=3):
detections = yolo_model.predict(frame)
draw_boxes(frame, detections)Example: Create thumbnails
# Extract 1 frame per second from 30 FPS video
for i, frame in enumerate(stream(source="video.mp4", skip_frames=30, drop_frames=False)):
cv2.imwrite(f"thumbnail_{i:04d}.jpg", frame)Automatically attempt to reconnect when the stream disconnects (camera unplugged, network drop, etc.).
# No reconnection (default): Stop on disconnect
stream(source=0, auto_reconnect=False)
# Auto-reconnect: Try to reconnect on failure
stream(source="rtsp://camera/stream", auto_reconnect=True)Example: 24/7 surveillance camera
for frame in stream(
source="rtsp://192.168.1.100/stream",
auto_reconnect=True,
reconnect_delay=5.0,
max_reconnect_attempts=0, # Infinite retries
):
record_frame(frame)How long to wait before attempting to reconnect after a failure.
# Quick reconnect (local camera)
stream(source=0, auto_reconnect=True, reconnect_delay=0.5)
# Moderate delay (network camera)
stream(source="rtsp://...", auto_reconnect=True, reconnect_delay=2.0)
# Slow reconnect (remote server, rate limiting)
stream(source="rtsp://...", auto_reconnect=True, reconnect_delay=10.0)How many times to try reconnecting before giving up. Set to 0 for infinite retries.
# Try 5 times then stop (default)
stream(source=0, auto_reconnect=True, max_reconnect_attempts=5)
# Try 3 times
stream(source=0, auto_reconnect=True, max_reconnect_attempts=3)
# Never give up (24/7 systems)
stream(source="rtsp://...", auto_reconnect=True, max_reconnect_attempts=0)Function called when stream successfully connects.
def handle_connect():
print("Stream connected!")
send_notification("Camera online")
vs = VideoStream(
source="rtsp://camera/stream",
on_connect=handle_connect,
)Function called when stream disconnects.
def handle_disconnect():
print("Stream lost!")
log_event("Camera disconnected")
vs = VideoStream(
source="rtsp://camera/stream",
auto_reconnect=True,
on_disconnect=handle_disconnect,
)Function called when stream successfully reconnects. Receives the attempt number.
def handle_reconnect(attempt_number):
print(f"Reconnected after {attempt_number} attempts!")
send_alert(f"Camera back online (took {attempt_number} tries)")
vs = VideoStream(
source="rtsp://camera/stream",
auto_reconnect=True,
on_reconnect=handle_reconnect,
)import cv2
from vision_stream_toolkit import VideoStream
def preprocess(frame):
"""Resize and convert to RGB."""
frame = cv2.resize(frame, (640, 480))
return cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
def on_connect():
print("Connected to camera!")
def on_disconnect():
print("Lost connection, attempting reconnect...")
def on_reconnect(attempts):
print(f"Reconnected after {attempts} attempt(s)")
# Full-featured stream configuration
vs = VideoStream(
source="rtsp://192.168.1.100:554/stream", # RTSP camera
queue_size=64, # Moderate buffer
fps_limit=15, # Cap at 15 FPS
transform=preprocess, # Resize + RGB
drop_frames=True, # Real-time mode
skip_frames=2, # Every 2nd frame
auto_reconnect=True, # Auto-reconnect
reconnect_delay=3.0, # Wait 3s between attempts
max_reconnect_attempts=0, # Never give up
on_connect=on_connect,
on_disconnect=on_disconnect,
on_reconnect=on_reconnect,
)
with vs:
for frame in vs:
# Process frame (already resized, RGB, every 2nd frame)
results = model.predict(frame)
# Monitor performance
if vs.stats.frames_processed % 100 == 0:
print(f"FPS: {vs.stats.fps_processing:.1f}, Dropped: {vs.stats.frames_dropped}")from vision_stream_toolkit import stream
from ultralytics import YOLO
import cv2
model = YOLO("yolov8n.pt")
for frame in stream(source=0, fps_limit=30):
results = model.predict(frame, verbose=False)
annotated = results[0].plot()
cv2.imshow("YOLOv8 Detection", annotated)
if cv2.waitKey(1) & 0xFF == ord('q'):
breakfrom vision_stream_toolkit import stream
import mediapipe as mp
import cv2
face_detection = mp.solutions.face_detection.FaceDetection()
for frame in stream(source=0, skip_frames=2):
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
results = face_detection.process(rgb)
if results.detections:
for detection in results.detections:
# Draw bounding box
pass
cv2.imshow("Face Detection", frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
breakfrom vision_stream_toolkit import stream
# Process all frames from a video file
frame_count = 0
for frame in stream(source="input.mp4", drop_frames=False):
# Your processing here
frame_count += 1
print(f"Processed {frame_count} frames")from vision_stream_toolkit import VideoStream
import cv2
# Open multiple cameras
cam1 = VideoStream(source=0).start()
cam2 = VideoStream(source=1).start()
try:
while cam1.is_running and cam2.is_running:
frame1 = cam1.read()
frame2 = cam2.read()
if frame1 and frame2:
combined = cv2.hconcat([frame1.frame, frame2.frame])
cv2.imshow("Dual Camera", combined)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
finally:
cam1.stop()
cam2.stop()| Scenario | Parameters |
|---|---|
| Live webcam + fast model | stream(source=0) (defaults) |
| Live webcam + slow model | stream(source=0, skip_frames=3) |
| Video file analysis | stream(source="video.mp4", drop_frames=False) |
| Reduce CPU usage | stream(source=0, fps_limit=15) |
| Thumbnail extraction | stream(source="video.mp4", skip_frames=30, drop_frames=False) |
| Pre-resize frames | stream(source=0, transform=resize_fn) |
| RTSP security camera | stream(source="rtsp://...", auto_reconnect=True) |
| Monitor performance | VideoStream(source=0) then access vs.stats |
| 24/7 surveillance | stream(source="rtsp://...", auto_reconnect=True, max_reconnect_attempts=0) |
| Metric | Traditional OpenCV | Vision Stream Toolkit |
|---|---|---|
| Lines of code | 50+ | 6 |
| Threading | Manual | Automatic |
| Queue management | Manual | Automatic |
| Frame dropping | Manual | drop_frames=True/False |
| Frame skipping | Manual | skip_frames=N |
| FPS limiting | Manual | fps_limit=N |
| Transforms | Separate loop | transform=fn |
| Cleanup | try/finally |
Automatic |
| Crash safety | Often leaks | Always cleans up |
| Statistics | Implement yourself | Built-in stats property |
| Auto-reconnect | Manual retry logic | auto_reconnect=True |
| Event callbacks | Not available | on_connect, on_disconnect |
| Learning curve | High | Minimal |
MIT License - see LICENSE file for details.
Contributions welcome! Please open an issue or PR on GitHub.