< All posts | Fediverse | RSS | GitHub | Talks

Apr 28 2023

Driver adventures for a 1999 webcam

We generally know that when we buy a piece of technology that it will not last forever, connectors wear out and/or go out of fashion. But I think the most frustrating reason to have to get rid of something is that drivers stop being made for devices.

USB has been a remarkable success. It has been with us for a long time and has kept a (mostly, ignoring USB-C) consistent connector. Meaning that very old devices made for USB 1 are still usable in systems that are being sold today. At least this would be the case if the older devices still had drivers for currently relevant operating systems.

The USB universal video, audio and storage classes have provided a standard for devices to implement to ensure that they can work with little custom work on drivers or no extra drivers at all, but they still have to have been made in a time where those standards existed.

Enter the QuickCam Express

A good friend of mine this week was clearing out stuff and handed me an old logitech QuickCam Express webcam, this was actually a pretty serious nostalgic moment as it happened to also be the same model as my first webcam, so thinking it could be funny at some work meetings to have an “early 2000s” vibe I took it home.

However the QuickCam Express has not had drivers since Windows XP it seems. I attached it to my Linux machine, and no module was loaded, and when I then attached it to my Windows 10 VM I was presented with an unknown device. Meaning I was out of luck for official support for this thing.

This was especially annoying since I had already taken this thing home. Not wanting to give up so quickly I decided to go and actually verify if this webcam still worked by installing a copy of Windows XP to see if it would correctly function on a period correct operating system.

(As a side note I believe it should be on record that installing Windows XP on reasonably fast modern systems is very amusing, the setup wizard will say that it has 30 minutes remaining and then proceed to blow through the entire installation in less than 15 seconds)

After installation, I loaded up Windows Movie Maker to see if the webcam would correctly work and was delighted to see that it does. (I must say the quality of webcams has definitely improved since 1999)

So the question was, how are we going to make this webcam that only has drivers up to Windows XP work on a modern day operating system?

User space time

In one of my previous blog posts I bought a number of “VGA2USB” video capture devices for very cheap and I later understood that these were very cheap because they also had no modern-day drivers, so I decided to correct it by writing a user space driver (and I now use those devices at least once a month!).

A user space driver is a driver that is embedded inside a program rather than a module of code inside the operating system. This means that the driver can run often on different versions of operating systems and often on different platforms with minimal code changes.

To get an idea of what the Windows XP driver was doing I loaded usbmon into my desktop that was running the Windows XP VM and then recorded the USB traffic going between the virtual machine and the webcam. This is invaluable in reverse engineering since it allows us to see a real time “known good communication” transcript that we can then build our own driver from.

Reassuringly it seemed that the communication was pretty basic, involving what looked like some basic settings bootstrapping and then isochronous data transfer of a pretty basic looking data stream.

I then began to look around to see if anybody had previously written a Linux driver for this webcam, and it turned out someone had in the form of qc-usb. So using that as a base I worked towards getting a very basic setup where I could stream image data.

To start with I used the same libusb wrapper go-usb and set it up to look for the QuickCam’s usb VID and PID:

ctx := gousb.NewContext()
defer ctx.Close()

