Create a remote control for Black Mirror: Bandersnatch with PubNub

Elvis Wolcott
Mar 21 · 20 min read

While watching Bandersnatch with a group of friends I encountered an unexpected issue. Often, the group was unable to come to a consensus before time ran out. To make watching Bandersnatch with a group easier, we’ll create a remote control designed specifically for Bandersnatch so that everyone can vote for their favorite option from their phone. This makes for a fun thematic twist when watching with others — nobody is completely in control of what path the group follows.

This project consists of a Chrome extension and a web app. The Chrome extension interacts with Netflix and counts votes while the web app collects them. We’ll write the Chrome extension in JavaScript and run it through Babel with Webpack to ensure it works with older versions of Chrome. Then, we’ll write a web app with Vue.js that handles collecting votes and sending them to to the extension. If you haven’t used any of these tools before, don’t worry. We’ll walk through the process of setting everything up and go through the code to understand how it all works.

If you want to try out the final product first or would like to watch Bandersnatch now (there are spoilers later on), you can try out a live version of the project here. The full source for this project is available on GitHub.

Spoiler Alert! There are some unique pathways which required the development of special cases. These pathways are mentioned throughout the project.


General Overview

PubNub allows us to send device-to-device messages instantaneously using a publish/subscribe model. A Chrome extension detects the available options while Bandersnatch is playing and publishes them to a channel where the web app is subscribed. The web app displays the options it receives from the extension and waits for a selection. When an option is selected, it publishes it to a channel where the extension is subscribed and the extension records the vote. When time runs out for the choice, the extension will select the most popular option go back to looking for options.


Setup

Before we can start the project, we need to get our development environment set up, add some configuration files, and sign up for free PubNub keys.

Setup your development environment

I strongly recommend using Visual Studio Code for this project. It has syntax highlighting for JavaScript, git support out of the box, and allows you to use the command line from within the editor. If you go with Visual Studio Code, the Vetur extension will add syntax highlighting for the Vue.js single file components we use later on.

You’ll also need Node.js so we can use NPM (Node Package Manager) to download some dependencies. The latest version is available for download here.

To test the project you’ll need a computer with Chrome and a Netflix account. Optionally, a mobile device will allow you to test the scanning feature of the web app.

To organize everything, start by creating a directory named . This will contain all of the code, assets, and build tools for the project.

Get your PubNub keys

  1. Sign up for a PubNub account or login to an existing account from the PubNub dashboard
  2. Click the big red button labeled
  3. Enter a name for your app (I went with “Bandersnatch PAC”) and click
  4. Select your new app and then select the
  5. Create a file named and paste in the snippet below. Replace the and with your keys from the PubNub dashboard

Specify a deploy path

Our extension and app will need to know where the website is hosted for generating and processing QR codes. Create and paste in the JSON below. When we deploy the web app at the end we’ll update this to reflect the publicly accessible URL.


Building the Chrome extension

We’re going to start with our extension because it handles most of the logic and interacting with Netflix.

Install dependencies

Create a directory named in the project directory. This is where we will put everything for the extension. In this directory, create with the following code and then . This might take a few minutes.

Our has 3 important parts.

The are packages that we use in our own code. We’re using for real time messaging and to generate QR codes.

The are packages that we use to build our code. does most of the heavy lifting of bundling our files together and allowing use to include packages from NPM in a browser application. handles compiling modern ES6 JavaScript (which has poor browser support, but is easier to read) into JavaScript that older versions of Chrome will understand. We also have to format our code and keep it readable.

The is a list of commands for building the extension and formatting the code. will build and package the extension and will format the code with Prettier.

We also need to create so Webpack knows how to build our source code into the extension.

The tell Webpack how to process files. We only have one rule, which tells Webpack to use Babel to process all of our files. Babel makes sure our fancy ES6 JavaScript is compatible with older browsers.

The specifies the entry points for the project. Webpack starts at each entry point and bundles it with all of the imported modules. Each entry point corresponds to one output file.

The contains a bit of custom setup for working with extensions. The copies files from the directory into . We use this to copy the HTML for the page action, images, and the extension manifest. There is also which creates a zip of the directory. This is helpful because the Chrome web store requires a zip of the extension to submit for publication.

Chrome extension elements:

The extension manifest contains metadata like the name, icons, permissions, and scripts required for the extension.

Content scripts run in the context of a tab. They can interact with and mutate the DOM, but can’t use the features of the and APIs.

Background scripts orchestrate the extension’s behavior and can access all of the extension APIs, but do not run in the tab’s context so they can not access the DOM.

