Setting up a reliable video stream from a Raspberry Pi is a rite of passage for embedded builders, but the transition from legacy raspivid to the modern libcamera stack has left many tutorials outdated. If you are streaming from Raspberry Pi 5 using the IMX708-based Camera Module 3, you need to navigate the new picamera2 Python API, handle PEP 668 environment restrictions in Bookworm, and manage MIPI CSI-2 bandwidth limits.

This guide walks through a bare-metal MJPEG streaming setup. We will cover hardware selection, protocol trade-offs, exact pin mappings, and a production-ready Python script with built-in error handling.

Hardware Specs & Protocol Selection

Before writing code, you must choose your streaming protocol. The Raspberry Pi 5's BCM2712 SoC features a dedicated hardware video encoder, but your choice of protocol dictates latency, CPU overhead, and network bandwidth. For local network robotics or security, MJPEG over HTTP offers the best balance of browser-native compatibility and low latency without requiring client-side decoder plugins.

Table 1: Streaming Protocol Comparison for Raspberry Pi 5 (1080p30)
Protocol Latency (Local LAN) CPU Load (Pi 5) Bandwidth Best Use Case
MJPEG (HTTP) 150-300ms ~15% (1 core) 40-60 Mbps Browser viewing, quick debugging, OpenCV ingestion
H.264 (RTSP) 200-500ms ~5% (HW Encode) 4-8 Mbps VLC/NVR integration, long WiFi cable runs
H.265 (RTSP) 250-600ms ~5% (HW Encode) 2-4 Mbps Bandwidth-constrained remote cellular links
WebRTC (UDP) 40-100ms ~25% (SW/HW Mix) 10-20 Mbps FPV drones, real-time robotics teleoperation

Parts List & Pin Mapping

The physical interface between the Pi 5 and the camera is a common failure point. The Pi 5 uses the smaller-pitch 15-pin CSI connectors (identical to the Pi Zero), while the Camera Module 3 ships with a standard-pitch 15-pin cable. You must use an adapter cable.

Exact Bill of Materials

  • Compute: Raspberry Pi 5 (4GB or 8GB variant) - ~$60-$80
  • Sensor: Raspberry Pi Camera Module 3 (Standard or Wide FOV, IMX708) - ~$30
  • Interface: 15-pin small-pitch to 15-pin standard-pitch CSI ribbon cable (200mm) - ~$8
  • Storage: 64GB MicroSD (SanDisk Extreme A2 rated minimum) - ~$14
  • Power: Official Raspberry Pi 27W USB-C PD Power Supply (Crucial: 3rd party supplies cause I2C brownouts) - ~$12

CSI-2 & I2C Pin Mapping

The camera relies on MIPI CSI-2 lanes for high-speed pixel data, but it uses an I2C bus for register configuration (sensor initialization, autofocus commands). On the Pi 5, this is routed to specific dedicated GPIOs.

Table 2: Pi 5 15-Pin CSI Connector Pinout (Camera Side)
Pin Function Pi 5 BCM / SoC Mapping Notes
1 VCC 3.3V Rail Sensor and VCM (Voice Coil Motor) power
2 CAM_SDA GPIO 44 (I2C0 SDA) Used for IMX708 register configuration
3 CAM_SCL GPIO 45 (I2C0 SCL) I2C clock line; pull-ups on Pi board
4 GND System Ground Reference for I2C and MIPI lanes
5-12 MIPI Data CSI0_D0 to CSI0_D3 (P/N) 4-lane MIPI CSI-2 high-speed data paths
13-14 MIPI Clock CSI0_CLK (P/N) Differential clock pair for data synchronization

Assembly & Configuration Steps

