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()