The most reliable way to build a Raspberry Pi stream in 2026 is using the picamera2 library on Raspberry Pi OS Bookworm, serving an MJPEG feed via a lightweight Flask server. This guide targets the Raspberry Pi 5 (4GB) as the primary board, though the code and wiring are fully backward-compatible with the Raspberry Pi 4 Model B. We will bypass outdated legacy stacks and use the modern libcamera framework to pull 640x480 frames directly from the IMX708 sensor, while simultaneously driving a status LED and a PWM servo for pan/tilt tracking.
Hardware Spec Sheet & Pin Mapping
Before writing code, you need the exact hardware. The Raspberry Pi 5 introduced a smaller, denser CSI connector, which trips up many builders migrating from the Pi 4.
| Component | Exact Variant / Model | Interface / Pin | Notes |
|---|---|---|---|
| Compute Board | Raspberry Pi 5 (4GB) | N/A | Requires active cooling for sustained video encoding |
| Camera Module | Camera Module 3 (IMX708) | CSI0 | Supports PDAF and HDR; requires 22-pin cable for Pi 5 |
| CSI Ribbon Cable | 22-pin to 15-pin FFC | CSI0 | Pi 5 uses 22-pin (0.5mm pitch); Pi 4 uses 15-pin (1mm pitch) |
| Power Supply | 27W USB-C PD (5V/5A) | USB-C PWR | Camera + Servo will brownout on standard 5V/3A supplies |
| Status LED | 5mm Red LED + 330Ω Resistor | GPIO 17 (Pin 11) | Indicates stream active status |
| Pan Servo | SG90 Micro Servo | GPIO 18 (Pin 12) | Hardware PWM0; requires external 5V rail for high torque |
Step-by-Step Build & Compilable Code
- Flash the OS: Use Raspberry Pi Imager to install Raspberry Pi OS (64-bit, Bookworm). Do not use Bullseye; the
picamera2library relies on Bookworm's Wayland and libcamera integration. - Install Dependencies: Open a terminal and install the required Python packages. We use
gpiozerowith thepigpiobackend for jitter-free servo PWM.sudo apt update sudo apt install python3-picamera2 python3-flask python3-gpiozero pigpio sudo systemctl enable pigpiod sudo systemctl start pigpiod - Wire the Hardware: Connect the 22-pin end of the CSI cable to the Pi 5 (contacts facing the board edge). Connect the LED anode to GPIO 17 via the 330Ω resistor, and the cathode to GND. Connect the servo signal wire to GPIO 18.
- Deploy the Stream Server: Save the following Python script as
stream_server.py. This code includes full error handling, pin definitions, and a threaded MJPEG generator.
import io
import logging
import time
from flask import Flask, Response
from picamera2 import Picamera2
from gpiozero import LED, AngularServo
from gpiozero.pins.pigpio import PiGPIOFactory
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
app = Flask(__name__)
# Pin Definitions
STATUS_LED_PIN = 17
PAN_SERVO_PIN = 18
camera = None
led = None
servo = None
def init_hardware():
global camera, led, servo
try:
# Initialize GPIO via pigpio daemon for stable PWM
factory = PiGPIOFactory()
led = LED(STATUS_LED_PIN, pin_factory=factory)
led.blink(on_time=0.5, off_time=0.5) # Fast blink during init
# Initialize Servo (adjust min/max pulse widths for your specific SG90)
servo = AngularServo(PAN_SERVO_PIN, min_angle=-90, max_angle=90,
min_pulse_width=0.0005, max_pulse_width=0.0025,
pin_factory=factory)
servo.angle = 0 # Center on boot
# Initialize Camera
camera = Picamera2()
# Configure for low-latency streaming: 640x480, RGB888 for fast JPEG encoding
config = camera.create_video_configuration(main={'size': (640, 480), 'format': 'RGB888'})
camera.configure(config)
camera.start()
led.on() # Solid ON indicates stream is live
logging.info('Hardware initialized successfully.')
except RuntimeError as e:
logging.error(f'Hardware initialization failed: {e}')
raise
def generate_frames():
while True:
if camera is None:
break
# Capture frame to memory buffer
buffer = io.BytesIO()
camera.capture_file(buffer, format='jpeg')
buffer.seek(0)
frame_data = buffer.getvalue()
# Yield frame in multipart MJPEG format
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + frame_data + b'\r\n')
# Throttle to ~30 FPS to prevent CPU thermal throttling on Pi 5
time.sleep(0.033)
@app.route('/video_feed')
def video_feed():
return Response(generate_frames(),
mimetype='multipart/x-mixed-replace; boundary=frame')
@app.route('/pan/<int:angle>')
def pan(angle):
if servo and -90 <= angle <= 90:
servo.angle = angle
return f'Panned to {angle} degrees', 200
return 'Invalid angle', 400
@app.route('/')
def index():
return '<h1>Raspberry Pi Stream</h1><img src="/video_feed" width="640">'
if __name__ == '__main__':
try:
init_hardware()
# Threaded=True is critical for handling multiple browser connections
app.run(host='0.0.0.0', port=8000, threaded=True)
except Exception as e:
logging.critical(f'Fatal server error: {e}')
finally:
logging.info('Shutting down hardware...')
if led: led.off()
if servo: servo.detach()
if camera: camera.stop()
Debugging: When the Stream Fails
Embedded video streaming is notorious for failing silently or throwing cryptic C++ library errors up through the Python bindings. Here are the exact error strings you will encounter and how to fix them.
- Cable Orientation: On the Pi 5, the bare metal contacts on the CSI ribbon must face away from the board (towards the outer edge). On the Pi 4, they face the Ethernet port. Backward cables fry the camera's I2C voltage regulator.
- Base System Test: Run
libcamera-hello -t 5000in the terminal. If this fails, your Python code will never work. Fix the OS/hardware layer first. - Voltage Brownouts: Run
dmesg | grep -i undervoltage. If you see warnings, the Pi is disabling the CSI I2C bus to save power. Upgrade your PSU.
Error 1: RuntimeError: Failed to acquire camera: Device or resource busy
What it means: The /dev/video0 node is locked by another process. libcamera enforces strict exclusive access to the sensor.
- Cause A (Most Likely): You have a zombie Python process from a previous crashed run still holding the camera. Fix: Run
sudo killall python3orfuser -k /dev/video0. - Cause B: The legacy
raspicamstack or a background service likemotionis running. Fix: Disable conflicting services viasudo systemctl stop motion.
Error 2: libcamera.WARN: ... No cameras available!
What it means: The libcamera IPA (Image Processing Algorithm) pipeline cannot communicate with the IMX708 sensor over the I2C bus.
- Cause A (Most Likely): You are using a standard 15-pin CSI cable on a Raspberry Pi 5 without the official 22-pin adapter cable. Fix: Buy the official Pi 5 camera cable.
- Cause B: The CSI ribbon cable is not fully seated in the ZIF connector, or the locking latch was not pressed down. Fix: Reseat the cable and ensure the latch is flush.
- Cause C: You are running an outdated Bullseye OS image that lacks the IMX708 sensor tuning files. Fix: Flash Bookworm.
Extending and Simplifying the Build
Depending on your end goal, you may not need a custom Flask server. Here is how to pivot the architecture based on your project requirements.
To Simplify (Raw H.264 CLI Stream):
If you just need to pipe video into VLC or OBS and don't care about GPIO control, drop Python entirely. Use the native rpicam-vid binary, which utilizes the Pi's hardware H.264 encoder for near-zero CPU load:
rpicam-vid --inline --listen -o tcp://0.0.0.0:8888 -t 0 --width 1280 --height 720
Connect to this via VLC using tcp://[PI_IP]:8888. Note that H.264 over TCP introduces 1-2 seconds of latency due to keyframe buffering.
To Extend (RTSP for NVR Integration):
If you want to feed this Raspberry Pi stream into a Network Video Recorder like Frigate or BlueIris, MJPEG over HTTP will consume massive bandwidth and lack hardware acceleration. Install MediaMTX on the Pi to convert the local camera feed into a standards-compliant RTSP stream. This allows you to push H.265/HEVC (supported natively on the Pi 5) to your NVR at 4K resolution with minimal network overhead.
Raspberry Pi Stream FAQ
How do I reduce the latency of my Raspberry Pi stream over WiFi?
MJPEG over Flask typically yields 150-300ms of latency on a local network. To push this below 100ms, you must disable the Pi's WiFi power-saving mode, which puts the radio to sleep between frame bursts, causing micro-stutters and buffering. Run sudo iw dev wlan0 set power_save off. Additionally, ensure you are connected to a 5GHz WiFi network; the 2.4GHz band is heavily congested and will drop MTU-sized video packets, forcing TCP retransmissions that spike latency to over 500ms.
Can I use a Raspberry Pi stream for a 24/7 Frigate security camera setup?
Yes, but with critical storage caveats. Running a 24/7 stream server directly off a microSD card will destroy the card's write sectors within 3 to 6 months due to the OS logging and temporary frame buffering. For a 24/7 security deployment, you must boot the Pi 5 from an external NVMe SSD via the PCIe HAT, or at minimum, configure your tmpfs RAM disk to handle all /tmp camera buffers. Furthermore, Frigate prefers RTSP over MJPEG, so use the MediaMTX extension mentioned above rather than the Flask script for NVR integration.
Why does my Raspberry Pi stream drop frames when I add a servo pan/tilt?
This is almost always a power delivery issue, not a CPU bottleneck. When an SG90 servo changes direction, it draws a sudden spike of 500mA-700mA. If the servo is powered from the Pi's 5V rail, this spike causes a micro-brownout on the 3.3V logic rail, which resets the camera's I2C control bus. The picamera2 library will silently drop frames while it attempts to re-establish sensor telemetry. Always use an isolated 5V buck converter for servos, tying only the GND and PWM signal wires back to the Pi.






