Proxmox-for-Channels: Step-by-Step for Passing Through One or More USB Capture Cards to a Channels DVR LXC

If your preference is to use USB Capture Cards to create a virtual encoder, as opposed to a network encoder, and have Channels DVR installed in a Proxmox LXC, read on:

Plug your capture card into a USB port (USB3 port for a USB3 device), and from the Proxmox shell, make sure v4l-utils is installed:

root@pve6:~# apt update
Hit:1 http://deb.debian.org/debian trixie InRelease
Get:2 http://security.debian.org/debian-security trixie-security InRelease [43.4 kB]                                    
Hit:3 http://deb.debian.org/debian trixie-updates InRelease                                                                    
Get:4 http://security.debian.org/debian-security trixie-security/main amd64 Packages [252 kB]                                  
Get:5 https://pkgs.tailscale.com/stable/debian trixie InRelease                                        
Get:6 http://download.proxmox.com/debian/pve trixie InRelease [4,357 B]
Fetched 306 kB in 0s (1,825 kB/s)   
All packages are up to date.    
root@pve6:~# apt install v4l-utils
v4l-utils is already the newest version (1.30.1-1).
The following packages were automatically installed and are no longer required:
  proxmox-kernel-7.0.12-1-pve-signed   proxmox-kernel-7.0.14-3-pve-signed  proxmox-kernel-7.0.14-6-pve-signed
  proxmox-kernel-7.0.14-12-pve-signed  proxmox-kernel-7.0.14-4-pve-signed  proxmox-kernel-7.0.6-2-pve-signed
Use 'apt autoremove' to remove them.

Summary:
  Upgrading: 0, Installing: 0, Removing: 0, Not Upgrading: 0

Then identify your capture card. I've already installed one that was assigned video0, and now I'm going to add a second card:

root@pve6:~# v4l2-ctl --list-devices
USB3.0 Video: USB3.0 Video (usb-0000:00:14.0-3):
        /dev/video2
        /dev/video3
        /dev/media1

USB3.0 Video: USB3.0 Video (usb-0000:00:14.0-4):
        /dev/video0
        /dev/video1
        /dev/media0

The second card's top line device is /dev/video2, so that's the device we'll pass through for video. Next we need to identify the sound devices associated with this card in /dev/snd:

root@pve6:~# ls -la /dev/snd
total 0
drwxr-xr-x  4 root root      420 Sep  7 13:15 .
drwxr-xr-x 22 root root     4920 Sep  7 13:15 ..
drwxr-xr-x  2 root root       80 Sep  7 13:15 by-id
drwxr-xr-x  2 root root      140 Sep  7 13:15 by-path
crw-rw----  1 root audio 116, 12 Aug  1 06:02 controlC0
crw-rw----  1 root audio 116, 14 Sep  7 09:30 controlC1
crw-rw----  1 root audio 116, 16 Sep  7 13:15 controlC2
crw-rw----  1 root audio 116, 10 Aug  1 06:02 hwC0D0
crw-rw----  1 root audio 116, 11 Aug  1 06:02 hwC0D2
crw-rw----  1 root audio 116,  3 Aug  1 06:02 pcmC0D0c
crw-rw----  1 root audio 116,  2 Aug  1 06:02 pcmC0D0p
crw-rw----  1 root audio 116,  4 Aug  1 06:02 pcmC0D1p
crw-rw----  1 root audio 116,  5 Aug  1 06:02 pcmC0D2c
crw-rw----  1 root audio 116,  6 Aug  1 06:02 pcmC0D3p
crw-rw----  1 root audio 116,  7 Aug  1 06:02 pcmC0D7p
crw-rw----  1 root audio 116,  8 Aug  1 06:02 pcmC0D8p
crw-rw----  1 root audio 116,  9 Aug  1 06:02 pcmC0D9p
crw-rw----  1 root audio 116, 13 Sep  7 09:30 pcmC1D0c
crw-rw----  1 root audio 116, 15 Sep  7 13:15 pcmC2D0c
crw-rw----  1 root audio 116,  1 Aug  1 06:02 seq
crw-rw----  1 root audio 116, 33 Aug  1 06:02 timer

Our previous devices were pcmC1D0c (stands for card 1, device 0), and controlC1 (as in control card1). The latest dates and times correspond to when I plugged-in the new device, so that's a good cross-check. Our new capture card audio is pcmC2D0c (card 2, device0), and controlC2 (control card 2).

Excellent, now we have what we need to pass specific devices through to our LXC. In the screenshot, you can see we're already passing through the 3 devices from earlier:

Screenshot 2026-09-07 at 13-52-47 pve6 - Proxmox Virtual Environment

