Update - It's up and running on my site, here's the link: - https://lachlanbarclay.net/quiz

After seeing Jimmy Carr's lockdown quiz, I thought it would be fun to film my own, except make it a bit more nerdy. So I filmed an episode:



… and then thought… hey, it would be great to have a leaderboard! And an easier way to enter responses on mobile.. and a way of keeping track of which episodes you've completed.

So being the nerd I am, I built a little nerd quiz web app.

Database Design

So what's the first step in building a great app? Getting the database schema designed properly. So here's attempt one:

One table for the quizzes. One table for the questions. One table for the answers. About as straightforward as it gets. But then it comes to storing the answers. I decided to have a table named UserQuizAnswer, which stores the particular answer that the user selected, for a particular quiz. This means I can query which questions they got right and wrong, and tally up their points.

After using this structure for a bit I realised that the UserQuizAnswer table is overkill. I don't really need to know what specific questions they got right or wrong, so for the moment, I'm just storing their overall score for the quiz. So here's take two:

Much simpler, easier to understand, and easier to query and update. Yes we don't know which questions they got wrong, but hey, I don't need to know that information. You can work it out for yourself by seeing the answers! :)

Building the App

The next step was to build a web app that queries this data and lets you answer the multiple choice questions.

My language of choice is asp.net core, so I added a few pages to my site to pull back the quiz, embed the video and give you a multiple choice questionnaire:

Nothing clever or particularly interesting going on here. The video is in a fixed position so you can scroll down the questionnaire while watching the video. It worked great on desktop… but on mobile it's very difficult to use. Keeping the video in place and the quiz answers proved to be a difficult bit of CSS.

So how about fixing the experience for mobile? How about we embed the video at the top, and show the correct multiple choices depending on where the video is currently playing? Is that even possible?

Well it turns out with iframe player api you're able to communicate with the video player and grab the current play position, and do other things like pause and resume playing the video. The code is pretty simple. First you add a div:

<div id="player"></div>

Then you add some code that loads the API's javascript:

var tag = document.createElement('script'); tag.src = "https://www.youtube.com/iframe_api"; var firstScriptTag = document.getElementsByTagName('script')[0]; firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);

This code loads in youtube's javascript for interacting with the video. This code will call a function named:

onYouTubeIframeAPIReady()

That you've previously added to the page. Once you're inside that function, you can load up a video:

var player; function onYouTubeIframeAPIReady() { player = new YT.Player('player', { height: '390', width: '640', videoId: 'M7lc1UVf-VE', events: { 'onReady': onPlayerReady, 'onStateChange': onPlayerStateChange } }); }

And you now have a player object that lets you interact with the video. You can play it, pause it, move to a different location, set the volume, and even load in other videos. It also fires a few events like when the user pauses the video.

Unfortunately there's no regular event fired when the user is playing a video to detect where they are currently watching (like at 50 seconds in). So the easiest way to do that is good old (horrible) polling:

videoMonitor = setInterval(function () { var videoCurrentTimeInSeconds = player.getCurrentTime(); console.log("Currently at location: " + videoCurrentTimeInSeconds); }, 500);

This bit of code fires twice a second and asks the video for the current play location. Works great:

Beautiful! It's now a simple matter to work out which question we are up to, and display the correct set of answers:

var questionLocations = [0, 33, 55, 83]; for (var i = 0; i < questionLocations.length-1; i++) { var startTime = questionLocations[i]; var endTime = questionLocations[i + 1]; if (videoCurrentTimeInSeconds >= startTime && videoCurrentTimeInSeconds < endTime) { var newQuestionID = i + 1; if (currentQuestion != newQuestionID) { $("#Question" + currentQuestion).hide(); $("#Question" + newQuestionID).show(400); currentQuestion = newQuestionID; } } }

And voila, we have a dynamic quiz that you can play on your mobile!

I've also added little question buttons that let you skip to a question, in case you missed one. Plus if you don't answer a question in time, it will pause the video to give you more time to answer - and then once you've selected your answer, it unpauses the video. The overall experience is quite nice.

Bugs

As usual there were a few problems - the main one was that on the iphone, the video maximises to full screen upon hitting play. This was a problem, but luckily there's a little workaround for this. You can specify a flag named "playsinline" which stops it from happening. There's a bunch of different player variables you can pass through that will affect the player. In this example, I'm also disabling the full screen option and also only showing related videos that are on my channel:

player = new YT.Player('player', { width: newWidth, height: newHeight, videoId: youtubeVideoCode, playerVars: { 'playsinline': '1', 'rel': '0', 'fs': '0' }, events: { 'onReady': onPlayerReady, 'onStateChange': onPlayerStateChange } });

Unfortunately there's no way to not display related videos when it's paused. It sucks that they've done this, and there's nothing you can do about it. Oh well!

Automated build and release

For my site, (lachlanbarclay.net), I've been using github actions to build and deploy my application. The overall experience is really good - it's fast and it's reliable. That's all I care about. It's actually been great. Upon a code push the changes appear on the site in about 80 seconds.

What could I do better?

Overall I'm quite happy with this solution and it's working well, but of course always love to hear feedback on better approaches! What's something obvious that I've missed?