Stupid solutions: Live server push without JS
2020-09-25Underjord is a tiny, wholesome team doing Elixir consulting and contract work. If you like the writing you should really try the code. See our services for more information.
So in my post Is this evil? I covered a way of tracking users with CSS. While thinking about those weird ways of using the web I also started thinking about pushing live data to clients without JS. Or at least maintaining a connection. So WebSockets requires JS. WebRTC requires JS. Even HLS (video streaming), which would otherwise be super cool, with captions for accessibility. But no. Or rather, maybe on Apple platforms. Eh. Not good enough.
And then it hit me. From some old Nerves projects I'd seen, that there is a standard for just sending a stream of JPEG frames as a video. MJPEG. Did you know about MJPEG? Lots of people don't. It is used by lots of webcams and security cameras. Common option for Raspberry Pi hacks as well. MJPEG is super simple which is its big advantage.
But video is what we expect. I was going for something else. So this could be used for a CI status light, showing any amount of visual status information. I use it for this:
That's live. Or dead if my server falls over.
So how does MJPEG work. Well, you take an <img/>
tag and you shove an MJPEG URL into it. Done.
Okay, that's how you use it. Not how it works. I implemented it in Elixir, Elixir is quite good at keeping state and serving updates. Links are below. But basically the browser opens the connection, receives some headers and some chunks of data and then realizes it is dealing with MJPEG. It wil then just expect the chunks to keep coming. Indefinitely. Because this is live video. Frame by frame of JPEG.
The basic code for the MJPEG headers and chunking was lifted from a pi camera repo made by the Nerves team. It had a lot of Frank Hunleth and Connor Rigby in it so kudos to those guys as always. This is what I did with it: lawik/mjpeg
My server implementation is here and uses the above code: lawik/mjpeg_example
So I receive the connection and then that calls my MjpegExample GenServer to persist the connection and keep track of how to notify that connection about new data. It also triggers an update to notify everyone already connected.
This is not polished, it is hammered together and I'm curious to see if it falls over the next time I get a decent amount of traffic.
I really like this approach because it is a fun hack that simply happens to work across browsers and quite well at that. I like how it is just an img element and no frills. I added lazy loading because that works more nicely with things like Google Lighthouse scores and the loading experience (your browser doesn't spend a few seconds thinking about loading the image).
Unfortunately it is absolutely a poor choice to actually use aside from fun and hacky stuff. There is no good way of doing accessibility with it. You can update the pixels and that is it. Unless you can chunk-stream a txt-file in an iframe. Haven't tried that yet...
So, don't use it. But isn't it pretty neat?
Of course, much like the CSS tracking, this can be used for evilish things. You can absolutely keep track of how long someone keeps receiving your frames and use that for your analytics. I also find it neat that it lets me know concurrent users. I just log the number along with sending it out because I want to know at what number it breaks and out of curiosity but you can probably do some mildly untoward things with this. Oh.. Wait. You could serve rotating ads with this. You know what the last frame you sent was so if you get a click on it you can direct that particular user to the right thing. New title: Ad placement entirely without JS, ugh, no thanks. Moving on.
If you know how to make this more dirty, hacky and fun or even more useful or accessible feel free to get in touch at lars@underjord.io or on Twitter where I'm @lawik.
Underjord is a 4 people team doing Elixir consulting and contract work. If you like the writing you should really try the code. See our services for more information.
Note: Or try the videos on the YouTube channel.