My office has a big, blank wall. It’s the boring background of all my Zoom calls.

A big ol’ blank wall

This wall is in dire need of… something. My first thought was my Buster Keaton poster:

A poster of Buster Keaton at a film editing table

But that’s too static. I want to change the background frequently and my basement can’t handle the amount of posters I would have to buy. So I bought a projector to show anything I want. Here’s what I’ve got so far:

Frames from The Night of the Hunter projected onto the wall

Here’s how I got there, but if you’re more interested in coding with GitHub Copilot you can jump ahead to the code.

The Plan

I love movies, so I want to turn the blank wall into a silver screen. I want the ballet from Singing in the Rain, the safe-cracking from Thief, the graffiti from Candyman. Moments from Lone Wolf and Cub, Rear Window, Raiders of the Lost Ark, Deep Red and so much more.

While considering my options, I remembered the Very Slow Movie Player (VSMP). The VSMP is a small e-ink screen that plays movies at super slow speed. What if I did that, but projected the results onto my blank wall? I could have a much more dynamic background and, when bored, explore the details of these scenes one frame at a time. Besides, who wouldn’t want to watch Gene Kelly dancing on loop for an entire meeting.

So I bought a projector, specifically a cheap, refurbished Epson Brightlink 585wi. I can connect directly via HDMI, so I’ll host this on a raspberry pi and connect the two.

The Content

An easy way to achieve frame-by-frame playback is to extract frames from the video as images. Often people use VLC or MakeMKV to extract the video from a Blu-ray, and FFMPEG to convert the video into images. FFMPEG is a remarkable library that should always be your first stop when working with video. I used the following command:

./ffmpeg.exe -ss 00:00:53 -i ./hunter.mp4 -vf fps=1/5 ./images/hunter%d.png

Here’s more details on all the parameters

  • ss: This parameter indicates where you will start recording. For Night of the Hunter I started 53 seconds in to skip past black screen and intro credits.
  • i: The input file.
  • vf: This is a “filtergraph” or a series of filters that modify the video. It’s incredibly powerful but I’m only using it to reduce the frame rate. I only want one frame every 5 seconds, hence 1/5th of a frame per second.
  • ./images/hunter%d.png: This is a filename template. FFMPEG will create the images using this pattern, replacing %d with an ever growing number for each image created.

The Code

I’ve got all my images and a way to get them on the wall. Now I need a way to show the images and cycle through each frame. Being a web developer, I built a simple website and web server to manage everything.

For this project I wanted to work fast and save time, so I turned to GitHub Copilot. Copilot has an interesting feature where you write a comment describing how your code should work and it will write the code. After using it for a while, I think it’s witchcraft. Let me show you.

I started with basic express boilerplate (note that this is all in TypeScript).

import * as express from 'express';
import * as fs from 'fs';
const app = express();
const port = 8080;
app.listen(port, () => {
    console.log('server started');
});

Next I built the landing page. This page will show all the possible scenes I can play, stored on the server as folders of images. To build this page I added the following comment between the port definition and app.listen

/**
 * This endpoint should scan the list of folders in the images directory and list them as html links
 */

…and GitHub Copilot wrote the following for me automatically:

app.get("/", (req, res) => {
    fs.readdir("./images", (err, files) => {
        if (err) {
            res.send("Error reading directory");
        } else {
            // build a list of links
            let links = "";
            for (let i = 0; i < files.length; i++) {
                links += `<a href="/${files[i]}">${files[i]}</a><br>`;
            }
            // send the list of links
            res.send(links);
        });
    });
});

Witchcraft.

I didn’t have to look up how readdir worked, it had error handling, it correctly guessed the directory ./images, and set up links anticipating the next endpoint I needed to build. That’s so much time saved.

Then I pushed Copilot a little harder. I added another comment to create a new page:

/*
 * This endpoint should return an html page containing links to all the images in the requested folder
 */

And GitHub Copilot created the following:

app.get("/:folder", (req, res) => {
    fs.readdir(`./images/${req.params.folder}`, (err, files) => {
        if (err) {
            res.send("Error reading file");
        } else {
            // build a list of links
            let links = "";
            for (let i = 0; i < files.length; i++) {
                links += `<img src="/${req.params.folder}/${encodeURIComponent(files[i])}" /><br>`;
            }
            // send the list of links
            res.send(links);
        }
    });
});

The URL is right, the folder structure is right, and the output is right. Now, there may be a security vulnerability in using req.params.folder directly in readdir. That scares me in a different way than the cauldron-based magic that wrote this code. Security seems to be a common concern with Copilot, the programmer still needs to be on guard. 

Lastly, I wrote:

/*
 * This endpoint should return the requested image 
 */

And sure enough, it created an endpoint that will load the image and serve it to the client when accessed.

Now I can see all the images associated with this project, but they’re all shown at once. I need to cycle through them one at a time. I was sure this was going to be too much for GitHub, but I tried it anyway:

/* 
 * Make all images on the page invisible, then show the first image and cycle through the images every 5 seconds
 */

And GitHub gave me:

<script>
        document.querySelectorAll("img").forEach(img => {
            img.style.visibility = "hidden";
        });
        document.querySelector("img").style.visibility = "visible";
        setInterval(() => {
            let current = document.querySelector("img");
            let next = current.nextElementSibling;
            if (next == null) {
                next = current.parentElement.firstElementChild;
            }
            current.style.visibility = "hidden";
            next.style.visibility = "visible";
        }, 5000);
</script>

And I was proven wrong. Copilot was definitely up to the task and wrote a viable solution before I even tried to think of the right approach.

The Clean Up

With the latest code the website was flashing between images. It was very jarring, so now I fade the scene from image to image. I didn’t bother with Copilot here, and in the end it required a bunch of trial and error. Here’s my final process:

  1. Start with one image on the page, absolutely positioned.
  2. When you want to fade in the new image, add it to the page absolutely positioned with a CSS fade in animation.
  3. When the animation is complete, update the first image to show the contents of the new image, and remove the new image.
  4. After a set amount of time, repeat the process starting at step 2.

I made other changes along the way to improve the project, and you can see the code here: https://github.com/Dashron/projector-art.

The Final Results

Frames from The Night of the Hunter projected onto the wall

In the end GitHub Copilot didn’t write everything for me, but it still saved me a lot of time. I think I’m going to put that extra time to good use, and watch a bunch of movies about witches.

Check out the code here

This article is better than it was thanks to Eric, my favorite casual wordsmith.

February 8th Tech