I know from doing a cat /etc/group earlier in my LXC, the video group is 44 and the audio group is 29, so I'll add the devices using those group IDs:

Screenshot 2026-09-07 at 14-00-51 pve6 - Proxmox Virtual Environment

And when all 3 new devices are added:

Screenshot 2026-09-07 at 14-03-01 pve6 - Proxmox Virtual Environment

Then, inside the Channels DVR LXC, we can confirm our devices are present:

root@channels:~# ls -la /dev/video*
crw-rw---- 1 root video 81, 0 Sep  7 09:41 /dev/video0
crw-rw---- 1 root video 81, 2 Sep  7 14:01 /dev/video2
root@channels:~# ls -la /dev/snd
total 0
drwxr-xr-x 2 root root      120 Sep  7 14:02 .
drwxr-xr-x 9 root root      580 Sep  7 14:01 ..
crw-rw---- 1 root audio 116, 14 Sep  7 09:41 controlC1
crw-rw---- 1 root audio 116, 16 Sep  7 14:02 controlC2
crw-rw---- 1 root audio 116, 13 Sep  7 11:27 pcmC1D0c
crw-rw---- 1 root audio 116, 15 Sep  7 14:02 pcmC2D0c

One more detail here, is that we need to find the name of the audio device associated with the new card. This is the same process I used with first card. We'll use another utility package for this, the alsa-utils.

root@channels:~# apt update
Hit:1 http://deb.debian.org/debian trixie InRelease
Get:2 http://security.debian.org trixie-security InRelease [43.4 kB]                                                      
Get:3 http://deb.debian.org/debian trixie-updates InRelease [47.3 kB]                                                                                        
Get:4 http://security.debian.org trixie-security/main amd64 Packages [252 kB]                                                          
Get:5 http://security.debian.org trixie-security/main Translation-en [154 kB]                        
Get:6 https://pkgs.tailscale.com/stable/debian trixie InRelease                                       
Get:7 https://dl.google.com/linux/chrome-stable/deb stable InRelease [2548 B]
Get:8 https://dl.google.com/linux/chrome-stable/deb stable/main amd64 Packages [1403 B]
Fetched 507 kB in 1s (694 kB/s)    
1 package can be upgraded. Run 'apt list --upgradable' to see it.
root@channels:~# apt install alsa-utils
alsa-utils is already the newest version (1.2.14-1).
Summary:
  Upgrading: 0, Installing: 0, Removing: 0, Not Upgrading: 1

And then list the available audio devices:

root@channels:~# arecord -l
**** List of CAPTURE Hardware Devices ****
card 1: Video [USB3.0 Video], device 0: USB Audio [USB Audio]
  Subdevices: 0/1
  Subdevice #0: subdevice #0
card 2: Video_1 [USB3.0 Video], device 0: USB Audio [USB Audio]
  Subdevices: 0/1
  Subdevice #0: subdevice #0

We used hw:Video,0 previously, so our new device is defined with:

hw:Video_1,0

Now, from the Channels WebUI, we'll have a look at our Custom Channels Source I set up for the first capture card, and we'll add a second. This source, plus your capture card(s) are what create your virtual encoder:

And the text:

#EXTM3U

#EXTINF:-1, channel-id="usb-hdmi1",channel-number="capture1",USB HDMI Capture1
capture://v4l2/video0/hw:Video,0/?framerate=60&width=1920&height=1080

#EXTINF:-1, channel-id="usb-hdmi2",channel-number="capture2",USB HDMI Capture2
capture://v4l2/video2/hw:Video_1,0/?framerate=60&width=1920&height=1080

Save it, and once guide data has refreshed, you can go to the "Capture" source in your EPG and test that each allows you to view your streaming stick. The channel numbers that Channels assigns, get plugged in to your new virtual encoder URL -- which looks like this:

http://channels:8089/devices/M3U-Capture/channels/90474/stream.mpg?format=ts&codec=copy

The "channel number" to be used in each URL can be viewed in the EPG, and would replace 90474. Replace "channels" with your LAN IP address or hostname (if you know it resolves). "M3U-Capture" is correct if your source was named "Capture".

These virtual encoder URLs can now be used in ah4c, along with their attached streaming sticks!

And here's what the transcoding workload looks like on my iGPU, with both Capture Cards in use:

intel-gpu-top: Intel Raptorlake_s (Gen12) @ /dev/dri/card0 -  797/1301 MHz;  25% RC6;  1.04/69.02 W;      355 irqs/s

         ENGINES     BUSY                                                                                                                                          MI_SEMA MI_WAIT
       Render/3D   20.45% |███████████████████████████▌                                                                                                          |      0%      0%
         Blitter    0.00% |                                                                                                                                      |      0%      0%
           Video    8.35% |███████████▎                                                                                                                          |      0%      0%
    VideoEnhance   11.05% |██████████████▉                                                                                                                       |      0%      0%

    PID      MEM      RSS              Render/3D                            Blitter                              Video                            VideoEnhance             NAME   
  53400   99492K   88604K |███▋                              ||                                  ||█▌                                ||█▉                                | ffmpeg 
  52413     150M  140976K |███▍                              ||                                  ||█▌                                ||██                                | ffmpeg 
