A Weird Imagination

Virtual web cam using GStreamer and v4l2loopback

The problem

I want to make my smartphone's camera appear as an actual camera device on my desktop so any application (primarily Discord) can use it like it were a normal USB web cam.

My previous post introduced minimal-webrtc-gstreamer, which got as far as getting the video stream from any web browser into a GStreamer pipeline, which reduces the problem to outputting a GStreamer pipeline into a virtual web cam device.

The solution

Download minimal-webrtc-gstreamer and install v4l2loopback. Then run

sudo modprobe v4l2loopback video_nr="42"\
    exclusive_caps=1 max_buffers=2
    --url "https://apps.aweirdimagination.net/camera/"\
    --receiveVideoTo /dev/video42\
    --sendAudio false

You can test by watching the stream with

gst-launch-1.0 v4l2src device=/dev/video42 ! autovideosink

Note that some applications, including the current desktop release of Discord may not support the virtual camera, showing a solid black square or failing to connect to it at all. It should work in the latest Chromium/Chrome browser, including for the Discord web app.

When done, remove the virtual camera device:

sudo modprobe -r v4l2loopback

The details

GStreamer tutorial

GStreamer is a big, complicated multimedia framework with lots of features and therefore a lot of its own jargon and concepts. It does a good job of hiding away the complexity in simple cases, but that magic does sometimes get in the way of understanding more complicated workflows.

I wish I had found this tutorial much earlier. It does a great job of explaining a lot of the concepts behind GStreamer alongside Python example code.

Multimonitor screen sharing

There was a recently fixed bug in libwebrtc that meant on Linux screen sharing from a multimonitor setup would only show an option for sharing the "entire screen", not each monitor separately. Chromium/Chrome (before version 83) and Firefox were both impacted because they both use libwebrtc. I actually encountered the bug as a bug in Discord because the Discord desktop application uses Electron which is based on Chromium (Electron 9 is based on Chromium 83 and therefore should have the fix, but Discord has not yet updated to the latest Electron.)

As this was a long-standing bug, there were multiple workarounds developed including Hangouts Linux Individual Screen Share and Mon2Cam, which use v4l2loopback and FFmpeg to create a virtual camera device and stream a section of the screen to it or show the section of the screen in a window that itself can be shared.

Therefore, through this investigation, I knew that v4l2loopback existed and could be used to create a virtual camera device that I could point Discord (or some other video chat application) at.

First attempt

GStreamer has the v4l2sink element for outputting to Video4Linux2 devices. That documentation includes the example

gst-launch-1.0 videotestsrc ! v4l2sink device=/dev/video1

so I just replaced the existing output code

disp = Gst.ElementFactory.make('autovideosink')


sink = Gst.ElementFactory.make('v4l2sink')
sink.set_property('device', '/dev/video42')

(After some research to figure out the programmatic equivalent of the device= part was to call the .set_property() method.)

To create the v4l2loopback device, I ran

sudo modprobe v4l2loopback video_nr="42" 'card_label=virtcam'


gst-launch-1.0 v4l2src device=/dev/video42 ! autovideosink

successfully showed my camera's video stream. But no other applications were happy. Either they didn't show the camera at all or they would show a single frame and then freeze.

v4l2loopback settings

The README for v4l2loopback suggests setting exclusive_caps=1 when using the device with Chrome/WebRTC, which is a workaround for programs that expect camera devices to not accept video input.

This bug discussion suggests setting max_buffers=2 may fix issues with the video freezing after a single frame.

Then the whole command is

sudo modprobe v4l2loopback video_nr="42"\
    exclusive_caps=1 max_buffers=2

Note that in my testing, I often had to reset the v4l2loopback device by running

sudo modprobe -r v4l2loopback

and then re-initializing it, so if you're experimenting and something that seems like it should work isn't working, that's worth a try.

Format conversions

You may have noticed that despite supporting a lot of different media formats, GStreamer pipelines often get away with not mentioning any formats explicitly. This is due to the GStreamer concept of capabilities where different elements can advertise what kinds of streams they support and elements like videoconvert will just automatically apply the necessary conversions based on their input and output.

When the default capabilities are not appropriate, you'll see pipelines written like

gst-launch-1.0  videotestsrc ! \
  video/x-raw, width=320, height=240 ! \

where there's a pipeline element (sometimes shown in quotes) with a desired video/audio format and possibly other information like the size or framerate. Since there's no keyword, it took me a while to eventually find in the tutorial I linked above that omitting the element name is equivalent to using the capsfilter element with that string as its caps property. Therefore, the equivalent Python code is

caps = Gst.Caps.from_string("video/x-raw, width=320, height=240")
filter = Gst.ElementFactory.make("capsfilter", "filter")
filter.set_property("caps", caps)

Correct capabilities

Various tips about using FFmpeg to output to a virtual camera mention explicitly setting -pix_fmt yuv420p or -pix_fmt yuyv422, so I set the capabilities filter to "video/x-raw,format=YUY2".

I also tried explicitly setting the framerate and size in case those mattered, but none of the variants got Discord to accept the video. I even tried following those tips to have FFmpeg clone the virtual camera to another virtual camera with various conversions set but nothing I tried worked.

Giving up on desktop Discord

While at one point I did get the desktop Discord client to show a short snippet of video from my camera, I've been unable to recreate it. As it works fine in Chromium and Discord is based on Chromium (1) I assume this will be fixed in some future version of Discord whenever they next upgrade the version of Electron they use, and (2) an easy workaround is to just use Discord through the website when I want video.


Have something to add? Post a comment by sending an email to comments@aweirdimagination.net. You may use Markdown for formatting.

There are no comments yet.