Page actions provide a popup when the extension icon is clicked. They allow the extension UI to be separate from the website it is interacting with.

Extension manifest

We’ll need to create a directory to put all our code and assets.

The extension manifest tells Chrome what tabs to inject content scripts into, where our files are, and the basic metadata. Our manifest defines a background script, a content script, and a page action. We’ll work on developing these next. Create and place it in .

Icons

Chrome needs the icons provided in various sizes to support different screen resolutions. Create an directory and save the images from the repository. Alternatively, you can create your own icon in Illustrator and export with the option checked using scales of , , , , , , and to create all the necessary versions.

Helpers

In order to make writing the extension easier I have provided some helper functions. Copy both files into .

These files provide a few useful functions.

Background script

The background script does not run in the tab’s context, so it is best to use it for networking in order to avoid potentially impacting video playback. It’s the first part of the extension to run, so it also handles some house keeping like injecting content into the Netflix tab and creating a channel code.

For our background script we’ll need a file named .

At the top of our file we want all of our imports. This pulls in some helper functions as well as PubNub and our PubNub keys.

The first thing we want to do is create a channel code. This is a unique identifier that will be used so that users can connect to the extension from the web app. It will also be the channel name used for PubNub. On startup, we’ll check to see if there’s already a code in , if there isn’t we’ll generate a new code.

Then, we can use the helper functions to start a port for communicating with other tabs.

We also want our background script to handle requests to inject content scripts because our other scripts lack the necessary API access.

Now that we can talk to other tabs, we want to add a listener that will enable our page action when a tab requests it.

The most important task for the background script is recording and tallying votes. We can do this by putting the votes in a JSON object and then finding the most popular when time runs out. We have deal with our first special pathway here. In one scene there is a 5 digit phone number instead of a binary choice. This means we’ll get an array of 5 choices from the app instead of just 1. We need to handle this slightly differently by encoding the votes as a string during the vote counting process so we can use them as a key.

We’ve got all our vote counting figured out, but we still need to connect to PubNub and subscribe to our channel so we can actually get votes.

When we get a message that is a we want to count the vote immediately. In the special case where we get a our options will be an array instead, so they need to be stringified first.

Our content script will send two different types of messages to pass to the web app. A options message with the available pathways and an end message with the timestamp where voting will close. For both of these we want to immediately pass the message from the content script to PubNub.

Content Script

The content script runs in the tab’s context which gives it access to the DOM. This allows it to detect changes in the page and extract the options to send to the background script. It also allows the extension to interact with the page to select options.

First we want to create a content script. This will check if the tab is actually Bandersnatch (not another title on Netflix) and then inject the main content script.

Our main content script goes in . Just like with the background script, we’ll start by importing some helper functions

Then, we’ll start a port to talk to the background script.

We also want the page action to be enabled, so we’ll ask the background script to enable the tab.

The challenging part is scraping the page to extract the information we need for the web app. For poking around in the page Chrome’s developer tools are invaluable. You can open developer tools with or by clicking the three dots in the top right and going to . allows you to inspect elements as you hover over them so you can see how Netflix renders the choices. or will pause script execution, which allows you to pause the countdown and look through the DOM with out a 10 second limit. Adding breakpoints to events like and in the panel is also useful for debugging click interactions.

The first thing we have to figure out is how to determine the options that are on screen. It turns out that all the choices have the class . This means a simple query will return all the choices. Most of the time the choices are just elements with text inside. In one scene, the options are images instead. In this case, they instead contain another with the class which has a background image instead of text. Knowing this, we can support choices that are and choices that are an and adjust our behavior accordingly.

In addition to extracting the options, we want to be able to select the winner after voting. We can do this by faking a click on one of the elements and adding a listener to our port. Because of how Netflix built their UI, a simple will not select an option. Instead, a followed by will simulate a click.

At this point, we aren’t running yet because we don’t know when there will be choices. Instead of running our function over and over (and slowing down the page) we can use a to watch for changes to the DOM. Our mutation observer will watch for any changes to the element with the class (Netflix player) and run our function . We can wait 5 seconds to register this mutation observer at the start in order to not impact the initial player load.

Our function will determine if we should check for the options. When ever options become available an element with the class is added, so we can wait for this mutation to . In , we want to get the options and then add a temporary mutation observer to look for the attribute which Netflix uses to store the duration of the decision. This comes as much as a second or two later (because the options are added to the DOM before they are visible) so by sending it as a separate message we can ensure that the options are not delayed.

Page Action

The page action presents the user with the domain of the remote app, the channel code, and a join link in the form of a QR code when the extension icon is clicked. This requires an HTML file and a small script.