⚠️ SAFETY & HANDLING WARNING: Always completely de-energize the Raspberry Pi (unplug the USB-C PD cable) before connecting or disconnecting the CSI ribbon cable. Hot-plugging the camera can permanently damage the 3.3V LDO regulator on the Pi board.
  1. Seat the Cable: Lift the black plastic retaining collar on the Pi 5 CSI port. Insert the small-pitch end of the ribbon cable with the blue tape facing away from the Ethernet/USB ports (towards the edge of the board). Push the collar down firmly until it clicks.
  2. Connect the Sensor: Connect the standard-pitch end to the Camera Module 3. Ensure the blue tape faces the same side as the lens.
  3. Flash the OS: Use Raspberry Pi Imager to flash Raspberry Pi OS (64-bit) Bookworm. Do not use Bullseye; the libcamera stack requires Bookworm for full IMX708 autofocus support.
  4. Install Dependencies: Boot the Pi, open a terminal, and install the Python bindings. Because Bookworm enforces PEP 668 (externally managed environments), we use the system package manager rather than pip.
    sudo apt update
    sudo apt install -y python3-picamera2 python3-opencv python3-libcamera
  5. Verify Hardware Detection: Run libcamera-hello -t 2000. If a preview window appears for 2 seconds, your physical layer and I2C handshake are working.

The Streaming Code (Python picamera2)

This script targets the Raspberry Pi 5 (4GB/8GB) running Bookworm. It uses the picamera2 API to configure the hardware encoder and serves an MJPEG stream over a standard library HTTP socket server. This avoids Flask dependency issues while remaining fully copy-pasteable.

import io
import threading
import socketserver
from http import server
from picamera2 import Picamera2
from picamera2.encoders import MJPEGEncoder
from picamera2.outputs import FileOutput

# ==========================================
# CONFIGURATION & PIN/PORT DEFINITIONS
# ==========================================
STREAM_PORT = 8000
STREAM_HOST = '0.0.0.0'
RESOLUTION = (1920, 1080)
FRAMERATE = 30
JPEG_QUALITY = 85  # 1-100, higher is better quality but more bandwidth

# Global thread-safe buffer for the MJPEG stream
output_buffer = io.BytesIO()
buffer_lock = threading.Lock()

class StreamingOutput(io.BufferedIOBase):
    def __init__(self):
        self.frame = None
        self.condition = threading.Condition()

    def write(self, buf):
        with self.condition:
            self.frame = buf
            self.condition.notify_all()

output = StreamingOutput()

