RSS Amplifier

Blogs by otaku - Coding Otaku · Dec 20, 2025

Dynamically Swapping Camera with v4l2loopback, ffmpeg, and fzf

0
Sign in to vote or save

Coding Otaku · Coding Otaku

This should probably in my notes, but it’s long enough that I’m not comfortable saving it there.

Have you ever had two or more cameras and wanted to switch between them when you are on a video call or when recording a tutorial? Well, most people will use something like OBS for this, and I understand. But you don’t really need to do that.

What we need

We need a dummy video device to use as camera. It needs to be stored as /dev/videoX and can be created using v4l2loopback, and it can usually be installed with your package manager.

What we have

First, look at the video devices you have.

ls /dev/video*
/dev/video0 /dev/video1 /dev/video2 /dev/video3 /dev/video4 /dev/video5

While it shows 6 devices for me, there are only 3 actual devices, others are used to store the metadata. To know more about the device, you can look them up in /sys/class/video4linux.

for dev in $(cut -d: -f2 /sys/class/video4linux/video*/dev);\
  do cat /sys/class/video4linux/video${dev}/{index,dev,name} | tr '\n' '\t'; \
  printf '\n';\
done;
Output

	#index	dev 	Name
	0	81:0	Integrated Camera: Integrated C
	1	81:1	Integrated Camera: Integrated C
	0	81:2	Integrated Camera: Integrated I
	1	81:3	Integrated Camera: Integrated I
	0	81:4	NexiGo N60 FHD Webcam Audio: Ne
	1	81:5	NexiGo N60 FHD Webcam Audio: Ne	

The one with index as 1 contains device metadata, and we can ignore those. The second device I have (81:2) not really usable, it was made for Windows Hello, and is an IR (Infrared) camera. So I’ll need to ignore that too when selecting camera.

Creating a dummy device

Once v4l2loopback is installed, we need to load it as a module. We need only one dummy video device, so we do sudo modprobe v4l2loopback devices=1.

Now, if we check the /dev again, we will see one more in the list.

ls /dev/video*
New devices

ls /dev/video*
/dev/video0  /dev/video1  /dev/video2  /dev/video3  /dev/video4  /dev/video5  /dev/video6

The device name should show up as dummy video device.

 cat /sys/class/video4linux/video6/{index,dev,name} | tr '\n' '\t';
Dummy Device Output

	#index	dev 	Name
	0	81:6	Dummy video device (0x0000)	

Now we have almost everything we require.

Using FFmpeg to duplicate video

This one is surprisingly simple with the help of ffmpeg. To mirror /dev/video0, all you need to do us run ffmpeg -f v4l2 -i /dev/video0 -f v4l2 /dev/video6. But, it comes with a lot of noise in the terminal, if you want to avoid the logs, just add -loglevel quiet at the end of the command.

To switch the camera from /dev/video0 to /dev/video4 without the logs, I run ffmpeg -f v4l2 -i /dev/video0 -f v4l2 /dev/video6 -loglevel quiet

Problems with swapping camera

So far, there are two problems with this approach, and both can be avoided by being a bit careful.

Camera resolutions

When the cameras have different resolutions, it will cause weird glitches when swapping between them. So, it is important to set the resolution the same when mirroring them. We also want to keep the aspect ratio when mirroring.

The approach and resolution you want may vary, for me, I do -vf "scale=1280=-1" to keep both cameras at 1280 width and preserve the aspect ratio. So it will look like this:

 ffmpeg -f v4l2 -i /dev/video0 -vf "scale=1280:-1" -f v4l2 /dev/video6 -loglevel quiet

Mirroring camera while using it.

This sometimes causes issues depending on the client. But most of the time you may not even be able to mirror because ffmpeg will throw error like Device or resource busy. The only solution I can think of is to always have at least one device being mirrored before selecting the device, and also avoid previewing it. The other way would be to kill the process doing the preview.

Swapping the camera faster

As you may have noticed already, right now, to swap the camera, we need to stop the ffmpeg command and restart it with a different argument. It’s not very fast process.

For this, I will use fzf and a bash script. You reference this and make a better script or tool if you need.

Handle module loading

use lsmod and grep to see if v4l2loopback is loaded, if not, load it manually.

if ! lsmod | grep v4l2loopback; then
    echo 'v4l2loopback module is not loaded, attempting to load it'
    sudo modprobe v4l2loopback devices=1
else
    echo 'v4l2loopback module is already loaded'
fi

You could change the else part to remove the module and add it back if required, but it would cause problems if you restart the script mid-streaming.

Avoid active camera devices from selection