Create . All we need is some white text on a black background and a element for our QR code.

In we’ll get the channel code and use the module to paint a QR code to the canvas in the page action.

… and we’re done with the extension!

To test it out you’ll need to build using the command. This should be run from , not . The built extension will be saved to . Open Chrome and navigate to chrome://extensions. Make sure is turned on in the top right corner. Then, click and find your directory. If you go to Netflix and start watching Bandersnatch the icon should turn red, and clicking it should open the page action.

Head back to the PubNub dashboard, select your project and then the . Select the on the left side and click the settings icon. Go to and paste the channel code from the page action for the and , then . As you watch Bandersnatch, you should start to see options come in to the console shortly before they appear on screen, followed by a message with the end timestamp.

You can vote from the console by sending the command.


Building the web app

The second part of the system, the web app, handles displaying options and collecting votes. We’ll write the web app in Vue and use Vue Router to handle routing between the landing page and remote.

Setup

To make creating a web app with Vue easier, we can use the Vue Command Line Interface (Vue CLI). will install it globally on your machine. With the CLI installed, from the project directory to make a new project.

For the preset choose and enable , ,, and . When prompted, select and choose for the CSS pre-processor and for the linter / formatter. Don’t change the setting and place config . You don’t need to save these settings as a preset.

It may take a few minutes to install all the dependencies and generate files for the project. Once the project has been created to install the rest of the dependencies for the web app.

Before we work on our UI, we need to adjust the generated project a little bit for our purposes. First, replace the contents of . This will change the routing paths for our app so that it defaults to using for the default path and for the path. It also will extract the channel as a parameter from the URL. For example, navigating to will pass us in .

We also need to adjust the component which contains the views from the router. We don’t have any page navigation that stays between views so our component just needs to hold the router view.

To make styling our app easier, we need a theme file that includes the colors and fonts for our app. The theme file should go in the directory.

To be able to use these fonts we need to update our to load them from Google Fonts.

Place in the directory and in the directory and we’ll be ready to get started with writing our views.

Join page

QR code scanning makes the connection process nearly instant

contains the logic and UI for connecting to the extension. For Vue components there are three sections, the template (HTML), script (JS), and style (SASS).

The template for our Join component has 3 elements which contain the sections of our UI. The contains the logo for the app and is always visible. The holds the video output from the camera, but is only enabled when we set to . When is , the component is rendered instead.

Syntax note: In Vue, the directive only renders an element if the expression evaluates to true. An element with would never be rendered and an element with would always be rendered.

Understanding Vue: Expressions in our template are always evaluated in our Vue. This means that function calls will run a function from the of our view and variables will come from the and fields of the Vue instance.

For the scanner, all we need is a to draw the camera output to and a message for the users.

If the user doesn’t have a camera or denies access, it falls back to manual entry seamlessly

To accept manual entry, the markup is a bit more complex. All of the input is going to go into a . Our form has event listeners so that we can respond to /, and the and arrows. The form contains elements for accepting digits and a to submit. The is the most interesting element here. The inputs are rendered from a list with to keep our markup simple. Each is bound to a variable in our Vue assistance and has properties so that the numeric keyboard will show up on phones.

Syntax note: In Vue, the directive is shorthand for . This adds an event listener using a function from the on the Vue instance. For example, runs the function whenever a keypress for delete or backspace occurs on the element.

Syntax Note: In Vue, the directive is shorthand for . This binds a property or piece of content to a piece of on the Vue instance. This way, the DOM will update whenever a change is made to the from our scripts.

Syntax Note: In Vue, the directive allows list rendering. The element with the directive is rendered once for each item in the iterator and gains that item in its context. For example, iterates through the array digits and renders the element each time. For each element that is rendered, the from the array and the in the array are available to be bound to.

Syntax Note: In Vue, the directive performs two way data binding. Like with , the value will update when the model is changed. Additionally, if the value of the input is changed the model will change as well.

Most of the work as far as handling the form and scanning happens in the script.

As always, we start by importing the dependencies that we need (this should go in a script tag in your component).

All of the logic goes inside the Vue instance. There’s 4 pieces worth looking at.

The is the heart of our Vue. The properties we put there become reactive, causing the markup bound to their values to re-render when anything changes. To start, we have an array for 12 empty which are used to render the input fields with and are bound to the values of the input with . This way, when the input value changes or we change the value of the data, they are connected and change together. The other fields are , which tracks the current position in the 12 digits and which dictates whether the camera or manual input will be used. The data is accessed inside our Vue through .