1101969   25024K   25024K |                                  ||                                  ||                                  ||                                  | ah4c   
1 Like

I think I've come up with a reasonably simple solution for dealing with USB capture cards changing device numbers after a Proxmox host reboot.

My setup has two identical MACROSILICON USB capture cards. Channels DVR accesses them through an M3U like this:

#EXTM3U

#EXTINF:-1, channel-id="usb-hdmi1",channel-number="capture1",USB HDMI Capture1
capture://v4l2/video0/hw:Video,0/?framerate=60&width=1920&height=1080

#EXTINF:-1, channel-id="usb-hdmi2",channel-number="capture2",USB HDMI Capture2
capture://v4l2/video2/hw:Video_1,0/?framerate=60&width=1920&height=1080

The problem is that /dev/video0, /dev/video2, and the corresponding ALSA card numbers aren't necessarily persistent across a Proxmox host reboot.

The capture cards themselves can be identified persistently by their USB serial numbers. In my case:

75401744
62279665

So rather than trying to force Linux to enumerate the devices in a particular order, I wrote a small script that runs on the Proxmox host and figures out what Linux actually assigned to each capture card.

For each serial number, it:

  1. Uses /dev/v4l/by-id/ to determine its current /dev/videoX device.
  2. Uses udevadm and the same USB serial number to determine its current ALSA card number.
  3. Compares those devices with the current Proxmox LXC devN passthrough entries.
  4. Uses pct set to update only the entries that have actually changed.

So after a reboot I can simply run:

passthrough-checker 103

If everything is already correct, I get something like:

Capture card 75401744:
  Video resolved to /dev/video0
  Audio resolved to ALSA card C0
  dev3: OK      /dev/video0
  dev4: OK      /dev/snd/pcmC0D0c
  dev5: OK      /dev/snd/controlC0

Capture card 62279665:
  Video resolved to /dev/video2
  Audio resolved to ALSA card C1
  dev6: OK      /dev/video2
  dev7: OK      /dev/snd/pcmC1D0c
  dev8: OK      /dev/snd/controlC1

If Linux enumerated something differently, the script fixes it:

dev4: UPDATE
         old: /dev/snd/pcmC0D0c,gid=29
         new: /dev/snd/pcmC1D0c,gid=29

Here's the script I'm using:

#!/bin/bash
set -euo pipefail

die() {
    echo "ERROR: $*" >&2
    exit 1
}

# -------------------------------------------------------------------
# CONFIGURATION
# -------------------------------------------------------------------

configure_ct() {
    case "$CT" in

        101)
            # Intel GPU only
            GPU_PCI="0000:00:02.0"
            GPU_DEV="1"
            GPU_GID="44"
            ;;

        103)
            # Intel GPU (optional/bonus)
            GPU_PCI="0000:00:02.0"
            GPU_DEV="1"
            GPU_GID="44"

            # Capture cards:
            # "SERIAL:VIDEO_DEV:PCM_DEV:CONTROL_DEV"
            #
            # VIDEO_DEV   = Proxmox devN for V4L2 video
            # PCM_DEV     = Proxmox devN for ALSA capture
            # CONTROL_DEV = Proxmox devN for ALSA control
            CAPTURE_DEVICES=(
                "75401744:3:4:5"
                "62279665:6:7:8"
            )
            ;;

        *)
            die "No device configuration defined for CT $CT"
            ;;
    esac
}

# -------------------------------------------------------------------
# RESOLVERS
# -------------------------------------------------------------------

resolve_gpu() {
    local pci="$1"
    local path

    path=$(readlink -f "/dev/dri/by-path/pci-${pci}-card") ||
        die "Unable to resolve GPU $pci"

    [[ -c "$path" ]] ||
        die "$path is not a character device"

    echo "$path"
}

resolve_video() {
    local serial="$1"
    local path

    path=$(readlink -f \
        "/dev/v4l/by-id/usb-MACROSILICON_USB3.0_Video_${serial}-video-index0") ||
        die "Unable to resolve video device for serial $serial"

    [[ -c "$path" ]] ||
        die "$path is not a character device"

    echo "$path"
}

