Using libcamera with OctoPrint - The Docker Way

I recently got myself an ArduCam 16MP Autofocus camera to run as a monitoring camera with my Raspberry Pi 4B. It uses an IMX519 sensor, which has libcamera support in Bullseye, but not raspicam. In fact, from Raspberry Pi OS Bullseye and on, only libcamera is supported. As such, it does not work with OctoPi out of the box.

ArduCam has published instructions on how to get OctoPi working with the libcamera stack (Solution Of Using Autofocus Camera On OctoPrint - Arducam), but in my opinion, their approach is not suitable for running a real OctoPi instance for several reasons:

  • It involves modifying a nightly version of OctoPi.
    • It is not a good idea to run something as sensitive and potentially dangerous as a 3D printer long term on an unstable nightly build.
  • The modifications are not trivially reversible and are difficult to document.

:anchor: The Docker Way

This method of using libcamera and OctoPrint eschews OctoPi entirely. Instead, this will use the official Raspberry Pi OS image and run a modified mjpg_streamer and OctoPrint in Docker containers. This approach has several benefits:

  • OctoPrint is completely decoupled from the host OS. This means that any urgent OS updates can be immediately applied with negligible risk of affecting the OctoPrint installation.
  • mjpg_streamer can be easily swapped out with a better solution for streaming libcamera cameras in the future, since it runs as just a basic Docker container.
  • Everything is documented as Dockerfiles and YAML files, meaning the setup is portable and can be easily replicated across multiple installations.

:warning: Warning: This is a technical guide

This guide assumes that you are capable of setting up and maintaining a Docker installation. If you are not comfortable with Docker, this may be too complex.

Setting up the camera

Before you can stream from a libcamera camera, you should first make sure that the camera functions as expected. In my case, I installed the kernel driver given by ArduCam (see ArduCam instructions above):

wget -O install_pivariety_pkgs.sh https://github.com/ArduCAM/Arducam-Pivariety-V4L2-Driver/releases/download/install_script/install_pivariety_pkgs.sh
chmod +x install_pivariety_pkgs.sh
./install_pivariety_pkgs.sh -p imx519_kernel_driver

You can test whether the camera works by installing and running a simple libcamera-still:

sudo apt install libcamera-apps-lite
libcamera-still

Dockerizing mjpg_streamer

To dockerize mjpg_streamer with support for libcamera, I opted to use the fork that ArduCam provided: GitHub - ArduCAM/mjpg-streamer: Fork of http://sourceforge.net/projects/mjpg-streamer/.

I created a Docker image that would download, patch, and build this fork:

Dockerfile

FROM debian:bullseye

ENV DEBIAN_FRONTEND noninteractive

RUN apt-get update && apt-get install -y cmake build-essential curl git

RUN echo "deb http://archive.raspberrypi.org/debian/ bullseye main" > /etc/apt/sources.list.d/raspi.list
RUN curl "https://archive.raspberrypi.org/debian/raspberrypi.gpg.key" | apt-key add -

RUN apt-get update && apt-get install -y libcamera-dev libjpeg-dev libgphoto2-dev

RUN git clone https://github.com/ArduCAM/mjpg-streamer.git /src
WORKDIR /src/mjpg-streamer-experimental

RUN sed -i "/input_libcamera/p;/input_/d;/output_http/p;/output_/d" CMakeLists.txt
RUN make && make install

COPY entrypoint.sh entrypoint.sh
ENV INPUT_FLAGS "-r 1920x1080 -f 60"
ENV OUTPUT_FLAGS "-w ./www"
ENTRYPOINT ["./entrypoint.sh"]

entrypoint.sh

#!/bin/sh
input="input_libcamera.so $INPUT_FLAGS"
output="output_http.so $OUTPUT_FLAGS"
./mjpg_streamer -i "$input" -o "$output"

Things to note about this Dockerfile:

  • The Raspberry Pi repository was added so that the libcamera-dev dependency could be downloaded.
  • All inputs other than input_libcamera and all outputs other than output_http were removed from CMakeLists.txt.
    • This is primarily to remove the dependency on libopencv-dev, which would have involved downloading over 700MB of data.
  • This image is not hermetic, meaning ArduCam could make changes to their fork that break the image or even introduce nefarious backdoors at any time.
    • Always vet any code you compile and run.