// idVendor=046d, idProduct=0870,
dev, err := ctx.OpenDeviceWithVIDPID(0x046d, 0x0870)
if err != nil || dev == nil {
        log.Fatalf("Could not open a device: %v", err)

deviceConfig, err := dev.Config(1)
if err != nil {
        log.Fatalf("Failed to get USB config for device: %v", err)

At that point we can grab an interface for the device so we can communicate with it.

// Config.Interface(num, alt int)
USBInterface, err := deviceConfig.Interface(0, 0)
if err != nil {
        log.Fatalf("cannot grab control interface %v", err)

Under the hood, something that you plug into a USB port has a device configuration, that specifies one or more interfaces, and that interface likely has multiple endpoints inside. There are multiple endpoints often because USB devices do more than one thing. For example in a USB sound card there may be the output function, a microphone, and the buttons on the card to control volume. These would often be separate endpoints, and for potentially different interface configurations for whatever setup the driver controlling it wants to use.

Since the really interesting task is the actual webcam image data, I exported out a CSV from wireshark of all of the control packets to setup the webcam that the VM sent to the webcam before it outputted an image, and put that in the user space driver to be replayed (without quite understanding what they do yet).

I then setup the transfer part by using the endpoint that used a isochronous data stream (a type of USB data exchange that guarantees latency and bandwidth on the bus) and found that would result in:

libusb: invalid param [code -2]

Confused about this, I went to look in the kernel log for more information since that often has helpful information in these situations:

kernel: usb 5-1.3: usbfs: usb_submit_urb returned -90

Helpfully, the kernel does have some useful information! -90 is a good hint about what is happening. The negative number would indicate it’s an error number from the kernel. So let’s look up what 90 means as an error number.

$ cat /usr/src/linux-headers-`uname -r`/include/uapi/asm-generic/errno.h | grep 90
#define        EMSGSIZE        90        /* Message too long */

We can see that 90 (or -90) means “Message too long”. This stumped me for a while, until I decided to look again at the USB Device configuration structures…

I then spotted that the 0’th interface (the one I had selected out of habit) had a MaxPacketSize of 0 for what I assumed was the image transfer endpoint.

This means that all attempts to get data from it using the first USB interface would fail. Now you might ask, why does the webcam have an endpoint with a 0 byte MaxPacketSize on its first interface? Who knows! But the other available interface is a mirror of the 0th, but with a MaxPacketSize of 1023. Good enough, and in no time, I had the ability to stream data out of the webcam!

We can now read the 1023 byte data chunks from the webcam. but now we need to decode what this stream of bytes really means!

Understanding the stream

The webcam appears to have a chunking protocol where it will give you image data in 1023 byte chunks. Based on patterns we could assume that 0x02,0x00 might be the start of a new frame. But I decided to take a look at the qc-usb driver to save time and found that 0x02,0x00 is the message ID for an image chunk: these are meant to be added to a larger image buffer, with the next 16 bits being the length of the chunk. The start and end of frames are encoded using different message IDs. Either way, a pretty easy thing to implement and it did not take long until we had full “frame” data.

Now comes the harder task of figuring out how images are encoded. My first attempt was to take the resolution that Windows XP claimed it was (320x240) and draw that directly as RGB 24bit colour pixels:

Well, it was worth a shot. At this point I did have a suspicion though that we still had raw sensor data without any other compression involved. This was because the data coming out is always 52096 bytes. If it was compressed you would see some kind of variation (even if small).

However to confirm this I simply put my hand in front of the webcam and took some more snapshots and saw that the lightness of the corrupted mess at least matched my hand being over the camera.

So we now know we are looking at raw sensor samples at least. The question is, how are those sensor values packaged?

I investigated the sensor in the webcam and discovered it was a Photobit PB-100, a sensor that has a resolution of 352x288. I then assumed that each pixel had to be around 4 bits based on the frame size of 52096 bytes:

 (52096*8)/(352*288) = 4.11

So what if we try that new resolution and assume that each sensor value is a 4 bit number?

With that I had an image that did actually look like me (it turned out I also had a blanket over me that was very useful as a test pattern!) – but bunched up and without any colour.

One of the benefits of working on image processing is that you get to see a lot of “Digital signal processing modern art”. I think I accidentally made a DSP Andy Warhol.

I then realised that I only have enough pixels for a monochrome image, but if I drew it out incorrectly, I got a “full-sized” image that resembled something that looked like me!

finalImg := image.NewRGBA(
        image.Rect(0, 0, 400, 400))
xLimit := 352

for idx, _ := range imgData {
        if idx+3 > len(imgData) {

        x := idx % xLimit
        y := idx / xLimit

        finalImg.SetRGBA(x, y, color.RGBA{

The next hurdle is colour. I assumed that this was going to be similar to my previous adventures and I tried doing a YUV colorspace transform (Since RGB was clearly not working) with no luck.

So I went back to reading the qc-usb linux driver source again, I discovered that the raw image data had a Bayer filter on it! So, I would have to undo the filter myself. The driver itself had a number of ways (ranked in how many cycles it took an Intel Pentium 2 to process an image) to do this, however I struggled to port any of them correctly to golang. So instead I found a TIFF library that had a function for it and used that instead.

And with that, I pointed my webcam at a rainbow lanyard. Switched it to GRBG mode and got:

This is the first colour image I got from my driver! I was thrilled! Next I setup a the camera to point at a test card to see how well it performed against it and while it did look amusing:

It does remind me again that we have come a long way with webcams since 1999…

Now to give the QuickCam some credit, some of the disappointing colour response is because I am using parameters that I got from a one-off USB packet capture. Since those control packets control things exposure/brightness, essentially those settings are frozen in time from when I first tested the webcam.

Interestingly the white balance is required to be processed on the driver side, I also learned that the auto brightness function of the webcam is controlled entirely on the driver side and not inside the webcam. I suppose this does make sense for 1999 but I suspect (but I don’t know for sure) that today a lot of these functions are controlled from inside the webcams, heck, some of the webcams run Linux now!

Feeding it back into the kernel

Now this is not very useful if I can’t join a Google meet with it, so for that we can use V4L2 Loopback to make a fake video device and allow us to inject our newly decoded webcam images back into the kernel so applications like Google meet can use it!

Doing this is actually fairly simple as ffmpeg can do all the heavy lifting, so all we really need to do is to give FFMPEG a MJPEG stream and the device.

First we create the V4L2 device:

sudo modprobe v4l2loopback exclusive_caps=1

And then we can feed MJPEG in as a webcam using something like:

ffmpeg -f mjpeg -i - -pix_fmt yuv420p -f v4l2 /dev/video0

I made my userspace driver do this automatically for maximum ease, and before I knew it, I could load up google meet and see a webcam from 1999 show my face again (with some weird processing artifacts, no idea on that that one)

Mission Success. We turned what was going to be nostalgic e-waste into a terrible but functioning webcam, and best of all, I learned even more about the horrors of USB on the way!

If you also happen to own one of these, You can find the code to do all of this here: benjojo/qc-usb-userspace

If you want to stay up to date with the blog you can use the RSS feed or you can follow me on Fediverse @benjojo@benjojo.co.uk (or twitter where I am now rarely posting)

You may have noticed I have not posted in a long time! This is because I’ve been working on my own business called bgp.tools. If you have any use for BGP monitoring or data analysis do take a look!

If you like what I do or think that you could do with some of my bizarre areas of knowledge I am also open for contact work, please contact me over at workwith@benjojo.co.uk!

Until next time!