resolve_audio_card() {
    local serial="$1"
    local pcm found

    for pcm in /dev/snd/pcmC*D0c; do
        [[ -c "$pcm" ]] || continue

        found=$(
            udevadm info -a -n "$pcm" 2>/dev/null |
            sed -n 's/.*ATTRS{serial}=="\([^"]*\)".*/\1/p' |
            head -n1
        )

        if [[ "$found" == "$serial" ]]; then
            basename "$pcm" |
                sed -n 's/pcmC\([0-9]*\)D0c/\1/p'
            return 0
        fi
    done

    return 1
}

# -------------------------------------------------------------------
# UPDATE ONLY IF NECESSARY
# -------------------------------------------------------------------

set_device() {
    local devnum="$1"
    local path="$2"
    local gid="$3"

    local wanted="${path},gid=${gid}"
    local current

    current=$(
        pct config "$CT" |
        sed -n "s/^dev${devnum}: //p"
    )

    if [[ "$current" == "$wanted" ]]; then
        echo "  dev${devnum}: OK      ${path}"
        return
    fi

    if [[ -z "$current" ]]; then
        echo "  dev${devnum}: ADD"
        echo "           new: ${wanted}"
    else
        echo "  dev${devnum}: UPDATE"
        echo "           old: ${current}"
        echo "           new: ${wanted}"
    fi

    pct set "$CT" \
        --dev${devnum} "path=${path},gid=${gid}"
}

# -------------------------------------------------------------------
# MAIN
# -------------------------------------------------------------------

[[ $# -ge 1 ]] ||
    die "Usage: $0 CTID [--start]"

CT="$1"
START=0

[[ "${2:-}" == "--start" ]] && START=1

GPU_PCI=""
GPU_DEV=""
GPU_GID=""
CAPTURE_DEVICES=()

configure_ct

echo "Resolving devices for CT $CT..."

# -------------------------------------------------------------------
# GPU
# -------------------------------------------------------------------

if [[ -n "$GPU_PCI" ]]; then
    GPU_PATH=$(resolve_gpu "$GPU_PCI")

    echo
    echo "GPU $GPU_PCI resolved to $GPU_PATH"

    set_device "$GPU_DEV" "$GPU_PATH" "$GPU_GID"
fi

# -------------------------------------------------------------------
# CAPTURE CARDS
# -------------------------------------------------------------------

for entry in "${CAPTURE_DEVICES[@]}"; do

    IFS=: read -r SERIAL VIDEO_DEV PCM_DEV CONTROL_DEV <<< "$entry"

    VIDEO=$(resolve_video "$SERIAL") ||
        die "Unable to find video device for serial $SERIAL"

    CARD=$(resolve_audio_card "$SERIAL") ||
        die "Unable to find USB audio device with serial $SERIAL"

    PCM="/dev/snd/pcmC${CARD}D0c"
    CONTROL="/dev/snd/controlC${CARD}"

    [[ -c "$VIDEO" ]] ||
        die "$VIDEO does not exist"

    [[ -c "$PCM" ]] ||
        die "$PCM does not exist"

    [[ -c "$CONTROL" ]] ||
        die "$CONTROL does not exist"

    echo
    echo "Capture card $SERIAL:"
    echo "  Video resolved to $VIDEO"
    echo "  Audio resolved to ALSA card C${CARD}"

    set_device "$VIDEO_DEV" "$VIDEO" "44"
    set_device "$PCM_DEV" "$PCM" "29"
    set_device "$CONTROL_DEV" "$CONTROL" "29"
done

echo
echo "CT $CT device check complete."

# -------------------------------------------------------------------
# OPTIONAL START
# -------------------------------------------------------------------

if [[ "$START" -eq 1 ]]; then

    if pct status "$CT" | grep -q 'status: running'; then
        echo "CT $CT is already running."
    else
        echo "Starting CT $CT..."
        pct start "$CT"
    fi
fi

I installed it as:

/usr/local/sbin/passthrough-checker

and made it executable with:

chmod 755 /usr/local/sbin/passthrough-checker

For now, my intention is simply to run it manually after reboot and before starting the Channels LXC:

passthrough-checker 103

There's also a --start option:

passthrough-checker 103 --start

which checks/fixes the passthrough devices and then starts the LXC.

It should also be pretty easy to fully automate this with a small systemd service on the Proxmox host: disable the LXC's normal onboot, have systemd run passthrough-checker after the host devices have been enumerated, and then let the script start the LXC with --start.

I haven't bothered with that yet because manually running one command after a host reboot is simple enough, and I like being able to see exactly what, if anything, changed.

As a bonus, the script can do the same thing for /dev/dri/cardX. My Intel iGPU's card number can also move after a reboot, so the script resolves its persistent PCI path and corrects that Proxmox devN entry as well. That's why the example includes the GPU code, but the capture-card video/audio enumeration was the main reason I wrote it.