To see whether a device is being used or not, we need to use lsof command. You will probably need to install it or find an alternative depending on the system you are on.

is_active() {
    lsof -nP +D /dev | grep --color=never "^$1" 2>&1 >/dev/null
}

The argument to is_active will be the device path (i.e, /dev/videoX)

Listing all possible devices

While an ls /dev/video* would do the trick, I want a detailed list of all supported devices including it’s name and ignoring metadata devices.

So I do the following:

  1. Read /sys/class/video4linux/video*/dev to get all device identifiers
  2. Check if that device is active using the is_active function we wrote, and ignore the device if it is active.
  3. Check the device index and ignore if it is not index 0
  4. Get the device name from /sys/class/video4linux/videoX/name
  5. Store the device identifier and name to a variable
  6. Store the dummy device identifier to use later.
    input=''
    # shellcheck disable=SC2013
    for identifier in $(cut -d: -f2 /sys/class/video4linux/video*/dev); do
        if is_active "/dev/video${identifier}"; then
            # echo "/dev/video${identifier} is currently active, skipping."
            continue
        fi
        index=$(cat "/sys/class/video4linux/video${identifier}/index")
        if [ "${index}" != "0" ]; then
            # Ignore non-device
            continue
        fi
        if [ -e "/dev/video${identifier}" ]; then
            # Get the device name
            read -r name <"/sys/class/video4linux/video${identifier}/name"
            # Check if it is a dummy device and store if it is.
            if grep -i 'dummy' "/sys/class/video4linux/video${identifier}/name" >/dev/null; then
                out="/dev/video${identifier}"
                printf 'Dummy device /dev/video%s\n' "${identifier}"
                continue
            fi
            input=$(printf '%s\n%s\t%s' "${input}" "/dev/video${identifier}" "${name}")
        fi
    done

FZF for camera selection

I use fzf to select the camera, the script will wait until I either select a camera or exit the script. And this whole thing is run in an infinite loop.

    camera=$(printf '%s\nExit' "${input}" | fzf --height=10 --layout=reverse --accept-nth 1 --prompt='Select camera > ')
    # Kill existing ffmpeg process
    # TODO: kill the process from previous execution instead
    pkill -f "ffmpeg -f v4l2 -i"
    # Exit if camera is not selected
    [ -z "${camera}" ] || [ "${camera}" = "Exit" ] && exit
    # Mirror the camera device to the dummy device
    ffmpeg -f v4l2 -i "${camera}" -vf "scale=1280:-1" -f v4l2 "${out}" -loglevel quiet &
    printf 'Using %s\n' "${camera}"
    sleep 3

The script

This is the final script, with fewer comments. I don’t use this script much any more, but I think it will help someone.

#!/usr/bin/sh
if ! lsmod | grep v4l2loopback; then
    echo 'v4l2loopback module is not loaded, attempting to load it'
    sudo modprobe v4l2loopback devices=1
else
    echo 'v4l2loopback module is already loaded'
    # sudo modprobe -r v4l2loopback
    # sudo modprobe v4l2loopback devices=1
fi
is_active() {
    lsof -nP +D /dev | grep --color=never "^$1" >/dev/null 2>&1
}
out=''
while true; do
    input=''
    # shellcheck disable=SC2013
    for identifier in $(cut -d: -f2 /sys/class/video4linux/video*/dev); do
        if is_active "/dev/video${identifier}"; then
            # echo "/dev/video${identifier} is currently active, skipping."
            continue
        fi
        index=$(cat "/sys/class/video4linux/video${identifier}/index")
        if [ "${index}" != "0" ]; then
            continue
        fi
        if [ -e "/dev/video${identifier}" ]; then
            read -r name <"/sys/class/video4linux/video${identifier}/name"
            if grep -i 'dummy' "/sys/class/video4linux/video${identifier}/name" >/dev/null; then
                out="/dev/video${identifier}"
                printf 'Dummy device /dev/video%s\n' "${identifier}"
                continue
            fi
            input=$(printf '%s\n%s\t%s' "${input}" "/dev/video${identifier}" "${name}")
        fi
    done
    camera=$(printf '%s\nExit' "${input}" | fzf --height=10 --layout=reverse --accept-nth 1 --prompt='Select camera > ')
    pkill -f "ffmpeg -f v4l2 -i"
    [ -z "${camera}" ] || [ "${camera}" = "Exit" ] && exit
    ffmpeg -f v4l2 -i "${camera}" -vf "scale=1280:-1" -f v4l2 "${out}" -loglevel quiet &
    printf 'Using %s\n' "${camera}"
    sleep 3
done

Read the original on codingotaku.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.