Using USB webcam on Colibri iMX8X

Hi,

I’m trying to get a USB webcam to run on a Colibri iMX8X, but - as I don’t know much about Linux, Docker OR Portainer - am stuck and can’t really tell what to search for / try next.
My goal is to eventually display the webcam feed in a Python (PySide) GUI and be able to take pictures. For that, I managed to make a project using the VS Code plugin with a Weston container and a Pyside2 container that currently displays an empty GUI trying to display webcam images using OpenCV.
Please bare with me, this is an experimental project, someone else set up the hardware many months ago and I only really know my way around the Python parts of all this.
Trying to follow How to use cameras on Torizon and other things I googled, I added permissions and mounts to my docker compose file and v4l-utils to my Dockerfile:

docker-compose.yml
#version: "3.9"
services:
  pyside-container-debug:
    build:
      context: .
      dockerfile: Dockerfile.debug
    image: ${LOCAL_REGISTRY}:5002/pyside-container-debug:${TAG}
    ports:
      - ${DEBUG_SSH_PORT}:${DEBUG_SSH_PORT}
      - ${DEBUG_PORT1}:${DEBUG_PORT1}
    volumes:
      - type: bind
        source: /tmp
        target: /tmp
      - type: bind
        source: /dev
        target: /dev
      # for camera access:
      - type: bind
        source: /sys
        target: /sys
      - type: bind
        source: /var/run/dbus
        target: /var/run/dbus
    # trying to add webcam:
    devices:
      #- /dev/video0:/dev/video0
      #- /dev/video0:/dev/video1
      #- /dev/video0:/dev/video2
      #- /dev/video0:/dev/video3
      - /dev/video0:/dev/video4
      - /dev/video0:/dev/video5
      - /dev/video0:/dev/video6
      - /dev/video0:/dev/video7
    device_cgroup_rules:
      # ... for tty
      - "c 4:* rmw"
      # ... for /dev/input devices
      - "c 13:* rmw"
      - "c 199:* rmw"
      # ... for /dev/dri devices
      - "c 226:* rmw"
    depends_on: [
      weston
    ]

  weston:
    image: commontorizon/weston${GPU}:3.3.2
    environment:
      - ACCEPT_FSL_EULA=1
    # Required to get udev events from host udevd via netlink
    network_mode: host
    volumes:
      - type: bind
        source: /tmp
        target: /tmp
      - type: bind
        source: /dev
        target: /dev
      - type: bind
        source: /run/udev
        target: /run/udev
      # for camera access:
      - type: bind
        source: /sys
        target: /sys
      - type: bind
        source: /var/run/dbus
        target: /var/run/dbus
    # trying to add webcam:
    devices:
      #- /dev/video0:/dev/video0
      #- /dev/video0:/dev/video1
      #- /dev/video0:/dev/video2
      #- /dev/video0:/dev/video3
      - /dev/video0:/dev/video4
      - /dev/video0:/dev/video5
      - /dev/video0:/dev/video6
      - /dev/video0:/dev/video7
    cap_add:
      - CAP_SYS_TTY_CONFIG
    # Add device access rights through cgroup...
    device_cgroup_rules:
      # ... for tty
      - "c 4:* rmw"
      # ... for /dev/input devices
      - "c 13:* rmw"
      - "c 199:* rmw"
      # ... for /dev/dri devices
      - "c 226:* rmw"
Dockerfile.debug
# ARGUMENTS --------------------------------------------------------------------
##
# Board architecture
##
ARG IMAGE_ARCH=

##
# Base container version
##
ARG BASE_VERSION=3.3.1

##
# Debug port
##
ARG DEBUG_SSH_PORT=

##
# Run as
##
ARG SSHUSERNAME=

##
# Directory of the application inside container
##
ARG APP_ROOT=

##
# Board GPU vendor prefix
##
ARG GPU=


FROM --platform=linux/${IMAGE_ARCH} \
     commontorizon/qt5-wayland${GPU}:${BASE_VERSION} AS debug

ARG IMAGE_ARCH
ARG GPU
ARG DEBUG_SSH_PORT
ARG SSHUSERNAME
ARG APP_ROOT

# SSH for remote debug
EXPOSE ${DEBUG_SSH_PORT}
EXPOSE 6512

ENV QT_QPA_PLATFORM="wayland"

