To use a Raspberry Pi as a streaming device for low-latency surveillance or live broadcasting, the Raspberry Pi 5 (8GB) paired with the Camera Module 3 and a hardware-accelerated H.264 backend delivers 720p at 30fps with sub-200ms glass-to-glass latency. While older tutorials rely on the legacy picamera library or CPU-bound Python loops, modern Pi streaming requires the picamera2 stack and direct VPU (Video Processing Unit) encoding to prevent thermal throttling and frame drops.

This guide walks through building a motion-triggered RTMP streaming node. We will integrate an HC-SR501 PIR sensor to wake the stream and a logic-level MOSFET to drive an IR illuminator for night vision, ensuring the Pi only pushes bandwidth when an event occurs.

Hardware Selection for Low-Latency Streaming

Not all Pi and camera combinations are viable for real-time streaming. The Pi Zero 2 W lacks the USB and memory bandwidth for stable 1080p RTMP encoding, and the older Camera V2 struggles in low-light HDR scenarios. Below is a benchmark of common streaming configurations tested on a local network in 2026.

Board Variant Camera Module Max Resolution / FPS Typical Latency (Glass-to-Glass) Est. Cost (2026)
Pi 5 (8GB) Module 3 (IMX708) 1080p @ 60fps / 4K @ 30fps ~120ms (Local RTSP) $135
Pi 5 (8GB) HQ Camera (IMX477) 1080p @ 30fps ~180ms $155 (w/ lens)
Pi 4 (8GB) Module 3 (IMX708) 1080p @ 30fps ~250ms $115
Pi Zero 2 W Camera V2 (IMX219) 720p @ 30fps ~600ms+ $60

The Verdict: The Pi 5 with the Camera Module 3 is the optimal baseline. The IMX708 sensor features on-chip PDAF (Phase Detection Auto Focus) and native HDR, which prevents the "blown out" highlights common in older modules when streaming outdoors.

Parts List and GPIO Pin Mapping

This build targets the Raspberry Pi 5 (8GB) running Raspberry Pi OS (Bookworm 64-bit). Do not use the 32-bit OS; libcamera memory allocation frequently fails on 32-bit kernels when handling 1080p buffers.

Bill of Materials:

  • Raspberry Pi 5 (8GB) + 27W USB-C PD Power Supply
  • Raspberry Pi Camera Module 3 Wide (IMX708)
  • HC-SR501 PIR Motion Sensor
  • IRLZ44N Logic-Level MOSFET (Vgs threshold < 2V, crucial for 3.3V GPIO)
  • 850nm IR LED Array (12V, 500mA)
  • 10kΩ pulldown resistor, 100Ω gate resistor

GPIO Pin Mapping (BCM Numbering)

Component Pi 5 Pin (BCM) Physical Pin Notes
PIR Data Out GPIO 17 Pin 11 3.3V logic safe on HC-SR501
PIR VCC 5V Pin 2 Requires 5V for internal regulator
MOSFET Gate GPIO 27 Pin 13 PWM or Digital HIGH via 100Ω resistor
Camera CSI CSI Port 1 N/A Blue tape faces USB/Ethernet ports

Assembly and OS Configuration

  1. Flash the OS: Use Raspberry Pi Imager to flash Raspberry Pi OS (64-bit, Bookworm). In the OS customization menu, enable SSH and set your WiFi credentials.
  2. Wire the MOSFET: Connect GPIO 27 to the 100Ω resistor, then to the IRLZ44N gate. Connect the 10kΩ resistor between the gate and source (GND) to prevent the IR array from turning on during Pi boot-up when GPIOs are floating. Connect the IR LED array to a 12V external supply, with the MOSFET drain switching the ground path.
  3. Install Dependencies: SSH into the Pi and update the package list. Install the modern camera stack and GPIO libraries:
    sudo apt update
    sudo apt install -y python3-picamera2 python3-gpiozero python3-libcamera ffmpeg
  4. Verify Hardware: Run rpicam-hello. A preview window should appear (if on a desktop) or the terminal should report the IMX708 sensor initialization. If it fails here, reseat the FFC cable.

Python Streaming Code with Motion Trigger

Piping raw RGB frames through Python's GIL will bottleneck your CPU and drop frames. Instead, we use picamera2's native H264Encoder to offload encoding to the Pi 5's hardware VPU, piping the compressed stream directly into an ffmpeg subprocess for RTMP delivery.

import time
import subprocess
from picamera2 import Picamera2
from picamera2.encoders import H264Encoder
from picamera2.outputs import FileOutput
from gpiozero import MotionSensor, LED

# --- PIN DEFINITIONS (BCM) ---
PIN_PIR = 17
PIN_IR_ILLUMINATOR = 27

