A Weird Imagination

Relative links in feeds

The problem#

In an RSS/Atom feed, relative links are a bad idea because it's unclear what they're relative to. There are ways to specify a base for them to be relative to, but since feed readers do not consistently respect those mechanisms, it's safer to just always use absolute URLs in feeds. And Pelican recommends setting RELATIVE_URLS = False to always generate absolute URLs. But that setting does not apply to the anchor links generated by the Markdown toc extension to link to headers.

The solution#

I wrote a Pelican plugin, absolute_anchors which rewrites all link destinations starting with # in every article to add the absolute URL of the article at the beginning of the link.

The details#

Read more…

Reacting to active window

Posted in

The problem#

Which window I have focused is a signal to the computer for the state I want it to be in. For instance, I normally leave my speaker muted so, for example, I don't accidentally play sound from a website with unexpected videos. But this means that when I do want sound, I need to manually unmute the sound, even though I've already told the computer that I want to watch Netflix, which always involves turning on the sound.

Of course, for the particular problem of unmuting the sound, adding a keyboard shortcut and rereading xkcd 1205: Is It Worth the Time? probably would have been a more appropriate solution. But I wanted a general solution to the problem.

The solution#

Download x11_watch_active_window.py. Then the following script will unmute the speakers if Netflix is focused:

#!/bin/sh
x11_watch_active_window.py | while read -r FocusApp
do
    if [ "Netflix - Google Chrome" = "$FocusApp" ]
    then
        echo Netflix is focused, unmuting.
        pactl set-sink-mute 0 0
    fi
done

The details#

Read more…

Virtual microphone using GStreamer and PulseAudio

The problem#

My previous post got the video from my smartphone to show up as a camera device on my desktop, but for a video chat, we probably also want audio. So, now the question is: how to build GStreamer pipelines that will allow minimal-webrtc-gstreamer to use virtual microphone and speaker devices that I can point a voice/video chat application at, allowing me to use my smartphone's microphone and speaker for applications on my desktop.

The solution#

The following requires that you are using PulseAudio as your sound server and have downloaded minimal-webrtc-gstreamer:

pactl load-module module-null-sink sink_name=virtspk \
    sink_properties=device.description=Virtual_Speaker
pactl load-module module-null-sink sink_name=virtmic \
    sink_properties=device.description=Virtual_Microphone_Sink
pactl load-module module-remap-source \
    master=virtmic.monitor source_name=virtmic \
    source_properties=device.description=Virtual_Microphone
./minimal-webrtc-host.py\
    --url "https://apps.aweirdimagination.net/camera/"\
    --receiveAudioTo device=virtmic\
    --sendAudio "pulsesrc device=virtspk.monitor"\
    --sendVideo false --receiveVideo false

You can reset your PulseAudio configuration by killing PulseAudio:

pulseaudio -k

You can make the PulseAudio settings permanent by following these instructions to put them in your default.pa file.

The details#

Read more…

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"\
    'card_label=virtcam'\
    exclusive_caps=1 max_buffers=2
./minimal-webrtc-host.py\
    --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#

Read more…

GStreamer WebRTC

The problem#

In my previous posts on minimal-webrtc, I set up a peer-to-peer connection between the web browsers on two different devices. For more flexibility, including making the remote camera and microphone appear as local camera and microphone devices, we need to handle the WebRTC connection outside of a web browser.

The solution#

minimal-webrtc-gstreamer is a command-line client for minimal-webrtc written in Python using the GStreamer library. It's mostly a modification of the webrtc-sendrecv.py demo script to use minimal-webrtc as the signaling server to make it easier for me to tinker with.

Run as follows:

./minimal-webrtc-host.py\
    --url "https://apps.aweirdimagination.net/camera/"\
    --receiveAudio --receiveVideo any

It will output a URL as text and QR code for the other device to connect to. With those options, the output from that device's camera will be shown on screen and the output from its microphone will be played through your speakers. That device will be sent video and audio test patterns. See ./minimal-webrtc-host.py --help for more information.

The details#

Read more…

Extracting slides from video presentations

The problem#

Washington state has been holding a lot of press conferences with updates about the COVID-19 situation recently. The information has always been summarized in a few slides during the video, but those slides and explanatory text are only posted separately several hours to a day later.

The solution#

youtube-dl will download videos off Twitter just given the URL of the tweet like this one. Then clone and run slide-detector:

./slide-detector.py video.mp4 473 105 727 397

(requires opencv-python) where video.mp4 is the filename of the video and the relevant section of the video is a 727x397 rectangle whose top-left corner is at the coordinates (473, 105), which is the correct rectangle to crop the linked video to just the main video section (i.e. omitting the ASL interpreter who is always on screen). Omit the numbers to not crop the video.

The script will output the slides as image files in the current directory with names like static_at_3:55.jpg for the slide that appears on the screen 3 minutes and 55 seconds into the video.

The details#

Read more…

Long Polling in Django Channels

The problem#

