Streaming video from a Raspberry Pi camera

In this guide, I will go through two ways to stream video from a Raspberry Pi.
First using the picamera library in Python, and then using the raspivid tool and VLC directly in the shell.

Published: 2020-05-30
Last updated: 2020-06-07 12:58

rpi
vlc
picamera
guide

>Python web server

The bulk of the code is taken from random nerd tutorials: Video Streaming with Raspberry Pi Camera.

>>Installation

You need to install the Python package for accessing the Pi camera.

pip install picamera

>>The script

# surv.py 

# Web streaming example
# Source code from the official PiCamera package
# http://picamera.readthedocs.io/en/latest/recipes2.html#web-streaming

import io
import picamera
import logging
import socketserver
from threading import Condition
from http import server
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--size", nargs=2, type=int, default=(640, 480), help="Size of captured images")
parser.add_argument("--frame-rate", "-r", type=int, default=30, help="Frame rate of camera capture")
parser.add_argument("--port", "-p", type=int, default=8000, help="Port to serve website on")
parser.add_argument("--rotation", type=int, default=180, help="Rotation of camera")
args = parser.parse_args()

width  = args.size[0]
height = args.size[1]

PAGE=f"""
<html>
<head>
<title>Raspberry Pi - Surveillance Camera</title>
</head>
<body>
<center><h1>Raspberry Pi - Surveillance Camera</h1></center>
<center><img src="stream.mjpg" style="max-width: 1024px; width: 100%;"></center>
</body>
</html>
"""

class StreamingOutput(object):
    def __init__(self):
        self.frame = None
        self.buffer = io.BytesIO()
        self.condition = Condition()

    def write(self, buf):
        if buf.startswith(b'\xff\xd8'):
            # New frame, copy the existing buffer's content and notify all
            # clients it's available
            self.buffer.truncate()
            with self.condition:
                self.frame = self.buffer.getvalue()
                self.condition.notify_all()
            self.buffer.seek(0)
        return self.buffer.write(buf)

class StreamingHandler(server.BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/':
            self.send_response(301)
            self.send_header('Location', '/index.html')
            self.end_headers()
        elif self.path == '/index.html':
            content = PAGE.encode('utf-8')
            self.send_response(200)
            self.send_header('Content-Type', 'text/html')
            self.send_header('Content-Length', len(content))
            self.end_headers()
            self.wfile.write(content)
        elif 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 = output.frame
                    self.wfile.write(b'--FRAME\r\n')
                    self.send_header('Content-Type', 'image/jpeg')
                    self.send_header('Content-Length', len(frame))
                    self.end_headers()
                    self.wfile.write(frame)
                    self.wfile.write(b'\r\n')
            except Exception as e:
                logging.warning(
                    'Removed streaming client %s: %s',
                    self.client_address, str(e))
        else:
            self.send_error(404)
            self.end_headers()

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

with picamera.PiCamera(resolution=f'{width}x{height}', framerate=args.frame_rate) as camera:
    output = StreamingOutput()
    camera.rotation = args.rotation
    camera.start_recording(output, format='mjpeg')
    try:
        address = ('', args.port)
        server = StreamingServer(address, StreamingHandler)
        server.serve_forever()
    finally:
        camera.stop_recording()

>>Running

$ python surv.py --help
usage: surv.py [-h] [--size SIZE SIZE] [--frame-rate FRAME_RATE] [--port PORT]
               [--rotation ROTATION]

optional arguments:
  -h, --help            show this help message and exit
  --size SIZE SIZE      Size of captured images
  --frame-rate FRAME_RATE, -r FRAME_RATE
                        Frame rate of camera capture
  --port PORT, -p PORT  Port to serve website on
  --rotation ROTATION   Rotation of camera

$ python surv.py

Before changing video settings, make sure to Picamera’s page on sensor modes. There are some connections between frame rate and resolution which would take time to figure out empirically. This was important for me, because I wanted the native aspect ratio and as high frame rate as possible.

Here is an excerpt for the V2 camera module with the settings I preferred:

# Resolution Aspect Ratio Framerates Video Image FoV Binning
4 1640x1232 4:3 1/10 <= fps <= 40 x   Full 2x2

And the corresponding invocation:

$ python surv.py --size 1640 1232 --frame-rate 40 --rotation 180 -p 8000

>>Result

Using your favorite web browser, you can then go to the IP address (or hostname if you have the luxury of mDNS) of the server:

Surveillance web page accessed at http://emaus-pi3:8000 with Firefox

I was quite astonished at the low latency of the stream! It is well under a second. Awesome! 🎉

>>>Monitoring traffic

I thought it would be possible to monitor the network traffic using the built-in developer tools in Firefox (accessed by pressing F12). However, the network logger does not update its stats until after an “image” has been downloaded completely. So we will only be able to get the statistics when the stream has ended. This is what is looks like while it is streaming:

Network logger tool in Firefox showing no activity for the stream

>VLC server

VLC can be used from the command line to stream video with RTP (Real-Time Protocol) through RTMP (Real-Time Messaging Protocol). It’s quite neat. 😊

There are several example on how to use this. See for example Chis Carey’s blog, and Sammit.

>>Installation

raspivid should already be installed, so you only need to install VLC.

apt install vlc

>>Running

raspivid has a bunch of flags that can be used for tweaking its output. It can rotate the video, apply video filters, change stream settings etc. Use raspivid 2>&1 | less for viewing its help with a pager.

>>>H.264

To set up an H264 video stream, use the following command:

$ raspivid -rot 180 -t 0 -fps 20 -w 640 -h 480 -o - |
    cvlc -vvv stream:///dev/stdin --sout '#rtp{access=udp,sdp=rtsp://:8554/stream}' :demux=h264

>>>>raspivid flags

I didn’t notice much change in latency when playing around with the settings. It was quite constant around 2-4 seconds depending on when the stream was started. Except for possibly when increasing the framerate.

>>>M-JPEG

VLC can also be used for setting up a Motion-JPEG stream using the following flags:

$ raspivid -cd MJPEG -rot 180 -t 0 -fps 20 -w 640 -h 480 -o - |
    cvlc -vvv stream:///dev/stdin --sout '#rtp{access=udp,sdp=rtsp://:8554/stream}' :demux=mjpeg

>>Result

Open the stream in VLC by going to MediaOpen Network Stream… (Ctrl + N):

Opening a stream in VLC

Viewing the stream

The latency of this stream is way higher than the picamera stream. For me it is at least 2 seconds, and can go up to around 6 seconds.

>>>Monitoring traffic

You can monitor the actual bitrate of the stream in VLC by going to ToolsMedia information (Ctrl + I), and then switching to the Statistics tab. This can be useful while tweaking the raspivid settings:

Stream statistics in VLC