Motion JPEG Streamer¶
The mjpg-streamer
container lets you pass a video stream from a local camera to a motioneye
container. The mjpg-streamer
and motioneye
containers can be running on the same or different hosts.
Each mjpg-streamer
container can process a stream from an official Raspberry Pi "ribbon cable" camera, or from a third-party USB-connected camera, such as those from Logitech.
Using mjpg-streamer
to handle your video streams gives you a consistent approach to supporting multiple cameras and camera types. You do not need to care about distinctions between "ribbon" or USB cameras, nor which hosts are involved.
Raspberry Pi Ribbon Camera¶
This section is only relevant if you are trying to use a camera that connects to your Raspberry Pi via a ribbon cable.
Beginning with Raspberry Pi OS Bullseye, the Raspberry Pi Foundation introduced the LibCamera subsystem and withdrew support for the earlier raspistill
and raspivid
mechanisms which then became known as the legacy camera system.
The introduction of the LibCamera subsystem triggered quite a few articles (and videos) on the topic, of which this is one example:
Although the LibCamera subsystem works quite well with "native" applications, it has never been clear whether it supports passing camera streams to Docker containers. At the time of writing (2023-10-23), this author has never been able to find any examples which demonstrate that such support exists.
It is important to understand that:
- This only applies to the Raspberry Pi Ribbon Camera;
- In order to access a Raspberry Pi Ribbon Camera, the
mjpg-streamer
container depends on the legacy camera system; and - The LibCamera subsystem and the legacy camera system are mutually exclusive.
In other words, if you want to use the mjpg-streamer
container to process a stream from a Raspberry Pi Ribbon Camera, you have to forgo using the LibCamera subsystem.
preparing your Raspberry Pi¶
If you have a Raspberry Pi Ribbon Camera, prepare your system like this:
-
Check the version of your system by running:
$ grep "VERSION_CODENAME" /etc/os-release
The answer should be one of "buster", "bullseye" or "bookworm".
-
Configure camera support:
-
if your system is running Buster, run this command:
$ sudo raspi-config nonint do_camera 0
Buster pre-dates LibCamera so this is the same as enabling the legacy camera system. In this context,
0
means "enable" and1
means "disable". -
if your system is running Bullseye or Bookworm, run these commands:
$ sudo raspi-config nonint do_camera 1 $ sudo raspi-config nonint do_legacy 0
The first command is protective and turns off the LibCamera subsystem, while the second command enables the legacy camera system.
When executed from the command line, both the
do_camera
anddo_legacy
commands are supported in the Bookworm version ofraspi-config
. However, neither command is available whenraspi-config
is invoked as a GUI in a Bookworm system. This likely implies that the commands have been deprecated and will be removed, in which case this documentation will break.
-
-
Reboot your system:
$ sudo reboot
-
Make a note that your ribbon camera will be accessible on
/dev/video0
.
Third-party cameras¶
The simplest approach is:
- Connect your camera to a USB port.
-
Run:
$ ls -l /dev/v4l/by-id
This is an example of the response with a LogiTech "C920 PRO FHD Webcam 1080P" camera connected:
lrwxrwxrwx 1 root root 12 Oct 23 15:42 usb-046d_HD_Pro_Webcam_C920-video-index0 -> ../../video1 lrwxrwxrwx 1 root root 12 Oct 23 15:42 usb-046d_HD_Pro_Webcam_C920-video-index1 -> ../../video2
In general, the device at
index0
is where your camera will be accessible, as in:/dev/v4l/by-id/usb-046d_HD_Pro_Webcam_C920-video-index0
If you don't get a sensible response to the ls
command then try disconnecting and reconnecting your camera, and rebooting your system.
Container variables¶
environment variables¶
variable | default | remark |
---|---|---|
MJPG_STREAMER_USERNAME |
container ID | changes each time the container is recreated |
MJPG_STREAMER_PASSWORD |
random UUID | changes each time the container restarts |
MJPG_STREAMER_SIZE |
640x480 |
should be one of your camera's natural resolutions |
MJPG_STREAMER_FPS |
5 |
frames per second |
device variable¶
variable | default | remark |
---|---|---|
MJPG_STREAMER_EXTERNAL_DEVICE |
/dev/video0 |
must be set to your video device |
Setting your variables¶
To initialise your environment, begin by using a text editor (eg vim
, nano
) to edit ~/IOTstack/.env
(which may or may not already exist):
-
If your
.env
file does not already define your time-zone, take the opportunity to set it. For example:TZ=Australia/Sydney
-
The access credentials default to random values which change each time the container starts. This is reasonably secure but is unlikely to be useful in practice, so you need to invent some credentials of your own. Example:
MJPG_STREAMER_USERNAME=streamer MJPG_STREAMER_PASSWORD=oNfDG-d1kgzC
-
Define the external device path to your camera. Two examples have been given above:
-
a ribbon camera:
MJPG_STREAMER_EXTERNAL_DEVICE=/dev/video0
-
a Logitech C920 USB camera:
MJPG_STREAMER_EXTERNAL_DEVICE=/dev/v4l/by-id/usb-046d_HD_Pro_Webcam_C920-video-index
-
-
If you know your camera supports higher resolutions, you can also set the size. Examples:
-
the ribbon camera can support:
MJPG_STREAMER_SIZE=1152x648
-
the Logitech C920 can support:
MJPG_STREAMER_SIZE=1920x1080
-
-
If the
mjpg-streamer
andmotioneye
containers are going to be running on:-
the same host, you can consider increasing the frame rate:
MJPG_STREAMER_FPS=30
Even though we are setting up a web camera, the traffic will never leave the host and will not traverse your Ethernet or WiFi networks.
-
different hosts, you should probably leave the rate at 5 frames per second until you understand the impact on network traffic.
-
-
Save your work.
Tip:
- Do not use quote marks (either single or double quotes) to surround the values of your environment variables. This is because docker-compose treats the quotes as part of the string. If you used quotes, please go back and remove them.
alternative approach¶
It is still a good idea to define TZ
in your .env
file. Most IOTstack containers now use the TZ=${TZ:-Etc/UTC}
syntax so a single entry in your .env
sets the timezone for all of your containers.
However, if you prefer to keep most of your environment variables inline in your docker-compose.yml
rather than in .env
, you can do that. Example:
environment:
- TZ=${TZ:-Etc/UTC}
- MJPG_STREAMER_USERNAME=streamer
- MJPG_STREAMER_PASSWORD=oNfDG-d1kgzC
- MJPG_STREAMER_SIZE=1152x648
- MJPG_STREAMER_FPS=5
Similarly for the camera device mapping:
devices:
- "/dev/v4l/by-id/usb-046d_HD_Pro_Webcam_C920-video-index:/dev/video0"
about variable substitution syntax¶
If you're wondering about the syntax used for environment variables:
- MJPG_STREAMER_USERNAME=${MJPG_STREAMER_USERNAME:-}
it means that .env
will be checked for the presence of MJPG_STREAMER_USERNAME=value
. If the key is found, its value will be used. If the key is not found, the value will be set to a null string. Then, inside the container, a null string is used as the trigger to apply the defaults listed in the table above.
In the case of the camera device mapping, this syntax:
- "${MJPG_STREAMER_EXTERNAL_DEVICE:-/dev/video0}:/dev/video0"
means that .env
will be checked for the presence of MJPG_STREAMER_EXTERNAL_DEVICE=path
. If the key is found, the path will be used. If the key is not found, the path will be set to /dev/video0
on the assumption that a camera is present and the device exists.
Regardless of whether a device path comes from .env
, or is defined inline, or defaults to /dev/video0
, if the device does not actually exist then docker-compose
will refuse to start the container with the following error:
Error response from daemon: error gathering device information while adding custom device "«path»": no such file or directory
Starting the container¶
-
Start the container like this:
$ cd ~/IOTstack $ docker-compose up -d mjpg-streamer
The first time you do this triggers a fairly long process. First, a basic operating system image is downloaded from DockerHub, then a Dockerfile is run to add the streamer software and construct a local image, after which the local image is instantiated as your running container. Subsequent launches use the local image so the container starts immediately. See also container maintenance.
-
Once the container is running, make sure it is behaving normally and has not gone into a restart loop:
$ docker ps -a --format "table {{.Names}}\t{{.RunningFor}}\t{{.Status}}"
The
docker ps
command produces a lot of output which generally results in line-wrapping and can be hard to read. The--format
argument reduces this clutter by focusing on the interesting columns. If you have IOTstackAliases installed, you can useDPS
instead of copy/pasting the above command.If the container is restarting, you will see evidence of that in the STATUS column. If that happens, re-check the values set in the
.env
file and "up" the container again. The container's log (see below) may also be helpful. -
Check the container's log:
$ docker logs mjpg-streamer i: Using V4L2 device.: /dev/video0 i: Desired Resolution: 1152 x 648 i: Frames Per Second.: 5 i: Format............: JPEG i: TV-Norm...........: DEFAULT o: www-folder-path......: /usr/local/share/mjpg-streamer/www/ o: HTTP TCP port........: 80 o: HTTP Listen Address..: (null) o: username:password....: streamer:oNfDG-d1kgzC o: commands.............: enabled
Many of the values you set earlier using environment variables show up here so viewing the log is a good way of making sure everything is being passed to the container.
Note:
- The
/dev/video0
in the first line of output is the internal device path (inside the container). This is not the same as the external device path associated withMJPG_STREAMER_EXTERNAL_DEVICE
. The container doesn't know about the external device path so it has no way to display it.
- The
Connecting the camera to MotionEye¶
- Use a browser to connect with MotionEye on port 8765.
- Authenticate as an administrator (the default is "admin" with no password).
- Click the ☰ icon at the top, left of the screen so that it rotates 90° and exposes the "Camera" popup menu.
- In the "Camera" popup menu field, click the ▾ and choose "Add Camera…".
- Change the "Camera Type" field to "Network Camera".
-
If the
motioneye
andmjpg-streamer
containers are running on:-
the same host, the URL should be:
http://mjpg-streamer:80/?action=stream
Here:
mjpg-streamer
is the name of the container. Technically, it is a host name (rather than a domain name); and-
port 80 is the internal port that the streamer process running inside the container is listening to. It comes from the right hand side of the port mapping in the service definition:
ports: - "8980:80"
-
different hosts, the URL should be in this form:
http://«name-or-ip»:8980/?action=stream
Here:
-
«name-or-ip»
is the domain name or IP address of the host on which themjpg-streamer
container is running. Examples:http://raspberrypi.local:8980/?action=stream http://my-spy.domain.com:8980/?action=stream http://192.168.200.200:8980/?action=stream
-
port 8980 is the external port that the host where the
mjpg-streamer
container is running is listening on behalf of the container. It comes from the left hand side of the port mapping in the service definition:ports: - "8980:80"
-
-
-
Enter the Username ("streamer" in this example).
- Enter the Password ("oNfDG-d1kgzC" in this example).
- Click in the Username field again. This causes MotionEye to retry the connection, after which the camera should appear in the Camera field.
- Click OK. The camera feed should start working.
Container maintenance¶
Because it is built from a local Dockerfile, the mjpg-streamer
does not get updated in response to a normal "pull". If you want to rebuild the container, proceed like this:
$ cd ~/IOTstack
$ docker-compose build --no-cache --pull mjpg-streamer
$ docker-compose up -d mjpg-streamer
$ docker system prune -f
If you have IOTstackAliases installed, the above is:
$ REBUILD mjpg-streamer
$ UP mjpg-streamer
$ PRUNE