In a web app where the display should be constantly up-to-date, the client needs some way to get up-to-date information from the server. One of the simplest ways to do so is to regularly (every few seconds) query the server asking if there is new information. This involves making a lot of requests and is wasteful of bandwidth and processor time on both the client and server (the latter can be improved with caching).

If updates are rare, it makes much more sense for the server to notify the client when they occur, but HTTP is designed around the client making requests to the server, not the other way around. And, furthermore, the Django web framework (like many web frameworks) is built around that model.

The solution#

Of course, this is a well-understood problem and there are a wide variety of APIs and libraries to solve it discussed on the Wikipedia page for Comet. The main workarounds are WebSockets which is a very flexible technology for two-way communication in a web browser and long polling which is a simpler technique which involves merely having the server not answer a request immediately and instead wait until it actually has an update to reply with.

In the rest of this blog post, I discuss the changes I made to convert a Django-based web app that I originally wrote to use a basic polling pattern and hosted using uWSGI to instead use long polling and be hosted using Gunicorn/Uvicorn. I also cover nginx configuration including hosting the app in a subdirectory.

The details#

Read more…

Troubleshooting python-xcffib

The problem#

The monitor-lock.py script in my previous blog post uses python-xlib, which currently mainly relies on manually porting Xlib functions to Python. This is why it is missing the barrier-related functions I needed in that post. There is work on automating this process, but it appears to be abandoned. I started trying to pick up where they had left off before finding the python-xcffib project which provides auto-generated bindings for libxcb and therefore gives full support for interacting with X at a low level from Python.

python-xcffib (named after the cffi library it uses for binding to the C XCB library) gives a slightly lower-level API than python-xlib, but they are both fairly thin wrappers over the X protocol, so the differences are minor. It was fairly straightforward to port my script from the previous post to use python-xcffib, available as monitor-lock-xcb.py.

Unfortunately, I ran into a bug in python-xcffib:

Traceback (most recent call last):
...
  File "./monitor-lock-xcb.py", line 38, in main
    devices = conn.xinput.XIQueryDevice(xcffib.xinput.Device.AllMaster).reply().infos
...
  File "/usr/lib/python3/dist-packages/xcffib/__init__.py", line 139, in _resize
    assert self.size + increment <= self.known_max
AssertionError

The solution#

I've submitted the fix upstream, so most likely you will not encounter this error. Updating to the latest version (after v0.8.1) should be sufficient to fix the problem.

The fix I applied was to modify the module's __init__.py (the location, which may be different on your machine, is in the stack trace). Specifically, on line 108 in the function Unpacker.unpack(), in the call to struct.calcsize(), change fmt to "=" + fmt.

The details#

Read more…

Lightweight multiseat X

Posted in

The problem#

I hosted a LAN party1 a little while ago and ended up needing to loan out multiple computers to guests in the interest of having no one try to lug their desktop over. As it turns out, I don't keep multiple of spare gaming-ready laptops around, so I needed to get more computers somehow.

The solution#

My desktop has three screens attached to it (two monitors plus a projector), so given an extra keyboard and mouse (or two), it should be possible to run multiple instances of the game on it at the same time to let multiple people play using the same computer.

The script from this forum post makes it easy to set up multi-pointer X so a second keyboard and mouse will get its own mouse cursor. Then each keyboard and mouse pair can interact with its own instance of the game.

As an additional aid, I wrote monitor-lock.py which allows you to assign a mouse to a monitor, so it cannot be moved off that monitor to prevent accidentally interacting with the other player's instance of the game.

The basic usage is that you first run it with no arguments to get the available screens and pointers getting an output something like this:

$ ./monitor-lock.py 
...
Available screens:
screen 0: {'x': 0, 'y': 0, 'width': 3840, 'height': 2160}
screen 1: {'x': 3840, 'y': 0, 'width': 1920, 'height': 1200}
screen 2: {'x': 3840, 'y': 1200, 'width': 1920, 'height': 1080}

Available pointers:
device 2: Virtual core pointer
device 17: second pointer

USAGE: ./monitor-lock.py [device] [screen]

and then in a screen session (so you don't have to worry about accidentally doing this on a monitor you've locked your pointer away from), run

./monitor-lock.py 2 0

and

./monitor-lock.py 17 1

to lock the primary pointer to the first screen and the second pointer to the second screen.

Just use Ctrl+C to kill the process when you want the pointer to be able to move freely again.

The details#

Read more…

Emulating Xbox controllers on Linux

Posted in

The problem#

The Xbox 360 controller has become the defacto standard controller in PC gaming in recent years, likely due to both the popularity of the Xbox and the fact that the controller can easily be used with a computer. One downside of this is that some games assume you have one. If the game supports it and is running through Steam, then Steam's controller settings will let you use any controller, but that doesn't work for all games, and you might not be using Steam. The game that prompted this blog post actually does have Steam controller support promised in the future, but it's in early access and they are busy developing other parts of the game.1

xboxdrv#

The solution is xboxdrv, the userspace Xbox controller driver. In addition to supporting actual Xbox controllers, it can also simulate Xbox controllers based on inputs from other devices like a PlayStation controller or some less common controller.

Read more…