# --- STREAMING CONFIG ---
# Replace with your YouTube/Twitch RTMP URL or local Nginx-RTMP server
RTMP_URL = "rtmp://a.rtmp.youtube.com/live2/YOUR_STREAM_KEY"
OUTPUT_TARGET = RTMP_URL if "YOUR_STREAM_KEY" not in RTMP_URL else "local_fallback.mkv"

# Initialize GPIO
pir = MotionSensor(PIN_PIR)
ir_led = LED(PIN_IR_ILLUMINATOR)

# Initialize Camera
picam2 = Picamera2()
video_config = picam2.create_video_configuration(
    main={"size": (1280, 720), "format": "YUV420"},
    controls={"FrameRate": 30}
)
picam2.configure(video_config)

# Hardware H.264 Encoder (2.5 Mbps for good 720p quality)
encoder = H264Encoder(bitrate=2500000)

try:
    picam2.start()
    print("Camera initialized successfully.")
except RuntimeError as e:
    print(f"Fatal: Camera initialization failed: {e}")
    exit(1)

# FFmpeg command to wrap the raw H.264 stream into FLV for RTMP
ffmpeg_cmd = [
    "ffmpeg", "-y",
    "-f", "h264", "-framerate", "30", "-i", "-",
    "-c:v", "copy", "-f", "flv", OUTPUT_TARGET
]

print("System armed. Waiting for motion...")

try:
    while True:
        pir.wait_for_motion()
        print("Motion detected! Starting stream and IR illuminator.")
        ir_led.on()
        
        # Start FFmpeg subprocess
        process = subprocess.Popen(ffmpeg_cmd, stdin=subprocess.PIPE)
        output = FileOutput(process.stdin)
        
        # Start hardware recording
        picam2.start_recording(encoder, output)
        
        # Stream for 30 seconds minimum, or until motion stops for 5 seconds
        timeout = time.time() + 30 
        while time.time() < timeout:
            time.sleep(1)
            if not pir.motion_detected:
                time.sleep(5) # Grace period
                if not pir.motion_detected:
                    break
        
        # Stop recording and clean up subprocess
        picam2.stop_recording()
        process.stdin.close()
        process.wait()
        ir_led.off()
        print("Stream ended. Waiting for next motion event...")

except KeyboardInterrupt:
    print("\nShutting down gracefully...")
finally:
    picam2.stop()
    ir_led.off()

Debugging: "Device or Resource Busy" and Stream Failures

When working with the libcamera stack, the most common roadblock occurs when a background process or a crashed previous script retains the lock on the CSI interface. You will see this exact error string in your terminal:

RuntimeError: Failed to acquire camera: Device or resource busy

If you encounter this, or if the stream outputs a black screen, here are the first three things to check:

  1. Hunt for Zombie Processes: Another script or the default rpicam daemon might be holding /dev/video0. Run fuser /dev/video0 to find the PID, then kill it with sudo kill -9 [PID].
  2. Verify the FFC Cable Orientation: On the Pi 5, the blue tape on the camera ribbon cable must face away from the PCB and towards the USB/Ethernet ports. A reversed cable will pass the initial I2C handshake but fail when the VPU attempts to pull MIPI data lanes, resulting in a resource busy or timeout error.
  3. Check Power Supply Brownouts: The Pi 5 requires a 5V/5A (27W) PD supply. If you are using a standard phone charger, the Pi will throttle the USB and CSI buses to prevent a brownout. Check for the lightning bolt icon or run vcgencmd get_throttled. If it returns anything other than throttled=0x0, upgrade your power supply.

For deeper troubleshooting on network drops, consult the official Raspberry Pi Camera Software documentation regarding buffer allocation limits in Bookworm.

Extending and Simplifying the Build

Depending on your deployment environment, you may need to scale this project up or strip it down.

How to Simplify (Continuous Local NVR)

If you don't need motion triggering or RTMP broadcasting and just want a continuous local RTSP stream for an NVR like Frigate or BlueIris, drop the PIR sensor and ffmpeg subprocess entirely. Instead, install MediaMTX on the Pi. You can then use rpicam-vid to push directly to the local RTSP server:

rpicam-vid -t 0 --inline --listen -o rtsp://localhost:8554/stream

This eliminates Python overhead and relies purely on native C++ binaries, reducing CPU load to under 15%.

How to Extend (MQTT and Home Assistant Integration)

To integrate this into a smart home ecosystem, add the paho-mqtt Python library. Inside the pir.wait_for_motion() loop, publish a payload to your MQTT broker:

client.publish("homeassistant/camera/driveway/motion", "ON", retain=True)

This allows Home Assistant to trigger automations (like turning on porch lights) the exact millisecond the Pi detects motion, while the video stream handles the visual verification. For advanced network configurations and RTMP tuning, refer to the FFmpeg Streaming Guide to adjust your GOP size and keyframe intervals for your specific CDN.