To use this image, all /dev/video* and /dev/v4l2* devices need to be passed through to the container. udev also needs to be passed through to the container. In my case, I was lazy and simply ran the container in privileged mode and mounted /run/udev:/run/udev:ro.

Input flags and output flags are defined using the INPUT_FLAGS and OUTPUT_FLAGS environment variables.

OctoPrint

Adding OctoPrint is very straightforward, since an official image is provided: octoprint/octoprint.

Being lazy yet again, I opted to run OctoPrint in privileged mode as well, but you only need to pass through the printer to the container (/dev/ttyACM0 in my case for a Prusa Mini+).

To get a couple of plugins to work better, I also mounted:

  • NavbarTemp
    • /proc/cpuinfo:/proc/cpuinfo:ro
    • /usr/bin/vcgencmd:/usr/bin/vcgencmd:ro
    • /usr/lib/aarch64-linux-gnu/libvcos.so.0:/usr/lib/aarch64-linux-gnu/libvcos.so.0:ro
    • /usr/lib/aarch64-linux-gnu/libvchiq_arm.so.0:/usr/lib/aarch64-linux-gnu/libvchiq_arm.so.0:ro
  • CameraSettings (done implicitly through privileged mode)
    • /dev/video*
    • /dev/v4l2*

Because mjpg_streamer is run in a separate container, the OctoPrint container does not need to be configured with ENABLE_MJPG_STREAMER.

A couple of other settings that will need to be changed from default for OctoPrint and optionally Octolapse to function correctly:

  • Settings > OctoPrint > Server > Commands > Restart OctoPrint
    • s6-svc -r /var/run/s6/services/octoprint
    • This allows OctoPrint to signal to the supervisor to restart.
  • Settings > Features > Webcam & Timelapse > Webcam > Stream URL
    • http://<LOCAL_IP>:<PORT>/?action=stream
    • The <LOCAL_IP>:<PORT> should point to your mjpg_streamer instance.
  • Settings > Plugins > Octolapse > Camera > [Your Camera] > Webcam Setup
    • Base Address: http://<LOCAL_IP>:<PORT>
    • Snapshot Address: {camera_address}?action=snapshot
    • Stream Address Template: http://<LOCAL_IP>:<PORT>/?action=stream

docker-compose

The final docker-compose.yaml I used for the setup was:

version: "3"

services:
  mjpg-streamer:
    build:
      context: ./mjpg-streamer
      dockerfile: Dockerfile
    restart: always
    privileged: true
    ports:
      - 8080:8080
    environment:
      - INPUT_FLAGS=-r 1920x1080 -f 30
      - OUTPUT_FLAGS=-w ./www
    volumes:
      - /run/udev:/run/udev:ro

  octoprint:
    image: octoprint/octoprint:latest
    restart: always
    privileged: true
    ports:
      - 80:80
    devices:
      - /dev/ttyACM0:/dev/ttyACM0
    volumes:
      - ./data/octoprint:/octoprint
      - /proc/cpuinfo:/proc/cpuinfo:ro
      - /usr/bin/vcgencmd:/usr/bin/vcgencmd:ro
      - /usr/lib/aarch64-linux-gnu/libvcos.so.0:/usr/lib/aarch64-linux-gnu/libvcos.so.0:ro
      - /usr/lib/aarch64-linux-gnu/libvchiq_arm.so.0:/usr/lib/aarch64-linux-gnu/libvchiq_arm.so.0:ro

Final thoughts

This approach of using Docker to create a separation of concerns between the OS, OctoPrint, and mjpg_streamer is, in my opinion, a cleaner and better way to run OctoPrint than through the OctoPi image.

Will I create an image for this?

No. That would defeat the entire point.

Will there be degraded performance due to Docker overhead?

Compared to running natively, there will definitely be a non-zero amount of reduced performance. However, I am running this setup on a Raspberry Pi 4B with 2GB RAM, and performance has not been an issue.

GitHub Files

In addition to the file contents I've pasted above, all the files that make up this setup are in GitHub - ruiqimao/octoprint-libcamera: libcamera with OctoPrint using Docker.

3 Likes

Great guide, very detailed. As to the concern of using a nightly build of OctoPi, the current release candidate of OctoPi is using Bullseye and I bet they would love to get some feedback related to getting libcamera working in the default image.

Are you running this on a 64 bit raspberry pi os?

Yes, with 64-bit Raspbian