The fields contains functions for our app to use. This is where we will put our event listeners for key presses and DOM events (like focus and click). The first method we need is which navigates to the next page when it is submitted. To accomplish this, we take the values of all the input values and trim them to 1 digit, then add them to the path and insert a where needed.

The rest of the methods deal with treating our 12 separate inputs as 1. By listening to key presses and focus events from the form with we can move the cursor through the inputs as if we were dealing with a single field.

We also need a watcher. Watchers are a function that run whenever the piece of data on our Vue is updated. Our watcher will trim and validate any input and advance to the next input to make the transition between inputs seamless.

The most complex portion is the function which runs when the template has been added to the DOM. At this point, we can start dealing with the camera and render its output. Because not all devices have cameras, we need to check for one first. If there is no camera the video output is disabled and manual entry is used instead by setting to . When the camera is available, each frame is fed into to be processed for a QR code. When a code is found, it navigates to the next page.

Finally, we can add some styling using the color theme we created earlier. We don’t need to do anything particularly fancy except add some responsive breakpoints so the inputs will scale with the screen.

Unfortunately, we can’t test our app out quite yet because we’re missing the component.

Channel (remote) page

The component is pretty simple, and once we finish it up we’ll be ready to try everything out.

This time our isn’t quite as complex. At the top there is a countdown bar which mirrors the countdown on screen. Below, it renders items from the and depending on whether the is or an it displays the as text or uses it as the URL for an image. In the case of scenes where multiple selections or required, it displays the selected options as the user enters them.

The script for our component handles connecting to PubNub and sending and receiving messages. When a message is received with it, updates the Vue data. When a message with a is received, the countdown is started. Using a click listener, the selected option is sent back the the extension and input is disabled to prevent one user casting multiple votes. There’s nothing too interesting going on here, so it’s not worth digging into the code.

Finally, we can add some styles to mimic the Bandersnatch UI.

With all our components complete, the web app is ready to test with . Navigating to http://localhost:8080/ should take you to the join page. Unfortunately, we can’t access the camera without HTTPS, so we can only use manual entry. To try out code scanning, we’ll have to deploy the app behind HTTPS. Ee’ll use Netlify for the sake of simplicity, so we don’t have to deal with servers or hosting.


Deploy with Netlify

Before we deploy, we can add a few commands to our files that will handle tasks like installing dependencies and building our source to make development easier in the future.

In the project directory, run and follow the prompts to create an empty .

Then, add these fields to the section. They allow us to lint and build the extension and the web app from the project directory. They also add a command which we will need to deploy our site.

In the section of add these scripts to lint and build the extension. They aren’t required to deploy the web app, but they’ll make rebuilding the extension more convenient in the future.

Finally, to complete our command, we need to create the script that is run by to generate . takes the environment variables during a build and writes them to . Using this script, we can insert our keys into our Netlify build even though will be ignored by git.

To deploy to Netlify we need to setup version control with git. In the project directory add a file. This tells git to not commit the built versions, npm packages, and because we don’t need this files tracked in git. and run . Then, login to your preferred git provider (GitHub, GitLab, or BitBucket) and create a new repository. They should give you URL for your repository that ends in . Run , replacing with the URL from your git provider. Then, to add all the files to your repository.

Now, login or sign up for a Netlify account. Click , select your git provider, and then link your account. You should see a list of repositories. Select the repository you created for this project. For the enter and for enter . Open the and click twice. For one, make the key and for the other make it . Fill in the values with your keys from . Because we don’t commit our , it needs to be recreated on the build server from these environment variables. This is handled by the script we created earlier. Click to put it online. You should get a domain where you project is hosted and be able to visit the web app in your browser.

Finally, replace with the Netlify URL in and commit the changes with . This allows the QR code to be scanned by any QR code scanner instead of only working with the scanner within the web app.

Anybody to can access the site now, and can join in if you share the code from your extension. To give others access to the extension you’ll need to share a the zip from with them. You can also submit it to the Chrome webstore, but the submission process is out of the scope for this write up.

With your site deployed, you can open the web app from your browser. If you enter or scan the code from your extension the app will connect and your can start controlling Bandersnatch from your phone.


Wrap Up

In this project, we developed a Chrome extension using JavaScript and Webpack that can detect the options and time while viewing Bandersnatch and calculate the winner of a vote. We also built a modern web app with Vue.js that allows users to connect to the extension, displays options and collects votes. Using PubNub, we were able to connect the two together with no visible latency. At the end, we made our web app public using Netlify for serverless deployment.

Elvis Wolcott

Written by

Developer with an interest in design.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade