Source code for device.camera_run

import subprocess
import argparse
import re
import shutil


"""
Requires following linux packages: vlc, (ffmpeg)

Serves webcam video stream at
    http://this-ip-address:5200/stream.mpjpeg
    http://this-ip-address:5200/stream.ts
(depending on the 'variant' parameter)

Latency, bitrate:
  MJPG     640x480@30fps ~  450ms  6.4Mb/s  (probably < 30fps)
  MJPG     640x480@15fps ~  700ms  3.8Mb/s
  MJPG    1280x720@30fps ~  900ms  7.0Mb/s  (probably only ~17fps)
  MJPG    1280x720@15fps ~ 1100ms  6.4Mb/s
  (...in browser, add +1100ms when watching in vlc)

  MJPG/TS  640x480@30fps ~ 2000ms in vlc  6.4Mb/s

  h264/TS  640x480@30fps ~ 3.6s in vlc  0.3Mb/s
  h264/TS 1280x720@30fps ~ 7.6s in vlc  ???Mb/s
  h264/TS 1280x720@15fps ~ 9.5s in vlc  0.6-1Mb/s

  h264/youtube 640x480@30fps ~ 2-5s (ultra low lat.) 400kb/s
  h264/youtube 854x480@30fps ~ 2.5s (ultra low lat.) 680kb/s
  h264/youtube 854x480@30fps ~ 2.5s (ultra low lat.) 680kb/s
"""

quality_presets = {
    # width, height, fps
    "min": (640, 480, 15),
    "low": (640, 480, 30),
    "medium": (960, 540, 30),
    "high": (1280, 720, 15),
    "max": (1280, 720, 30),
}
quality_presets_alt = {
    f"{h}p@{fps}": (w, h, fps) for w, h, fps in quality_presets.values()
}

def _start_vlc_process(device, quality, stream_out, disable_log=False):
    try:
        if quality in quality_presets:
            width, height, fps = quality_presets[quality]
        else:
            width, height, fps = quality_presets_alt[quality]
    except KeyError:
        raise KeyError(f"Unknown quality option. Use: {quality_presets} or {quality_presets_alt}")
    try:
        process = subprocess.Popen([
                "cvlc",
                f"v4l2://{device}", "--v4l2-chroma", "MJPG",
                "--v4l2-width", f"{width:d}", "--v4l2-height", f"{height:d}"
                "--v4l2-fps", f"{fps:d}", "--sout", stream_out,
                "-I", "dummy", "-A", "adummy"
            ],
            stderr=subprocess.DEVNULL if disable_log else None,
        )
    except FileNotFoundError:
        raise RuntimeError("VLC player is required, install it using 'sudo apt install vlc'")
    return process

def _start_ffmpeg_process(stream_input, rtmp_url, add_audio=True, disable_log=False):
    silent_audio = ["-f", "lavfi", "-i", "anullsrc"]
    try:
        process = subprocess.Popen([
                "ffmpeg", "-i", stream_input,
                *(silent_audio if add_audio else []),
                "-b:v", "4000k", "-b:a", "128k",
                "-c:v", "libx264",
                # "-preset", "veryfast",
                "-preset", "fast",
                "-f", "flv", rtmp_url
            ],
            stderr=subprocess.DEVNULL if disable_log else None,
        )
    except FileNotFoundError:
        raise RuntimeError("FFmpeg encoder is required, install it using 'sudo apt install ffmpeg'")
    return process

[docs] def check_dependencies(): """Return True if all dependencies are installed (vlc, ffmpeg).""" return all(shutil.which(cmd) is not None for cmd in ("cvlc", "ffmpeg"))
[docs] def start_stream(device, quality="medium", port=5200): """Start a vlc process that serves the MPJPEG stream at http://<ip-address>:5200/stream.mpjpeg Use process.terminate(); process.poll() to stop the returned process. MPJPEG is a raw format captured by the webcam, the resulting stream has relatively low latency (500-1000ms), but it eats a lot of bandwidth. Only a handful of people can watch at the same time before the Raspberry PI is unable to keep up (the video slows down). """ return _start_vlc_process( device, quality, f"#http{{mux=mpjpeg,dst=:{port}/stream.mpjpeg}}", )
[docs] def start_encoder_rtmp(rtmp_url, mpjpeg_host="localhost:5200"): """Start a ffmpeg process that encodes the MPJPEG stream and sends it to to the specified RTMP server (e.g. rtmp://server.com/something). Use process.terminate(); process.poll() to stop the returned process. Encoding the video (h264) and streaming to a RTMP server is a simple way how to bring the content to a large audience for example via youtube livestreams. This comes at the expense of increased latency (10-40s) and significant CPU demands (Raspberry PI may be bordeline for 720p@30fps). """ # stream_out = ( # "#duplicate{{" # "dst=http{{mux=mpjpeg,dst=:{port}/stream.mpjpeg}}," # "dst='transcode{{vcodec=h264}}" # ":std{{access=rtmp,mux=ffmpeg{{mux=flv}},dst={rtmp_url}}}'" # "}}" # ).format(port=port, rtmp_url=rtmp_url) # return _start_vlc_process(device, quality, stream_out) return _start_ffmpeg_process( f"http://{mpjpeg_host}/stream.mpjpeg", rtmp_url, add_audio=True, # Add silent audio track, required for youtube )
parser = argparse.ArgumentParser(description=( "Serve webcam video stream via MPJPEG (unencoded). Optionally, publish" " a second stream (encoded) to RTMP streaming server. VLC media player" " is required to run this program. Install it via 'sudo apt install vlc'." )) parser.add_argument("device", help="webcam device, e.g. /dev/video0") parser.add_argument( "-q", "--quality", default="medium", help="one of: min, low, medium, high, max (default=medium)" ) parser.add_argument( "-p", "--port", default=5200, help="server listening port (default=5200)" ) parser.add_argument( "-r", "--rtmp-url", dest="rtmp_url", default=None, help=( "specify a RTMP url to create a secondary encoded stream and start" " publishing it to an RTMP server, e.g. rtmp://example.com/stream_key" ) )
[docs] def get_my_ips(loopback=False): """Return a list of IPv4 adresses assigned to each network interface. The local address 127.0.0.1 of the loopback interface is omitted by default. """ try: res = subprocess.run(["ip", "address"], capture_output=True, text=True) except FileNotFoundError: return None if res.returncode != 0: return None return [ip for ip in re.findall( r"inet ([\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}.[\d]{1,3})", res.stdout ) if (ip != "127.0.0.1" or loopback)]
if __name__ == "__main__": args = parser.parse_args() all_ips = get_my_ips() ip = all_ips[0] if len(all_ips) == 1 else "<ip-address>" if args.rtmp_url: print(f"Serving at http://{ip}:{args.port}/stream.mpjpeg") print(f"Publishing to {args.rtmp_url}") proc_camera = start_stream(args.device, args.quality, args.port) proc_rtmp = start_encoder_rtmp(args.rtmp_url) # process = start_stream_with_rtmp(args.device, args.rtmp_url, args.quality, args.port) else: print(f"Serving at http://{ip}:{args.port}/stream.mpjpeg") proc_camera = start_stream(args.device, args.quality, args.port) proc_rtmp = None try: proc_camera.wait() except KeyboardInterrupt: print("Exiting...") if proc_rtmp: proc_rtmp.terminate() proc_rtmp.wait()