Back to Silas S. Brown's home page

Raspberry Pi Zero W projector controller

In 2017 I was asked to set up a mini projector to be controlled from an iPhone; I'm posting these notes on how I did it in case they are useful (but usual disclaimers apply).

The iPhone's "screen mirroring" function worked only with an "Apple TV" (not anybody else's). The mini projector (bought on offer from Maplin mail-order, for a user who needed a large 'hands-free' display for physiotherapy instructional videos and wanted the option of putting them on the room's walls or ceiling as appropriate to the position) did supply iOS software, but it could play only videos that had already been downloaded to the phone, and Apple didn't make it particularly easy to pre-download the video you want (even Telegram Messenger 'bots' that download videos have been blocked on iOS due to Apple's worry that they'll be used to infringe copyright). If the phone had been Android 4.4+ it could have sent any screen to the projector (Android "screen mirroring" was never limited to only one company's television), but that user wanted to control it from a stock iOS device, so I had to do a rather more complex workaround.

The basic idea was to use Web Adjuster to assist the user as they browsed video sites, inserting extra links to play the videos on the projector. I didn't have a spare wildcard domain for this Adjuster, so I went into the iPhone's Wi-Fi settings and told it to use a proxy when connected to the home access point. But I still needed a separate computer to run that proxy and control the projector.

We bought a Raspberry Pi Zero W with case, Raspbian MicroSD, HDMI to Mini HDMI adaptor, and USB OTG adaptor for initial setup (I suggest finding an OTG adaptor that has a little cable between the USB and the MicroUSB end; I made the mistake of ordering the adaptors before the Zero W had arrived and not realising how cramped its two MicroUSB ports are---it took me some time to plug in both a power lead and the bulky non-cabled OTG adaptor I'd bought). It was not necessary to buy GPIO headers and such. The mini projector had an auxiliary USB power output which was adequate for powering the Zero W (although there was no way of turning off the power without unplugging it---leaving it in would result in the Zero W continuing to be powered from the projector's internal battery).

To ensure the SD card would not be harmed by loss of power without proper shutdown, I made it run read-only. I thinned down the Raspbian distribution by removing all the X11-related packages to leave a console-only setup (but had to keep dbus for omxplayer), and then disabled all swap space, replaced rsyslog with busybox-syslogd, put ,ro after the defaults in /etc/fstab, symlinked various directories in /var to /tmp (which I symlinked to /dev/shm), and set /boot/cmdline.txt to ro noswap fastboot. I even removed DHCP and had systemd run my own script to set up the Wi-Fi with a static IP address using wpa_supplicant and ifconfig, but I found I had to temporarily remount / read-write or wpa_supplicant wouldn't run. (I could remount it read-only half a second after wpa_supplicant started; I hope nobody cuts the power in that particular startup half-second.) I also had to restore resolv.conf from the script, as in this configuration wpa_supplicant tended to symlink it to a non-existent file on startup. While I was at writing a script, I removed fake-hwclock and added a simple date --set "$(nc -w 1 192.168.0.3 13)" where 192.168.0.3 is a machine I knew would be on and serving daytime on port 13 from openbsd-inetd. (The Zero W had to know the date in case it did any SSL-certificate validity checks while fetching video streams.)

In order to have half a chance of playing Internet videos smoothly, we needed as much RAM as possible to be assigned to the GPU, so I set gpu_mem in config.txt to 384. (I did try the maximum of 448, leaving only 64M for the system, but that made Raspbian 9 boot considerably slower because it had insufficient room for buffers. You'd have to use a Raspberry Pi 3 instead of a Zero W if you need much more GPU RAM.) I used an existing Raspberry Pi 1 B+ (with a wired connection that was always on and running public services) for the Web Adjuster part, setting up an SSH key for it to be able to log in to the Zero W without password and run omxplayer -o hdmi with a video URL (I used tvservice --explicit='CEA 3 HDMI' for a 16:9 screen of 720x480, which was as near as the Pi could get to the projector's native resolution of 854x480, and I used a simple killall omxplayer.bin for the stop control). Performing the Web handling on the other Pi meant we didn't have to take up RAM on the Zero W for proxying and adjusting instead of for graphics (the B+ on the other hand was configured with almost all RAM for the system and hardly any for graphics---you don't need much for fbterm and tmux); it also meant the phone's proxy settings didn't have to change when the Zero W was powered down, and a third advantage was that packets sent from the wired B+ somehow seemed to reach the reconfigured Zero W more reliably than did packets sent from other Wi-Fi clients on the network.

The proxy itself was a home-compiled version of nginx with ngx_http_proxy_connect_module, by default set to pass through all CONNECT and non-SSL traffic to the specified remote sites (and without logging), but with an exception for an imaginary new wildcard domain (non-SSL) which it passed to my Adjuster instead. This was faster than passing all traffic through the adjuster in real_proxy mode, and it saved worrying about intercepting SSL sites and having the phone accept fake certificates (and/or complications due to its having already seen Strict-Transport-Security headers for the sites); the user simply browsed an alternate version of the site at my imaginary domain if they wanted the extra links to play videos on the projector, or browsed the site at its original domain if they didn't.

To add the links for playing videos on the projector, I used Adjuster's prominentNotice="htmlFilter" option with an htmlFilter set to a Python function that scanned the original site for certain regular expressions to identify final video URLs (these regexps will probably have to change from time to time at the whim of the video sites) I also had to add some Javascript to headAppend for some sites, and had to use htmlUrl so the function could use youtube-dl --proxy="" -f best -g where appropriate.

The links to play and stop the videos pointed to URLs which I handled using an Adjuster extensions module that simply started an ssh process to run the above-mentioned commands on the Zero W (with careful shell quoting of video URLs); when the Zero W was switched off, these links simply did nothing. (The extensions module was set to back-convert adjusted domains before sending them to the Zero W, because the Zero W wasn't using the proxy.)

I also set a normal GNU/Linux laptop to mirror its screen to the projector if plugged in via HDMI; this was simply a matter of using arandr to find a suitable xrandr command that overlays both screens, and putting this in an infinite loop (with sleep 5) in a script which I called .screenlayout/projector.sh and placed in .config/lxsession/LXDE/autostart (as the laptop was running LXDE), so that the xrandr command would be executed shortly after any HDMI cable is plugged in. Ideally I should have used MiracleCast to imitate Android 4.4+'s "screen mirroring" over WiFi, but that required a systemd upgrade which would probably involve changing the entire distro, and as the user primarily wanted to use the iPhone rather than the laptop I thought this was unlikely to be worth the extra effort.


All material © Silas S. Brown unless otherwise stated.