# Make sure we don't get notifications we can't answer during building.
ENV DEBIAN_FRONTEND="noninteractive"

# for vivante GPU we need some "special" sauce
RUN apt-get -q -y update && \
        if [ "${GPU}" = "-vivante" ] || [ "${GPU}" = "-imx8" ]; then \
            apt-get -q -y install \
            imx-gpu-viv-wayland-dev \
        ; else \
            apt-get -q -y install \
            libgl1 \
        ; fi \
    && \
    apt-get clean && apt-get autoremove && \
    rm -rf /var/lib/apt/lists/*

# your regular RUN statements here
# Install required packages
RUN apt-get -q -y update && \
    apt-get -q -y install \
    v4l-utils \
    openssl \
    openssh-server \
    rsync \
    file \
    screen \
    python3-minimal \
    python3-pip \
    python3-setuptools \
    python3-venv \
    qml-module-qtquick-controls \
    qml-module-qtquick-controls2 \
    qml-module-qtquick2 \
    python3-pyside2.qtwidgets \
    python3-pyside2.qtgui \
    python3-pyside2.qtqml \
    python3-pyside2.qtcore \
    python3-pyside2.qtquick \
    python3-pyside2.qtnetwork \
    qml-module-qtquick-dialogs \
    qtwayland5 \
    && apt-get clean && apt-get autoremove && \
    rm -rf /var/lib/apt/lists/*

# edge case for amd64
# this prevent the error: texture atlas allocation failed
RUN apt-get -q -y update && \
    if [ "${IMAGE_ARCH}" = "amd64" ]; then \
        apt-get -q -y install \
        libqt5quick5 \
    ; fi \
    && \
    apt-get clean && apt-get autoremove && \
    rm -rf /var/lib/apt/lists/*

# automate for torizonPackages.json
RUN apt-get -q -y update && \
    apt-get -q -y install \
# DO NOT REMOVE THIS LABEL: this is used for VS Code automation
    # __torizon_packages_dev_start__
    # __torizon_packages_dev_end__
# DO NOT REMOVE THIS LABEL: this is used for VS Code automation
    && \
    apt-get clean && apt-get autoremove && \
    rm -rf /var/lib/apt/lists/*

# Create virtualenv
RUN python3 -m venv ${APP_ROOT}/.venv --system-site-packages

# Install pip packages on venv
COPY requirements-debug.txt /requirements-debug.txt
RUN . ${APP_ROOT}/.venv/bin/activate && \
    pip3 install --upgrade pip && pip3 install -r requirements-debug.txt && \
    rm requirements-debug.txt

# ⚠️ DEBUG PURPOSES ONLY!!
# THIS CONFIGURES AN INSECURE SSH CONNECTION

# create folders needed for the different components
# configures SSH access to the container and sets environment by default
RUN mkdir /var/run/sshd && \
    sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' \
        -i /etc/pam.d/sshd && \
    if test $SSHUSERNAME != root ; \
        then mkdir -p /home/$SSHUSERNAME/.ssh ; \
        else mkdir -p /root/.ssh ; fi && \
    echo "PermitUserEnvironment yes" >> /etc/ssh/sshd_config && \
    echo "PermitRootLogin yes" >> /etc/ssh/sshd_config && \
    echo "Port ${DEBUG_SSH_PORT}" >> /etc/ssh/sshd_config && \
    echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config && \
    echo "PermitEmptyPasswords yes" >> /etc/ssh/sshd_config && \
    su -c "env" $SSHUSERNAME > /etc/environment && \
    echo "$SSHUSERNAME:" | chpasswd -e

RUN rm -r /etc/ssh/ssh*key && \
    dpkg-reconfigure openssh-server

# Copy the application source code in the workspace to the $APP_ROOT directory
# path inside the container, where $APP_ROOT is the torizon_app_root
# configuration defined in settings.json
COPY --chown=$SSHUSERNAME:$SSHUSERNAME ./src ${APP_ROOT}/src

CMD [ "/usr/sbin/sshd", "-D" ]

I commented out devices 0-3 because of the output of these two commands, not sure if that’s correct:

devices
torizon@colibri-imx8x-07329158:~$ ls -ltrh /dev/video*
crw-rw---- 1 root video 81, 1 Dec 18 17:17 /dev/video1
crw-rw---- 1 root video 81, 0 Dec 18 17:17 /dev/video0
crw-rw---- 1 root video 81, 3 Dec 18 17:17 /dev/video3
crw-rw---- 1 root video 81, 2 Dec 18 17:17 /dev/video2
crw-rw---- 1 root video 81, 5 Jan  8 11:21 /dev/video5
crw-rw---- 1 root video 81, 6 Jan  8 11:21 /dev/video6
crw-rw---- 1 root video 81, 4 Jan  8 11:21 /dev/video4
crw-rw---- 1 root video 81, 7 Jan  8 11:21 /dev/video7

torizon@colibri-imx8x-07329158:~$ for I in /sys/class/video4linux/*; do cat $I/name; done
amphion-vpu-decoder
amphion-vpu-encoder
mxc-jpeg-dec
mxc-jpeg-enc
Logitech BRIO
Logitech BRIO
Logitech BRIO
Logitech BRIO

Plugging in the webcam, dmesg says this:

[ 3889.518830] usb 1-1.2.1.3.2: Found UVC 1.00 device Logitech BRIO (046d:085e)
[ 3889.524534] input: Logitech BRIO as /devices/platform/bus@5b000000/5b110000.usb/5b130000.usb/xhci-hcd.1.auto/usb1/1-1/1-1.2/1-1.2.1/1-1.2.1.3/1-1.2.1.3.2/1-1.2.1.3.2:1.0/input/input9

Running v4l2-ctl --list-devices doesn’t print anything for the webcam though:

amphion vpu decoder (platform: amphion-vpu):
        /dev/video0

I don’t really know where to go from here. I’m not sure if I even need the v4l stuff (rather GStreamer?), but what it currently prints indicates to me that the webcam isn’t correctly recognized.
How to get it to do that I don’t know, some threads on this forum make it sound like it should “just work”.

I’m also confused by the users. The output from v4l2-ctl I got using a “Container console” on Portainer running as root user, whereas when ssh-ing with the torizon user (the other console outputs I posted), v4l2-ctl is an unrecognized command.

tdx-info Output
Software summary
------------------------------------------------------------
Bootloader:               U-Boot
Kernel version:           5.15.129-6.5.0+git.6f8fd49366db #1-TorizonCore SMP PREEMPT Fri Dec 22 11:15:52 UTC 2023
Kernel command line:      root=LABEL=otaroot rootfstype=ext4 quiet logo.nologo vt.global_cursor_default=0 plymouth.ignore-serial-consoles splash fbcon=map:3 ostree=/ostree/boot.1/torizon/7e7d5c182b8db9a981d1f942cee2a808b3535fa86862471d67f718c4959f44df/0
Distro name:              NAME="TorizonCore"
Distro version:           VERSION_ID=6.5.0-build.8
Distro variant:           VARIANT="Docker"
Hostname:                 colibri-imx8x-07329158
------------------------------------------------------------

Hardware info
------------------------------------------------------------
HW model:                 Toradex Colibri iMX8QXP on Colibri Evaluation Board V3
Toradex version:          0038 V1.0D
Serial number:            07329158
Processor arch:           aarch64
------------------------------------------------------------

Hi, I’m the Toradex AI Assistant. The Toradex Support team will follow up with you shortly!

Did you like my answer?

:+1: :-1:


Hello,

To successfully use a USB webcam with your Colibri iMX8X module and display the feed in a Python GUI, you’ll need to ensure your Docker containers are properly configured to access the webcam hardware. Here’s a step-by-step guide to help you troubleshoot and set up your system:

1. Verify Webcam Recognition

  • Check Device Detection: Use dmesg to confirm that the webcam is detected when plugged in. Your logs indicate that the Logitech BRIO is recognized.
  • List Video Devices: Run ls /dev/video* to see available video devices. Your output shows multiple /dev/videoX entries, which is typical.

2. Configure Docker Containers

  • Map the Correct Device: In your docker-compose.yml, ensure you map the correct /dev/videoX device to the container. For instance, if /dev/video4 is your webcam, map it as - /dev/video4:/dev/video0 inside the container.
  • Simplify Device Mappings: Remove unnecessary device mappings to avoid confusion.

3. Set Device Permissions

  • User Permissions: Ensure the user running the container has permissions to access video devices. You might need to add the user to the video group or run the container with elevated privileges.

4. Test with GStreamer

  • GStreamer Pipeline: Use GStreamer to test the video stream:
    gst-launch-1.0 v4l2src device=/dev/video0 ! videoconvert ! autovideosink
    
    This command should display the webcam feed if everything is configured correctly.

5. Integrate with Python and OpenCV

  • OpenCV Setup: Once the webcam is accessible, use OpenCV in your Python application to capture and display video. Ensure OpenCV is installed in your container and use the cv2.VideoCapture class to access the video stream.

6. Address User and Command Confusion

  • Command Availability: The v4l2-ctl command should be available if v4l-utils is installed in the container. Make sure you are executing commands in the correct environment (container vs. host).

By following these steps, you should be able to configure your setup to access and display the webcam feed in your Python GUI. If issues persist, double-check device mappings and permissions, and ensure all necessary software components are correctly installed in your containers.

Best regards,
Toradex AI Assistant

Please, also check if these links can help you solve your problem:

Display output from mipi camera.

Okay, I believe I am a step further, after adding - "c 81:* rmw" to the device_cgroup_rules sections in the compose file (got the 81 from ls -l /dev/video* following this article), v4l2 now outputs this:

v4l2-ctl --list-devices
root@64bc7770d04f:/# v4l2-ctl --list-devices
amphion vpu decoder (platform: amphion-vpu):
        /dev/video0
        /dev/video1

mxc-jpeg codec (platform:58400000.jpegdec):
        /dev/video2

mxc-jpeg codec (platform:58450000.jpegenc):
        /dev/video3

Logitech BRIO (usb-xhci-hcd.1.auto-1.2.1.3.2):
        /dev/video4
        /dev/video5
        /dev/video6
        /dev/video7

…but also

v4l2-ctl -D
root@64bc7770d04f:/# v4l2-ctl -D
Driver Info:
        Driver name      : amphion-vpu
        Card type        : amphion vpu decoder
        Bus info         : platform: amphion-vpu
        Driver version   : 5.15.129
        Capabilities     : 0x84204000
                Video Memory-to-Memory Multiplanar
                Streaming
                Extended Pix Format
                Device Capabilities
        Device Caps      : 0x04204000
                Video Memory-to-Memory Multiplanar
                Streaming
                Extended Pix Format

…which I interpret as the “amphion-vpu” device being used instead of my webcam, even when I remove all devices apart from /dev/video4 in the compose file.
Specifying video4 gives this output:

v4l2-ctl --device 4 -D
root@64bc7770d04f:/# v4l2-ctl --device 4 -D
Driver Info:
        Driver name      : uvcvideo
        Card type        : Logitech BRIO
        Bus info         : usb-xhci-hcd.1.auto-1.2.1.3.2
        Driver version   : 5.15.129
        Capabilities     : 0x84a00001
                Video Capture
                Metadata Capture
                Streaming
                Extended Pix Format
                Device Capabilities
        Device Caps      : 0x04200001
                Video Capture
                Streaming
                Extended Pix Format

…notably missing the “Media Driver Info” parts that are shown on the How to use cameras on Torizon page.

Oof. After adding the gstreamer libs to the Dockerfile (except for gstreamer1.0-doc which produced an error) and successfully getting a webcam stream using
gst-launch-1.0 v4l2src device='/dev/video3' ! "video/x-raw, format=YUY2, framerate=5/1, width=640, height=480" ! fpsdisplaysink video-sink=waylandsink text-overlay=false sync=false
from the documentation page, I now can’t debug my container anymore (I always use VS Code’s Run > Start Debugging because I don’t know what to call manually), always get this error:
connect ECONNREFUSED 192.168.178.89:6512
Help please?

Hi @uwdlg

Thanks for this very detailed explanation of the issue.
That’s a lot of information, so I’m going to go one step at a time.

I actually did a very similar project some time ago, I’m going to attach the files here, maybe it can provide some insights for your development.

Dockerfile
# ARGUMENTS --------------------------------------------------------------------
##
# Board architecture
##
ARG IMAGE_ARCH=arm64

##
# Base container version
##
ARG BASE_VERSION=3.2.1

##
# Directory of the application inside container
##
ARG APP_ROOT="mpu-test_cv2"

##
# Board GPU vendor prefix
##
ARG GPU="-vivante"


FROM --platform=linux/${IMAGE_ARCH} \
     torizon/qt5-wayland${GPU}:${BASE_VERSION} AS deploy

ARG IMAGE_ARCH
ARG GPU
ARG APP_ROOT

# for vivante GPU we need some "special" sauce
RUN apt-get -q -y update && \
        if [ "${GPU}" = "-vivante" ] || [ "${GPU}" = "-imx8" ]; then \
            apt-get -q -y install \
            imx-gpu-viv-wayland-dev \
        ; else \
            apt-get -q -y install \
            libgl1 \
        ; fi \
    && \
    apt-get clean && apt-get autoremove && \
    rm -rf /var/lib/apt/lists/*

# Install python packages
RUN apt-get -q -y update && \
    apt-get -q -y install \
    python3-minimal \
    python3-pip \
    python3-dev \
    python3-venv \
    python3-pyside2.qtwidgets \
    python3-pyside2.qtgui \
    python3-pyside2.qtqml \
    python3-pyside2.qtcore \
    python3-pyside2.qtquick \
    python3-pyside2.qtnetwork \
    python3-pyside2.qtmultimedia \
    python3-pyside2.qtmultimediawidgets \
    && apt-get clean && apt-get autoremove && \
    rm -rf /var/lib/apt/lists/*

# Install pyqt packages
RUN apt-get -q -y update && \
    apt-get -q -y install \
    qtwayland5 \
    qtmultimedia5-dev \
    qml-module-qtquick-controls \
    qml-module-qtquick-controls2 \
    qml-module-qtquick2 \
    qml-module-qtquick-dialogs \
    qml-module-qtmultimedia \
    libqt5multimedia5-plugins \
    && apt-get clean && apt-get autoremove && \
    rm -rf /var/lib/apt/lists/*

# Install gstream packages
RUN apt-get -y update && apt-get install -y --no-install-recommends libgstreamer1.0-0 \
    gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad \
    gstreamer1.0-plugins-ugly gstreamer1.0-tools gstreamer1.0-OpenCV \
    python3-gst-1.0 gstreamer1.0-rtsp libgstrtspserver-1.0-0 gir1.2-gst-rtsp-server-1.0 && \
    apt-get clean && apt-get autoremove && rm -rf /var/lib/apt/lists/*

# Install others packages
RUN apt-get -q -y update && \
    apt-get -q -y install \
    build-essential \
    libcairo2-dev \
    libgirepository1.0-dev \
    v4l-utils \
    rsync \
    nano \
    && apt-get clean && apt-get autoremove && \
    rm -rf /var/lib/apt/lists/*

# Create virtualenv
RUN python3 -m venv ${APP_ROOT}/.venv --system-site-packages

# Install pip packages on venv
COPY requirements-release.txt /requirements-release.txt
RUN . ${APP_ROOT}/.venv/bin/activate && \
    pip3 install --upgrade pip && pip3 install --break-system-packages -r requirements-release.txt && \
    rm requirements-release.txt

USER torizon

# Copy the application source code in the workspace to the $APP_ROOT directory 
# path inside the container, where $APP_ROOT is the torizon_app_root 
# configuration defined in settings.json
COPY ./src ${APP_ROOT}/src

WORKDIR ${APP_ROOT}

ENV APP_ROOT=${APP_ROOT}
ENV QT_QPA_PLATFORM="wayland"
ENV QT_DEBUG_PLUGINS=0
ENV GST_PLUGIN_SCANNER="usr/lib/aarch64-linux-gnu/gstreamer1.0/gstreamer-1.0/gst-plugin-scanner"

# Activate and run the code
CMD . .venv/bin/activate && python3 src/main.py --no-sandbox
docker-compose.yml
version: '3.8'

services:
  weston:
    image: torizon/weston:${CT_TAG_WESTON}
    container_name: weston
    network_mode: host
    cap_add:
      - CAP_SYS_TTY_CONFIG
    volumes:
      - /dev:/dev
      - /tmp:/tmp
      - /run/udev:/run/udev
    devices:
      - "/dev/tty7:/dev/tty7"
    device_cgroup_rules:
      - 'c 4:* rmw'
      - 'c 13:* rmw'
      - 'c 199:* rmw'
      - 'c 226:* rmw'
    command: --developer weston-launch --tty=/dev/tty7 --user=torizon

  mpu:
    image: <my_image>
    container_name: mpu
    network_mode: host
    cap_add:
      - CAP_SYS_TTY_CONFIG
    volumes:
      - /dev:/dev
      - /tmp:/tmp
      - /run/udev:/run/udev
      - /var/run/dbus:/var/run/dbus
      - /dev/galcore:/dev/galcore
    environment:
      ACCEPT_FSL_EULA: '1'
      MPU_I2C_BUS: '4'
      MPU_DEVICE_ADDRESS: '0x69'
      MPU_dt: 0.001
    privileged: true
    depends_on:
      - weston
video.py
from PySide2.QtWidgets import QApplication, QWidget, QLabel, QVBoxLayout
from PySide2.QtGui import QImage, QPixmap
from PySide2.QtCore import Qt, QThread, Signal, QByteArray, QDataStream, QBuffer

import numpy as np

import gi
gi.require_version('Gst', '1.0')
from gi.repository import Gst, GObject

import sys
# Initialize GStreamer
Gst.init(None)


class VideoThread(QThread):
    frame_data_signal = Signal(bytes)
    width  = 1280
    height = 720

    def __init__(self, parent=None):
        super(VideoThread, self).__init__(parent)
        self.pipeline = None

    def run(self):
        # Set up the GStreamer pipeline
        self.pipeline = Gst.parse_launch(
            f"v4l2src device=/dev/video0 ! videoconvert ! video/x-raw,format=RGB,width={self.width},height={self.height} ! appsink name=sink emit-signals=True"
        )

        appsink = self.pipeline.get_by_name("sink")
        appsink.connect("new-sample", self.on_new_sample, appsink)

        # Start playing
        self.pipeline.set_state(Gst.State.PLAYING)

        # Run the GStreamer main loop
        loop = GObject.MainLoop()
        loop.run()

    def on_new_sample(self, sink, data):
        sample = sink.emit("pull-sample")
        if sample:
            buffer = sample.get_buffer()

            # Map the buffer to a readable format
            success, map_info = buffer.map(Gst.MapFlags.READ)
            if success:
                frame_data = map_info.data
                print(type(frame_data))
                self.frame_data_signal.emit(bytes(frame_data))

                # Unmap the buffer
                buffer.unmap(map_info)

            return Gst.FlowReturn.OK

    def stop_pipeline(self):
        if self.pipeline:
            self.pipeline.set_state(Gst.State.NULL)

class VideoPlayer(QWidget):
    width  = 1280
    height = 720

    def __init__(self):
        super(VideoPlayer, self).__init__()

        self.video_thread = VideoThread(self)

        self.label = QLabel(self)
        self.label.setAlignment(Qt.AlignCenter)

        layout = QVBoxLayout(self)
        layout.addWidget(self.label)

        self.video_thread.frame_data_signal.connect(self.update_frame)
        self.video_thread.start()

    def update_frame(self, frame_data):
        # Convert frame data to a NumPy array
        # frame_array = np.frombuffer(frame_data.data(), dtype=np.uint8).reshape((self.height, self.width, 3))
        # frame_rgb = (frame_array).copy(order='C')

        qimage = QImage(frame_data, self.width, self.height, QImage.Format_RGB888)

        # Set the image in the QLabel
        pixmap = QPixmap.fromImage(qimage)

        self.label.setPixmap(pixmap)

    def closeEvent(self, event):
        self.video_thread.stop_pipeline()
        self.video_thread.wait()
        event.accept()

In my example, I used the -v /dev:/dev and privileged: true. This basically gives to the container full permissions and full access to all the devices, which is not a best practice but may help you to debug during development stage.

I recommend taking a while to understand the general concept of a container.
But in a few words, the container is an environment isolated from your linux environment. So it don’t have access to your devices, such as a camera, unless you explicit allow it. This also includes libraries, binaries, files and everything.

Thank you for answering!
All my problems now seem to be related to gstreamer. Whenever I add the packages to the Dockerfile (I also tried the subset from yours, moving them to before/after other packages are installed), I get the “connection refused” error I described when I hit Start Debugging. I can’t tell what the underlying problem is, I’m unsure how to get more helpful error messages when debugging fails.
I believe I can rule out any network changes or errors from my python file running into a timeout as debugging always works again as soon as I remove the gstreamer packages from the Dockerfile.
I also tried creating a new project and copied in your files, but when I debug that (after removing the leftover numpy import) it doesn’t get past the gstreamer import:

Exception has occurred: ModuleNotFoundError
No module named 'gi'