SIXTEENmm

1743 films and counting...

Simple is Best - Blog

2020-01-25

Many web developers can get caught up in designing their stack, in trying to account for all the edge cases and trying to build trampolines to catch things that are unexpected.

This can lead to overpowered, difficult and complex, mixes of technologies.

Unfortunately the simple truth is that for most people this is completely pointless.


We are not a big website. We serve about 500Gb of data a week. That's a tiny amount of traffic when compared to all the giants in the field.

The giants need a highly complex stack to handle their data, we don't.

So, how do we serve our video, and manage users and all of that?


Our main backend is a simple Flask application. I do mean simple.

We've split up a number of things, but the Flask application itself weighs in at around 3000 LoC (Lines of Code).

This is not particularly scary, or impressive. It's about normal for all the things the site can do - categories, search, user settings, billing, streaming, etc.

There's no clever JavaScript assisting us, or ensuring that multiple kinds of video are streamed on a per-device basis. We do have multiple formats of video available, but we allow the browser to choose which one. We've prioritised the VP-9 format, but it's your device that makes that choice in the end.

<source src="{{url_for('get_video_stream', uuid=item['uuid'], format='webm')}}" type="video/webm">
<source src="{{url_for('get_video_stream', uuid=item['uuid'], format='mp4')}}" type="video/mp4">
<source src="{{url_for('get_video_stream', uuid=item['uuid'], format='ogv')}}" type="video/ogv">

This idea, of actually using the complexity of the browser to get what we want, rather than hacking it together with enormous payloads of JavaScript is spread throughout the site.

When you logout, for example, we do two things:

This header is underutilised, and makes the developers life so easy.

Clear-Site-Data is a header that:

clears browsing data (cookies, storage, cache) associated with the requesting website

All in one go, without a huge amount of difficulty remembering what state has been set or is required.

Most websites probably don't want to do this when you logout, because they want to continue to track you, to understand you, and target you.

SIXTEENmm isn't like that. We don't stalk you, because we don't want your information. You're already paying us if you're a user. Why would we want your information on top of that? The responsibility of guarding it is immense. No thanks.


We continue to use what the browser gives us everywhere we can. You may have noticed that the half-implemented browsers on no-longer-updated TVs and the like don't play very well with our videos.

We're trying to work around it, but at the moment we haven't found a satisfactory solution.

This is because, for the streaming work, we make use of Range Requests. Chrome, Safari, Firefox, Edge and even modern Internet Explorer will all try and make a range request the first time you play a video, only downloading what they need.

There's no need to drag in JavaScript to try and time the playing of a video with some custom request/buffering system. It's already been implemented for us, so we use it.

Unfortunately, badly behaved browsers don't all implement range requests, which means they only download the first \~3Mb of a video, which isn't very helpful. We are still thinking about how to get passed this without destroying the experience we already have.

By using standard video and source tags with Range Requests, we get a seamless experience, provided by the browser in a way that is highly accessible. Not just for a screen reader. Everything works as the user expects it will. No jumping. No waving the mouse erratically just to get a timeline to fail to appear.


JavaScript is only used where it can enhance the experience. It has to be seamless, so that the user doesn't realise that they're waiting for some program to run. It should happen as quietly as possible.

Our buttons for ignoring or adding to lists our one example, and probably the slowest on the site.

However, we also have shortcut keys for controlling the video, like skipping ahead. These are things a user expects to exist, but if they don't run, they don't break anything. The user still gets to watch their video, the user can still seek or control volume, etc.

We have a tiny bit of JS running in the background that records where you're up to, and updates your history list. This controls the resume function later on, allowing you to come back. But JS can fail to run at anytime for just about any reason, so you can't guarantee it will run.

Instead of trying to create a timing system that proactively retries, we allow it to fail. It doesn't disrupt the user much if the video resumes a minute earlier rather than thirty seconds.

These are the two halves of recording progress:

function measure_progress() {
    var el = document.getElementById('playingfilm');
    if(!el.paused) {
        // Fire AJAX query.
        // /progress/<uuid>/<timestamp>
        var url = '/progress/' + el.dataset.id + '/' + el.currentTime;
        fetch(url, { credentials: "same-origin" }).then(
                function(response) {}
            ).catch(
                function(response) {
                    console.log("Failed to record progress. Network failure.");
                }
            );
    }
}

@app.route('/progress/<uuid>/<timestamp>')
def record_progress(uuid, timestamp):
    if 'username' in session:
        username = session['username']
        data = filmdb.get_uuid(uuid)
        if not data:
            return ''
        userdata.add_history(username, uuid, timestamp, data['runtime'])
    return ''

A clever user could use their session cookie and create a timestamp the doesn't make sense. This will only affect them, and mean that the resume won't work for them. It won't stop the video from playing or destroy their history list or anything of the like.

Failure is acceptable. Which keeps things simple.

function set_resume() {
    var el = document.getElementById('playingfilm');
    if(el.paused) {
        var progress = el.dataset.progress;
        el.currentTime = progress;
    }
    el.addEventListener('ended', go_next);
}

If you set an invalid progress, then the browser will simply ignore it. There are no major side effects. No exfiltration paths to modify data that's supposed to be protected.

userdata.add_history actually does verify the timestamp makes sense. That's why the runtime is also passed to that particular function. It returns false and does nothing if it can't verify anything.


Simple is best. You don't need complexity to provide a decent experience.


Continue reading...