Recently I had to test a videoservice website for a server to server integration. Since I did not want to stream myself while doing tests, I have investigated how to leverage the great flexibility of Linux tooling. Let's review what I've learned in order to stream an arbitrary video on the browser.
§ The components
Let's start with some basic facts:
- A video file is composed of various streams (audio and video), often more than one.
- The camera and microphone physical devices are mapped to Linux devices (seen as files) in userspace.
The simplified workflow is:
- create a virtual device
- play the file sending the streams to the virtual device instead of the default ones (webcam and speakers)
- have the browser use these virtual devices instead of the real ones
Let's start! First we need to create a virtual video device camera.
Install the kernel module for
v4l2loopback, the kernel interface for a video loopback (i.e. fake) video device:
$ sudo apt install v4l2loopback-dkms
If for any reason you can't install a package, compile it yourself:
$ git clone https://github.com/umlaeute/v4l2loopback $ make && sudo make install && sudo depmod -a
Create a virtual video device
$ ls /dev/video* video0 video1 $ sudo modprobe v4l2loopback $ ls /dev/video* video0 video1 video2
Ok, we now have a fake camera (
§ Feed a virtual cam with a video stream
Open Firefox on any test website that allow using the camera and microphone, example a videocall website or simply this test page.
Now follow this sequence:
Play the video with
ffmpegand redirect the video stream to the v4l2 loopback device:
ffmpeg -re -i test.avi -f v4l2 /dev/video2
Have the website ask for permissions to use camera and microphone: you should now see an additional "dummy device", which is the stream being played by ffmpeg.
If you start recording, you should see that the video stream comes from
ffmpeg, not the webcam. The audio stream will be discarded by
ffmpeg so the website will record a video mute.
§ Feed a virtual cam and a virtual mic with both a/v streams
To also passthrough the audio, there's a bit more legwork.
Let's have a look in detail to the recording device on our workstation. Each physical audio card has a
sound and a
monitor device. We want to use the latter to grab the audio. Take note of the
alsa.card_name, it will be used later. Note that even if the name says "ALSA" it's also the name of the Pulseaudio device (go figure).
$ pactl list sources Source #0 Name: alsa_output.pci-0000_00_1f.3.analog-stereo.monitor Description: Monitor of Built-in Audio Analog Stereo Properties: device.description = "Monitor of Built-in Audio Analog Stereo" device.class = "monitor" alsa.card_name = "HDA Intel PCH" alsa.long_card_name = "HDA Intel PCH at 0xe8248000 irq 167" alsa.driver_name = "snd_hda_intel" sysfs.path = "/devices/pci0000:00/0000:00:1f.3/sound/card0" Source #1 Name: alsa_input.pci-0000_00_1f.3.analog-stereo Description: Built-in Audio Analog Stereo Properties: device.description = "Built-in Audio Analog Stereo" device.class = "sound" alsa.card_name = "HDA Intel PCH" alsa.long_card_name = "HDA Intel PCH at 0xe8248000 irq 167" alsa.driver_name = "snd_hda_intel" sysfs.path = "/devices/pci0000:00/0000:00:1f.3/sound/card0"
In the previous output, we note that
Source #0 has
device.class = "monitor" and its name is "HDA Intel PCH".
Now we have to tell
ffmpeg to split the audio and video streams and send each one to the proper virtual device. Let's identify the streams in the file we want to play:
$ ffprobe test.avi 2>&1 | grep Stream Stream #0:0(eng): Audio: aac (LC) (mp4a / 0x6134706D) ... Stream #0:1(eng): Video: h264 (Main) (avc1 / 0x31637661) ...
We identify that
#0:0 is the audio stream and
#0:1 is the video stream.
Let's play the video and instruct ffmpeg according to the information we have:
ffmpeg -i test.mp4 \ # send the stream "0:0" to a device with format "v4l2" on "/dev/video2" -map 0:0 -f v4l2 /dev/video2 \ # send the stream "0:1" to a device with format "pulse" called "HDA Intel PCH" -map 0:1 -f pulse "HDA Intel PCH"
Now you should see that firefox is recording both from the virtual cam and the virtual mic.
To verify this you can open
pavucontrol, go to the "Recording" tab and observe that Firefox is using the monitor device, you should see the volume bar moving:
Now reload the website to reset permissions and have it ask them again. This time when you select the monitor device you'll be plugged to the
ffmpeg stream running:
And you're set, you'll be recording both audio and video from
- Keep in mind that upon reboot of the computer, the
v4l2kernel module will not be loaded again (in order to persist kernel modules across reboots there's another procedure).
- I'm unsure if this all works also on chrome/chromium. I don't use it anymore but I vaguely recall that it does not allow the user to select which devices to use (but I might be wrong here).
- Firefox is just an example, but you can use this trick theoretically with any application for videoconfering (jitsi, zoom, whereby, etc.). YMMV, though.
§ Bonus: Renaming the devices
Unrelated to this topic, but I'll throw this in for future use. If you don't like the device names, you can easily change them with:
pacmd update-sink-proplist YOURDEVICE device.description=NEWDEVICENAME (if it's a sink, i.e. an output device) pacmd update-source-proplist YOURDEVICE device.description=NEWDEVICENAME (if it's an input device)
pacmd update-sink-proplist \ alsa_output.pci-0000_00_1f.3.analog-stereo.monitor \ device.description=LoopbackDevice
To reset these settings, kill pulseaudio with
pulseaudio -k then start
pavucontrol to force pulseaudio to restart again. I didn't investigate how to persiste them.
Here's some references. As usual, figuring out everything involves a bit of searching and assembling various pieces from different places: