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.

ComponentExact Variant / ModelInterface / PinNotes
Compute BoardRaspberry Pi 5 (4GB)N/ARequires active cooling for sustained video encoding
Camera ModuleCamera Module 3 (IMX708)CSI0Supports PDAF and HDR; requires 22-pin cable for Pi 5
CSI Ribbon Cable22-pin to 15-pin FFCCSI0Pi 5 uses 22-pin (0.5mm pitch); Pi 4 uses 15-pin (1mm pitch)
Power Supply27W USB-C PD (5V/5A)USB-C PWRCamera + Servo will brownout on standard 5V/3A supplies
Status LED5mm Red LED + 330Ω ResistorGPIO 17 (Pin 11)Indicates stream active status
Pan ServoSG90 Micro ServoGPIO 18 (Pin 12)Hardware PWM0; requires external 5V rail for high torque
Bench Tip: Never power an SG90 servo directly from the Pi 5's 5V GPIO header if you are also running the camera module. The IMX708 draws up to 250mA during initialization, and the servo stall current can exceed 700mA. Use a separate 5V buck converter tied to a common ground for the servo power rail.

Step-by-Step Build & Compilable Code

  1. Flash the OS: Use Raspberry Pi Imager to install Raspberry Pi OS (64-bit, Bookworm). Do not use Bullseye; the picamera2 library relies on Bookworm's Wayland and libcamera integration.
  2. Install Dependencies: Open a terminal and install the required Python packages. We use gpiozero with the pigpio backend 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
  3. 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.
  4. 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.

First Three Things to Check When It Fails:
  1. 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.
  2. Base System Test: Run libcamera-hello -t 5000 in the terminal. If this fails, your Python code will never work. Fix the OS/hardware layer first.
  3. 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 python3 or fuser -k /dev/video0.
  • Cause B: The legacy raspicam stack or a background service like motion is running. Fix: Disable conflicting services via sudo 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.