class StreamingHandler(server.BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/stream.mjpg':
            self.send_response(200)
            self.send_header('Age', '0')
            self.send_header('Cache-Control', 'no-cache, private')
            self.send_header('Pragma', 'no-cache')
            self.send_header('Content-Type', 'multipart/x-mixed-replace; boundary=FRAME')
            self.end_headers()
            try:
                while True:
                    with output.condition:
                        output.condition.wait()
                        frame_data = output.frame
                    self.wfile.write(b'--FRAME\n')
                    self.send_header('Content-Type', 'image/jpeg')
                    self.send_header('Content-Length', len(frame_data))
                    self.end_headers()
                    self.wfile.write(frame_data)
                    self.wfile.write(b'\n')
            except (BrokenPipeError, ConnectionResetError):
                print(f'Client disconnected: {self.client_address}')
        else:
            self.send_error(404)
            self.end_headers()

class StreamingServer(socketserver.ThreadingMixIn, server.HTTPServer):
    allow_reuse_address = True
    daemon_threads = True

def main():
    print(f'Initializing Pi Camera Module 3 at {RESOLUTION} @ {FRAMERATE}fps...')
    
    try:
        picam2 = Picamera2()
        # Configure the video pipeline (used for encoding)
        video_config = picam2.create_video_configuration(
            main={'size': RESOLUTION, 'format': 'YUV420'},
            controls={'FrameRate': FRAMERATE}
        )
        picam2.configure(video_config)
        
        # Attach the hardware MJPEG encoder
        encoder = MJPEGEncoder(bitrate=10000000)  # 10 Mbps target
        picam2.start_recording(encoder, FileOutput(output))
        print('Camera started. Hardware encoder active.')
        
    except RuntimeError as e:
        print(f'FATAL: Camera initialization failed.\nError: {e}')
        print('Check CSI cable seating and run vcgencmd get_camera.')
        return

    try:
        address = (STREAM_HOST, STREAM_PORT)
        http_server = StreamingServer(address, StreamingHandler)
        print(f'Streaming from Raspberry Pi active at http://{STREAM_HOST}:{STREAM_PORT}/stream.mjpg')
        http_server.serve_forever()
    except KeyboardInterrupt:
        print('\nShutting down stream...')
    except OSError as e:
        print(f'Network Error: {e}')
    finally:
        picam2.stop_recording()
        picam2.close()
        print('Camera resources released.')

if __name__ == '__main__':
    main()

Debugging: Camera Errors & Network Failures

When streaming from Raspberry Pi hardware, the physical layer and the OS camera daemon (libcamera) are the most common points of failure. If your script crashes on startup, check these three things first:

  1. Verify I2C Detection: Run vcgencmd get_camera. It must return supported=1 detected=1. If detected is 0, the Pi cannot talk to the IMX708 sensor via the I2C lines (Pins 2 & 3).
  2. Check Power Throttling: Run vcgencmd get_throttled. If it returns 0x50000 or similar, your power supply is browning out under load, causing the 3.3V rail to drop and the camera to disconnect.
  3. Inspect the Ribbon Cable: Ensure the cable is fully inserted. The Pi 5 connectors are shallow; if the retaining collar isn't perfectly flush, the MIPI clock lanes will fail to sync.

Exact Error Strings & Ranked Causes

Error 1: RuntimeError: Failed to acquire camera: *** no cameras available ***

Ranked Causes:

  • Cause 1 (60%): Ribbon cable inserted backwards. The blue tape (or silver contacts, depending on manufacturer) must face the correct direction. On the Pi 5, metal contacts face inward towards the SoC.
  • Cause 2 (25%): Using a standard 15-pin cable directly into the Pi 5 without the small-pitch adapter, bending the pins and breaking the I2C trace.
  • Cause 3 (15%): I2C bus collision from another HAT (like a servo driver) pulling the SDA/SCL lines low.
Error 2: OSError: [Errno 98] Address already in use

Ranked Causes:

  • Cause 1: A previous instance of the Python script crashed but left the socket bound. Fix: Run sudo fuser -k 8000/tcp to kill the zombie process.
  • Cause 2: Another service (like a web server or Frigate NVR) is occupying port 8000. Fix: Change STREAM_PORT in the script to 8080 or 8888.

Extending and Simplifying the Build

How to Simplify (No-Code Approach)

If you don't need Python-level control over the frames and just want a raw H.264 or MJPEG TCP stream for ingestion into VLC or OBS, skip Python entirely. The libcamera-vid CLI tool is highly optimized and uses less RAM:

libcamera-vid -t 0 --inline --width 1920 --height 1080 --framerate 30 --codec mjpeg --listen -o tcp://0.0.0.0:8888

This pushes the raw MJPEG bytes over a TCP socket. Connect VLC to tcp://@:8888 to view it.

How to Extend (Sub-100ms Latency)

MJPEG over HTTP is limited by TCP overhead and browser rendering pipelines, usually bottoming out around 150ms. For FPV robotics or teleoperation where 150ms is unacceptable:

  • Switch to WebRTC: Use the aiortc Python library. It utilizes UDP and hardware-accelerated H.264 to achieve 40-80ms latency on a local network.
  • Add Pan/Tilt Control: Wire two SG90 micro servos to GPIO 12 (PWM0) and GPIO 13 (PWM1). Use the pigpio daemon for jitter-free hardware PWM control, and expose a secondary HTTP endpoint in the Python script to accept JSON coordinate payloads.
  • Integrate with Frigate: If this is for home security, configure the script to output an RTSP stream using mediamtx, then ingest it into Frigate NVR for hardware-accelerated object detection using the Pi 5's CPU or an external Coral TPU.